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