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.
5 // Created by Adam Iser on 5/31/05.
6 // Copyright 2006 The Adium Team. All rights reserved.
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>
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;
25 @implementation AIContactMenu
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
32 + (id)contactMenuWithDelegate:(id<AIContactMenuDelegate>)inDelegate forContactsInObject:(AIListObject *)inContainingObject
34 return [[[self alloc] initWithDelegate:inDelegate forContactsInObject:inContainingObject] autorelease];
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
42 - (id)initWithDelegate:(id<AIContactMenuDelegate>)inDelegate forContactsInObject:(AIListObject *)inContainingObject
44 if ((self = [super init])) {
45 [self setDelegate:inDelegate];
46 containingObject = [inContainingObject retain];
48 // Register as a list observer
49 [[AIContactObserverManager sharedManager] registerListObjectObserver:self];
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
65 [[AIContactObserverManager sharedManager] unregisterListObjectObserver:self];
66 [[NSNotificationCenter defaultCenter] removeObserver:self];
68 [containingObject release]; containingObject = nil;
75 * @brief Set the containing object
77 * Updates the containing object, and rebuilds the menu items.
79 - (void)setContainingObject:(AIListObject *)inContainingObject
81 [containingObject release];
83 containingObject = [inContainingObject retain];
89 * @brief Returns the existing menu item for a specific contact
91 * @param contact AIListContact whose menu item to return
92 * @return NSMenuItem instance for the contact
94 - (NSMenuItem *)existingMenuItemForContact:(AIListContact *)contact
96 return (menuItems ? [self menuItemWithRepresentedObject:contact] : nil);
99 - (void)contactOrderChanged:(NSNotification *)notification
101 AIListObject *changedObject = [notification object];
102 if (changedObject && changedObject == containingObject) {
107 //Delegate -------------------------------------------------------------------------------------------------------------
108 #pragma mark Delegate
110 * @brief Set our contact menu delegate
112 - (void)setDelegate:(id<AIContactMenuDelegate> )inDelegate
114 delegate = inDelegate;
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:)];
122 shouldUseUserIcon = ([delegate respondsToSelector:@selector(contactMenuShouldUseUserIcon:)] &&
123 [delegate contactMenuShouldUseUserIcon:self]);
125 shouldUseDisplayName = ([delegate respondsToSelector:@selector(contactMenuShouldUseDisplayName:)] &&
126 [delegate contactMenuShouldUseDisplayName:self]);
128 shouldDisplayGroupHeaders = ([delegate respondsToSelector:@selector(contactMenuShouldDisplayGroupHeaders:)] &&
129 [delegate contactMenuShouldDisplayGroupHeaders:self]);
131 shouldSetTooltip = ([delegate respondsToSelector:@selector(contactMenuShouldSetTooltip:)] &&
132 [delegate contactMenuShouldSetTooltip:self]);
134 - (id<AIContactMenuDelegate> )delegate
140 * @brief Inform our delegate when the menu is rebuilt
146 // Update our values for display name and group header options.
147 shouldUseDisplayName = ([delegate respondsToSelector:@selector(contactMenuShouldUseDisplayName:)] &&
148 [delegate contactMenuShouldUseDisplayName:self]);
150 shouldDisplayGroupHeaders = ([delegate respondsToSelector:@selector(contactMenuShouldDisplayGroupHeaders:)] &&
151 [delegate contactMenuShouldDisplayGroupHeaders:self]);
153 [delegate contactMenu:self didRebuildMenuItems:[self menuItems]];
157 * @brief Inform our delegate of menu selections
159 - (void)selectContactMenuItem:(NSMenuItem *)menuItem
161 if (delegateRespondsToDidSelectContact) {
162 [delegate contactMenu:self didSelectContact:[menuItem representedObject]];
167 //Contact Menu ---------------------------------------------------------------------------------------------------------
168 #pragma mark Contact Menu
170 * @brief Build our contact menu items
172 - (NSArray *)buildMenuItems
174 NSArray *listObjects = nil;
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;
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.
184 if (!shouldDisplayGroupHeaders) {
185 listObjects = [self listObjectsForMenuFromArrayOfListObjects:listObjects];
188 // Sort what we're given
189 //XXX is this container right?
190 listObjects = [listObjects sortedArrayUsingActiveSortControllerInContainer:adium.contactController.contactList];
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])];
198 // Create menus for them
199 return [self contactMenusForListObjects:listObjects];
203 * @brief Creates an array of list objects which should be presented in the menu, expanding any containing objects
205 - (NSArray *)listObjectsForMenuFromArrayOfListObjects:(NSArray *)listObjects
207 NSMutableArray *listObjectArray = [NSMutableArray array];
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.
214 if (!delegateRespondsToShouldIncludeContact || [delegate contactMenu:self shouldIncludeContact:(AIListContact *)listObject]) {
215 if (delegateRespondsToValidateContact)
216 listObject = [delegate contactMenu:self validateContact:(AIListContact *)listObject];
218 [listObjectArray addObject:listObject];
221 } else if ([listObject isKindOfClass:[AIListGroup class]]) {
222 [listObjectArray addObjectsFromArray:[self listObjectsForMenuFromArrayOfListObjects:[(AIListGroup *)listObject uniqueContainedObjects]]];
226 return listObjectArray;
230 * @brief Creates an array of NSMenuItems for each AIListObject
232 - (NSArray *)contactMenusForListObjects:(NSArray *)listObjects
234 NSMutableArray *menuItemArray = [NSMutableArray array];
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]];
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:@""
248 representedObject:listObject];
250 // The group isn't clickable.
251 [menuItem setEnabled:NO];
252 [self _updateMenuItem:menuItem];
254 // Add the group and contained objects to the array.
255 [menuItemArray addObject:menuItem];
256 [menuItemArray addObjectsFromArray:[self contactMenusForListObjects:containedListObjects]];
261 // Just add the menu item.
262 NSMenuItem *menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:@""
264 action:@selector(selectContactMenuItem:)
266 representedObject:listObject];
267 [self _updateMenuItem:menuItem];
268 [menuItemArray addObject:menuItem];
274 return menuItemArray;
278 * @brief Update a menu item to reflect its contact's current status
280 - (void)_updateMenuItem:(NSMenuItem *)menuItem
282 AIListObject *listObject = [menuItem representedObject];
285 [[menuItem menu] setMenuChangedMessagesEnabled:NO];
287 if ([listObject isKindOfClass:[AIListContact class]]) {
288 [menuItem setImage:[self imageForListObject:listObject usingUserIcon:shouldUseUserIcon]];
291 NSString *displayName = listObject.displayName;
293 if (!displayName || (!shouldUseDisplayName && listObject.formattedUID)) {
294 displayName = listObject.formattedUID;
298 [menuItem setTitle:displayName];
300 [menuItem setToolTip:(shouldSetTooltip ? [listObject.statusMessage string] : nil)];
302 [[menuItem menu] setMenuChangedMessagesEnabled:YES];
307 * @brief Update menu when a contact's status changes
309 - (NSSet *)updateListObject:(AIListObject *)inObject keys:(NSSet *)inModifiedKeys silent:(BOOL)silent
311 if ([inObject isKindOfClass:[AIListContact class]]) {
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"]) {
320 //Note that this will return nil if we don't ahve a menu item for inObject
321 NSMenuItem *menuItem = [self existingMenuItemForContact:(AIListContact *)inObject];
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];
332 //If it's silent, wait for a pause before performing the actual rebuild
333 [self performSelector:@selector(rebuildMenu) withObject:nil afterDelay:1.0];
339 [self _updateMenuItem:menuItem];
342 [self _updateMenuItem:menuItem];