Frameworks/Adium Framework/Source/AIContactMenu.m
author Zachary West <zacw@adium.im>
Fri Oct 16 11:14:37 2009 -0400 (2009-10-16)
changeset 2621 f50210ffe03f
parent 2605 3b3a28a01bc0
child 3165 f524212f94f7
permissions -rw-r--r--
Don't try and set a title if it's going to be nil. I have no idea why this is happening, probably something busted for the user. Fixes #12772.
     1 //
     2 //  AIContactMenu.m
     3 //  Adium
     4 //
     5 //  Created by Adam Iser on 5/31/05.
     6 //  Copyright 2006 The Adium Team. All rights reserved.
     7 //
     8 
     9 #import <Adium/AIContactControllerProtocol.h>
    10 #import <Adium/AISortController.h>
    11 #import <Adium/AIContactMenu.h>
    12 #import <AIUtilities/AIMenuAdditions.h>
    13 #import <AIUtilities/AIParagraphStyleAdditions.h>
    14 #import <Adium/AIListContact.h>
    15 #import <Adium/AIListGroup.h>
    16 #import <Adium/AIContactList.h>
    17 
    18 @interface AIContactMenu ()
    19 - (id)initWithDelegate:(id<AIContactMenuDelegate>)inDelegate forContactsInObject:(AIListObject *)inContainingObject;
    20 - (NSArray *)contactMenusForListObjects:(NSArray *)listObjects;
    21 - (NSArray *)listObjectsForMenuFromArrayOfListObjects:(NSArray *)listObjects;
    22 - (void)_updateMenuItem:(NSMenuItem *)menuItem;
    23 @end
    24 
    25 @implementation AIContactMenu
    26 
    27 /*!
    28  * @brief Create a new contact menu
    29  * @param inDelegate Delegate in charge of adding menu items
    30  * @param inContainingObject Containing contact whose contents will be displayed in the menu, nil for all contacts/groups
    31  */
    32 + (id)contactMenuWithDelegate:(id<AIContactMenuDelegate>)inDelegate forContactsInObject:(AIListObject *)inContainingObject
    33 {
    34 	return [[[self alloc] initWithDelegate:inDelegate forContactsInObject:inContainingObject] autorelease];
    35 }
    36 
    37 /*!
    38  * @brief Init
    39  * @param inDelegate Delegate in charge of adding menu items
    40  * @param inContainingObject Containing contact whose contents will be displayed in the menu, nil for all contacts/groups
    41  */
    42 - (id)initWithDelegate:(id<AIContactMenuDelegate>)inDelegate forContactsInObject:(AIListObject *)inContainingObject
    43 {
    44 	if ((self = [super init])) {
    45 		[self setDelegate:inDelegate];
    46 		containingObject = [inContainingObject retain];
    47 
    48 		// Register as a list observer
    49 		[[AIContactObserverManager sharedManager] registerListObjectObserver:self];
    50 		
    51 		// Register for contact list order notifications (so we can update our sorting)
    52 		[[NSNotificationCenter defaultCenter] addObserver:self
    53 									   selector:@selector(contactOrderChanged:)
    54 										   name:Contact_OrderChanged
    55 										 object:nil];
    56 
    57 		[self rebuildMenu];
    58 	}
    59 	
    60 	return self;
    61 }
    62 
    63 - (void)dealloc
    64 {
    65 	[[AIContactObserverManager sharedManager] unregisterListObjectObserver:self];
    66 	[[NSNotificationCenter defaultCenter] removeObserver:self];
    67 
    68 	[containingObject release]; containingObject = nil;
    69 	delegate = nil;
    70 	
    71 	[super dealloc];
    72 }
    73 
    74 /*!
    75  * @brief Set the containing object
    76  *
    77  * Updates the containing object, and rebuilds the menu items.
    78  */
    79 - (void)setContainingObject:(AIListObject *)inContainingObject
    80 {
    81 	[containingObject release];
    82 	
    83 	containingObject = [inContainingObject retain];
    84 	
    85 	[self rebuildMenu];
    86 }
    87 
    88 /*!
    89  * @brief Returns the existing menu item for a specific contact
    90  *
    91  * @param contact AIListContact whose menu item to return
    92  * @return NSMenuItem instance for the contact
    93  */
    94 - (NSMenuItem *)existingMenuItemForContact:(AIListContact *)contact
    95 {
    96 	return (menuItems ? [self menuItemWithRepresentedObject:contact] : nil);
    97 }
    98 
    99 - (void)contactOrderChanged:(NSNotification *)notification
   100 {
   101 	AIListObject *changedObject = [notification object];
   102 	if (changedObject && changedObject == containingObject) {
   103 		[self rebuildMenu];
   104 	}
   105 }
   106 
   107 //Delegate -------------------------------------------------------------------------------------------------------------
   108 #pragma mark Delegate
   109 /*!
   110  * @brief Set our contact menu delegate
   111  */
   112 - (void)setDelegate:(id<AIContactMenuDelegate>	)inDelegate
   113 {
   114 	delegate = inDelegate;
   115 	
   116 	//Ensure the the delegate implements all required selectors and remember which optional selectors it supports.
   117 	if (delegate) NSParameterAssert([delegate respondsToSelector:@selector(contactMenu:didRebuildMenuItems:)]);
   118 	delegateRespondsToDidSelectContact = [delegate respondsToSelector:@selector(contactMenu:didSelectContact:)];
   119 	delegateRespondsToShouldIncludeContact = [delegate respondsToSelector:@selector(contactMenu:shouldIncludeContact:)];
   120 	delegateRespondsToValidateContact = [delegate respondsToSelector:@selector(contactMenu:validateContact:)];
   121 
   122 	shouldUseUserIcon = ([delegate respondsToSelector:@selector(contactMenuShouldUseUserIcon:)] &&
   123 								 [delegate contactMenuShouldUseUserIcon:self]);
   124 	
   125 	shouldUseDisplayName = ([delegate respondsToSelector:@selector(contactMenuShouldUseDisplayName:)] &&
   126 							[delegate contactMenuShouldUseDisplayName:self]);
   127 	
   128 	shouldDisplayGroupHeaders = ([delegate respondsToSelector:@selector(contactMenuShouldDisplayGroupHeaders:)] &&
   129 								 [delegate contactMenuShouldDisplayGroupHeaders:self]);
   130 	
   131 	shouldSetTooltip = ([delegate respondsToSelector:@selector(contactMenuShouldSetTooltip:)] &&
   132 								 [delegate contactMenuShouldSetTooltip:self]);	
   133 }
   134 - (id<AIContactMenuDelegate>	)delegate
   135 {
   136 	return delegate;
   137 }
   138 
   139 /*!
   140  * @brief Inform our delegate when the menu is rebuilt
   141  */
   142 - (void)rebuildMenu
   143 {
   144 	[super rebuildMenu];
   145 	
   146 	// Update our values for display name and group header options.
   147 	shouldUseDisplayName = ([delegate respondsToSelector:@selector(contactMenuShouldUseDisplayName:)] &&
   148 							[delegate contactMenuShouldUseDisplayName:self]);
   149 	
   150 	shouldDisplayGroupHeaders = ([delegate respondsToSelector:@selector(contactMenuShouldDisplayGroupHeaders:)] &&
   151 								 [delegate contactMenuShouldDisplayGroupHeaders:self]);
   152 	
   153 	[delegate contactMenu:self didRebuildMenuItems:[self menuItems]];
   154 }
   155 
   156 /*!
   157  * @brief Inform our delegate of menu selections
   158  */
   159 - (void)selectContactMenuItem:(NSMenuItem *)menuItem
   160 {
   161 	if (delegateRespondsToDidSelectContact) {
   162 		[delegate contactMenu:self didSelectContact:[menuItem representedObject]];
   163 	}
   164 }
   165 
   166 
   167 //Contact Menu ---------------------------------------------------------------------------------------------------------
   168 #pragma mark Contact Menu
   169 /*!
   170  * @brief Build our contact menu items
   171  */
   172 - (NSArray *)buildMenuItems
   173 {
   174 	NSArray *listObjects = nil;
   175 	
   176 	// If we're not given a containing object, use all the contacts
   177 	if (containingObject == nil) {
   178 		listObjects = adium.contactController.useContactListGroups ? adium.contactController.allGroups : adium.contactController.allContacts;
   179 
   180 		/* The contact controller's -allContacts gives us an array with meta contacts expanded
   181 		 * Let's put together our own list if we need to. This also gives our delegate an opportunity
   182 		 * to decide if the contact should be included.
   183 		 */
   184 		if (!shouldDisplayGroupHeaders) {
   185 			listObjects = [self listObjectsForMenuFromArrayOfListObjects:listObjects];
   186 		}
   187 
   188 		// Sort what we're given
   189 		//XXX is this container right?
   190 		listObjects = [listObjects sortedArrayUsingActiveSortControllerInContainer:adium.contactController.contactList];
   191 	} else {
   192 		// We can assume these are already sorted
   193 		listObjects = [self listObjectsForMenuFromArrayOfListObjects:([containingObject conformsToProtocol:@protocol(AIContainingObject)] ?
   194 																	  [(AIListObject<AIContainingObject> *)containingObject uniqueContainedObjects] :
   195 																	  [NSArray arrayWithObject:containingObject])];
   196 	}
   197 	
   198 	// Create menus for them
   199 	return [self contactMenusForListObjects:listObjects];
   200 }
   201 
   202 /*!
   203 * @brief Creates an array of list objects which should be presented in the menu, expanding any containing objects
   204  */
   205 - (NSArray *)listObjectsForMenuFromArrayOfListObjects:(NSArray *)listObjects
   206 {
   207 	NSMutableArray	*listObjectArray = [NSMutableArray array];
   208 	
   209 	for (AIListObject *listObject in [[listObjects copy] autorelease]) {
   210 		if ([listObject isKindOfClass:[AIListContact class]]) {
   211 			/* Include if the delegate doesn't specify, or if the delegate approves the contact.
   212 			 * Note that this includes a metacontact itself, not its contained objects.
   213 			 */
   214 			if (!delegateRespondsToShouldIncludeContact || [delegate contactMenu:self shouldIncludeContact:(AIListContact *)listObject]) {
   215 				if (delegateRespondsToValidateContact)
   216 					listObject = [delegate contactMenu:self validateContact:(AIListContact *)listObject];
   217 				if (listObject)
   218 					[listObjectArray addObject:listObject];
   219 			}
   220 
   221 		} else if ([listObject isKindOfClass:[AIListGroup class]]) {
   222 			[listObjectArray addObjectsFromArray:[self listObjectsForMenuFromArrayOfListObjects:[(AIListGroup *)listObject uniqueContainedObjects]]];
   223 		}
   224 	}
   225 	
   226 	return listObjectArray;
   227 }
   228 
   229 /*!
   230 * @brief Creates an array of NSMenuItems for each AIListObject
   231  */
   232 - (NSArray *)contactMenusForListObjects:(NSArray *)listObjects
   233 {
   234 	NSMutableArray	*menuItemArray = [NSMutableArray array];
   235 	
   236 	for (AIListObject *listObject in listObjects) {
   237 		// Display groups inline
   238 		if ([listObject isKindOfClass:[AIListGroup class]]) {
   239 			NSArray			*containedListObjects = [self listObjectsForMenuFromArrayOfListObjects:[(AIListObject<AIContainingObject> *)listObject uniqueContainedObjects]];
   240 			
   241 			// If there's any contained list objects, add ourself as a group and add the contained objects.
   242 			if ([containedListObjects count] > 0) {
   243 				// Create our menu item
   244 				NSMenuItem *menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:@""
   245 																							target:self
   246 																							action:nil
   247 																					 keyEquivalent:@""
   248 																				 representedObject:listObject];
   249 
   250 				// The group isn't clickable.
   251 				[menuItem setEnabled:NO];
   252 				[self _updateMenuItem:menuItem];
   253 				
   254 				// Add the group and contained objects to the array.
   255 				[menuItemArray addObject:menuItem];
   256 				[menuItemArray addObjectsFromArray:[self contactMenusForListObjects:containedListObjects]];
   257 				
   258 				[menuItem release];
   259 			}
   260 		} else {
   261 			// Just add the menu item.
   262 			NSMenuItem *menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:@""
   263 																						target:self
   264 																						action:@selector(selectContactMenuItem:)
   265 																				 keyEquivalent:@""
   266 																			 representedObject:listObject];
   267 			[self _updateMenuItem:menuItem];
   268 			[menuItemArray addObject:menuItem];
   269 			[menuItem release];
   270 		}
   271 
   272 	}
   273 	
   274 	return menuItemArray;
   275 }
   276 
   277 /*!
   278  * @brief Update a menu item to reflect its contact's current status
   279  */
   280 - (void)_updateMenuItem:(NSMenuItem *)menuItem
   281 {
   282 	AIListObject	*listObject = [menuItem representedObject];
   283 	
   284 	if (listObject) {
   285 		[[menuItem menu] setMenuChangedMessagesEnabled:NO];
   286 
   287 		if ([listObject isKindOfClass:[AIListContact class]]) {
   288 			[menuItem setImage:[self imageForListObject:listObject usingUserIcon:shouldUseUserIcon]];
   289 		}
   290 		
   291 		NSString *displayName = listObject.displayName;
   292 		
   293 		if (!displayName || (!shouldUseDisplayName && listObject.formattedUID)) {
   294 			displayName = listObject.formattedUID;
   295 		}
   296 		
   297 		if (displayName)
   298 			[menuItem setTitle:displayName];
   299 		
   300 		[menuItem setToolTip:(shouldSetTooltip ? [listObject.statusMessage string] : nil)];
   301 
   302 		[[menuItem menu] setMenuChangedMessagesEnabled:YES];
   303 	}
   304 }
   305 
   306 /*!
   307  * @brief Update menu when a contact's status changes
   308  */
   309 - (NSSet *)updateListObject:(AIListObject *)inObject keys:(NSSet *)inModifiedKeys silent:(BOOL)silent
   310 {
   311 	if ([inObject isKindOfClass:[AIListContact class]]) {
   312 
   313 		//Update menu items to reflect status changes
   314 		if ([inModifiedKeys containsObject:@"Online"] ||
   315 			[inModifiedKeys containsObject:@"Connecting"] ||
   316 			[inModifiedKeys containsObject:@"Disconnecting"] ||
   317 			[inModifiedKeys containsObject:@"IdleSince"] ||
   318 			[inModifiedKeys containsObject:@"StatusType"]) {
   319 
   320 			//Note that this will return nil if we don't ahve a menu item for inObject
   321 			NSMenuItem	*menuItem = [self existingMenuItemForContact:(AIListContact *)inObject];
   322 
   323 			//Update the changed menu item (or rebuild the entire menu if this item should be removed or added)
   324 			if (delegateRespondsToShouldIncludeContact) {
   325 				BOOL shouldIncludeContact = [delegate contactMenu:self shouldIncludeContact:(AIListContact *)inObject];
   326 				BOOL menuItemExists		  = (menuItem != nil);
   327 				//If we disagree on item inclusion and existence, rebuild the menu.
   328 				if (shouldIncludeContact != menuItemExists) {
   329 					[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(rebuildMenu) object:nil];
   330 
   331 					if (silent) {
   332 						//If it's silent, wait for a pause before performing the actual rebuild
   333 						[self performSelector:@selector(rebuildMenu) withObject:nil afterDelay:1.0];
   334 
   335 					} else {
   336 						[self rebuildMenu];
   337 					}
   338 				} else { 
   339 					[self _updateMenuItem:menuItem];
   340 				}
   341 			} else {
   342 				[self _updateMenuItem:menuItem];
   343 			}
   344 		}
   345 	}
   346 	
   347     return nil;
   348 }
   349 
   350 @end