Merged [13645] and [13646]: Fixed one of the longest-standing Adium bugs: The Color...
[adiumx.git] / Source / AIContactController.m
blob3234ddd92f527b4524bbe4cd7449df6edd72dc7c
1 /* 
2  * Adium is the legal property of its developers, whose names are listed in the copyright file included
3  * with this source distribution.
4  * 
5  * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
6  * General Public License as published by the Free Software Foundation; either version 2 of the License,
7  * or (at your option) any later version.
8  * 
9  * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
10  * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
11  * Public License for more details.
12  * 
13  * You should have received a copy of the GNU General Public License along with this program; if not,
14  * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
15  */
17 // $Id$
19 //#define CONTACTS_INFO_WITH_PROMPT
21 #import "AIAccountController.h"
22 #import "AIContactController.h"
23 #import "AIContactInfoWindowController.h"
24 #import "AIInterfaceController.h"
25 #import "AILoginController.h"
26 #import "AIMenuController.h"
27 #import "AIPreferenceController.h"
28 #import "AIToolbarController.h"
29 #import "AIToolbarController.h"
30 #import "ESContactAlertsController.h"
31 #import <AIUtilities/AIDictionaryAdditions.h>
32 #import <AIUtilities/AIFileManagerAdditions.h>
33 #import <AIUtilities/AIMenuAdditions.h>
34 #import <AIUtilities/AIToolbarUtilities.h>
35 #import <AIUtilities/CBApplicationAdditions.h>
36 #import <AIUtilities/ESImageAdditions.h>
37 #import <Adium/AIAccount.h>
38 #import <Adium/AIChat.h>
39 #import <Adium/AIContentMessage.h>
40 #import <Adium/AIListContact.h>
41 #import <Adium/AIListGroup.h>
42 #import <Adium/AIListObject.h>
43 #import <Adium/AIMetaContact.h>
44 #import <Adium/AIService.h>
45 #import <Adium/AISortController.h>
46 #import <Adium/AIUserIcons.h>
48 #ifdef CONTACTS_INFO_WITH_PROMPT
49 #import "ESShowContactInfoPromptController.h"
50 #endif
52 #define PREF_GROUP_CONTACT_LIST                 @"Contact List"                 //Contact list preference group
53 #define KEY_FLAT_GROUPS                                 @"FlatGroups"                   //Group storage
54 #define KEY_FLAT_CONTACTS                               @"FlatContacts"                 //Contact storage
55 #define KEY_FLAT_METACONTACTS                   @"FlatMetaContacts"             //Metacontact objectID storage
57 #define OBJECT_STATUS_CACHE                             @"Object Status Cache"
59 #define VIEW_CONTACTS_INFO                              AILocalizedString(@"Get Info",nil)
60 #define VIEW_CONTACTS_INFO_WITH_PROMPT  AILocalizedString(@"Get Info...",nil)
61 #define GET_INFO_MASK                                   (NSCommandKeyMask | NSShiftKeyMask)
62 #define ALTERNATE_GET_INFO_MASK                 (NSCommandKeyMask | NSShiftKeyMask | NSControlKeyMask)
64 #define TITLE_SHOW_INFO                                 AILocalizedString(@"Show Info",nil)
66 #define UPDATE_CLUMP_INTERVAL                   1.0
68 #define TOP_METACONTACT_ID                              @"TopMetaContactID"
69 #define KEY_IS_METACONTACT                              @"isMetaContact"
70 #define KEY_OBJECTID                                    @"objectID"
71 #define KEY_METACONTACT_OWNERSHIP               @"MetaContact Ownership"
72 #define CONTACT_DEFAULT_PREFS                   @"ContactPrefs"
74 #define SHOW_GROUPS_MENU_TITLE                  AILocalizedString(@"Show Groups",nil)
75 #define SHOW_GROUPS_IDENTIFER                   @"ShowGroups"
77 #define KEY_HIDE_CONTACT_LIST_GROUPS    @"Hide Contact List Groups"
78 #define PREF_GROUP_CONTACT_LIST_DISPLAY @"Contact List Display"
80 #define SERVICE_ID_KEY                                  @"ServiceID"
81 #define UID_KEY                                                 @"UID"
83 @interface AIContactController (PRIVATE)
84 - (AIListGroup *)processGetGroupNamed:(NSString *)serverGroup;
85 - (void)_performDelayedUpdates:(NSTimer *)timer;
86 - (void)loadContactList;
87 - (void)saveContactList;
88 - (NSSet *)_informObserversOfObjectStatusChange:(AIListObject *)inObject withKeys:(NSSet *)modifiedKeys silent:(BOOL)silent;
89 - (void)_updateAllAttributesOfObject:(AIListObject *)inObject;
90 - (void)prepareContactInfo;
92 - (NSMenu *)menuOfAllContactsInContainingObject:(AIListObject<AIContainingObject> *)inGroup withTarget:(id)target firstLevel:(BOOL)firstLevel;
93 - (void)_menuOfAllGroups:(NSMenu *)menu forGroup:(AIListGroup *)group withTarget:(id)target level:(int)level;
95 - (id)_performSelectorOnFirstAvailableResponder:(SEL)selector;
96 - (id)_performSelectorOnFirstAvailableResponder:(SEL)selector conformingToProtocol:(Protocol *)protocol;
98 - (NSArray *)_arrayRepresentationOfListObjects:(NSArray *)listObjects;
99 - (void)_loadGroupsFromArray:(NSArray *)array;
101 - (void)_listChangedGroup:(AIListObject *)group object:(AIListObject *)object;
102 - (void)prepareShowHideGroups;
103 - (void)_performChangeOfUseContactListGroups;
105 - (void)_positionObject:(AIListObject *)listObject atIndex:(int)index inGroup:(AIListObject<AIContainingObject> *)group;
106 - (void)_moveObjectServerside:(AIListObject *)listObject toGroup:(AIListGroup *)group;
107 - (void)_renameGroup:(AIListGroup *)listGroup to:(NSString *)newName;
109 //MetaContacts
110 - (AIMetaContact *)metaContactWithObjectID:(NSNumber *)inObjectID;
111 - (BOOL)_restoreContactsToMetaContact:(AIMetaContact *)metaContact updatingPreferences:(BOOL)updatePreferences;
112 - (void)_restoreContactsToMetaContact:(AIMetaContact *)metaContact fromContainedContactsArray:(NSArray *)containedContactsArray updatingPreferences:(BOOL)updatePreferences;
113 - (void)addListObject:(AIListObject *)listObject toMetaContact:(AIMetaContact *)metaContact;
114 - (BOOL)_performAddListObject:(AIListObject *)listObject toMetaContact:(AIMetaContact *)metaContact;
115 - (void)removeListObject:(AIListObject *)listObject fromMetaContact:(AIMetaContact *)metaContact;
116 - (void)_loadMetaContactsFromArray:(NSArray *)array;
117 - (void)_saveMetaContacts:(NSDictionary *)allMetaContactsDict;
118 - (void)breakdownAndRemoveMetaContact:(AIMetaContact *)metaContact;
119 - (void)_storeListObject:(AIListObject *)listObject inMetaContact:(AIMetaContact *)metaContact;
121 - (NSArray *)allContactsWithServiceID:(NSString *)inServiceID UID:(NSString *)inUID;
123 - (void)_addMenuItemsFromArray:(NSArray *)contactArray toMenu:(NSMenu *)contactMenu target:(id)target offlineContacts:(BOOL)offlineContacts;
124 @end
126 @implementation AIContactController
128 //init
129 - (void)initController
131         //Default account preferences
132         [[adium preferenceController] registerDefaults:[NSDictionary dictionaryNamed:CONTACT_DEFAULT_PREFS
133                                                                                                                                                 forClass:[self class]]
134                                                                                   forGroup:PREF_GROUP_CONTACT_LIST];
135     //
136         nextOrderIndex = 1;
137     contactObservers = [[NSMutableSet alloc] init];
138     sortControllerArray = [[NSMutableArray alloc] init];
139     activeSortController = nil;
140     delayedStatusChanges = 0;
141         delayedModifiedStatusKeys = [[NSMutableSet alloc] init];
142         delayedAttributeChanges = 0;
143         delayedModifiedAttributeKeys = [[NSMutableSet alloc] init];
144     delayedContactChanges = 0;
145         delayedUpdateRequests = 0;
146         updatesAreDelayed = NO;
148         //
149         contactDict = [[NSMutableDictionary alloc] init];
150         groupDict = [[NSMutableDictionary alloc] init];
151         metaContactDict = [[NSMutableDictionary alloc] init];
152         contactToMetaContactLookupDict = [[NSMutableDictionary alloc] init];
154         contactList = [[AIListGroup alloc] initWithUID:ADIUM_ROOT_GROUP_NAME];
156         //Get Info window and menu items
157         [self prepareContactInfo];
159         //Show Groups menu item
160         [self prepareShowHideGroups];
162         //Observe content (for preferredContactForContentType:forListContact:)
163     [[adium notificationCenter] addObserver:self
164                                    selector:@selector(didSendContent:)
165                                        name:CONTENT_MESSAGE_SENT
166                                      object:nil];
169 //finish initing
170 - (void)finishIniting
172         [self loadContactList];
173         [self sortContactList];
176 //close
177 - (void)closeController
179         [self saveContactList];
182 //dealloc
183 - (void)dealloc
185     [contactList release];
186     [contactObservers release]; contactObservers = nil;
188     [super dealloc];
191 - (void)clearAllMetaContactData
193         NSString                *path;
194         NSDictionary    *metaContactDictCopy = [metaContactDict copy];
195         NSEnumerator    *enumerator;
196         AIMetaContact   *metaContact;
198         if ([metaContactDictCopy count]) {
199                 [self delayListObjectNotifications];
201                 //Remove all the metaContacts to get any existing objects out of them
202                 enumerator = [metaContactDictCopy objectEnumerator];
203                 while (metaContact = [enumerator nextObject]) {
204                         [self breakdownAndRemoveMetaContact:metaContact];
205                 }
207                 [self endListObjectNotificationsDelay];
208         }
210         [metaContactDict release]; metaContactDict = [[NSMutableDictionary alloc] init];
211         [contactToMetaContactLookupDict release]; contactToMetaContactLookupDict = [[NSMutableDictionary alloc] init];
213         //Clear the preferences for good measure
214         [[adium preferenceController] setPreference:nil
215                                                                                  forKey:KEY_FLAT_METACONTACTS
216                                                                                   group:PREF_GROUP_CONTACT_LIST];
217         [[adium preferenceController] setPreference:nil
218                                                                                  forKey:KEY_METACONTACT_OWNERSHIP
219                                                                                   group:PREF_GROUP_CONTACT_LIST];
221         //Clear out old metacontact files
222         path = [[[adium loginController] userDirectory] stringByAppendingPathComponent:OBJECT_PREFS_PATH];
223         [[NSFileManager defaultManager] removeFilesInDirectory:path
224                                                                                                 withPrefix:@"MetaContact"
225                                                                                          movingToTrash:NO];
226         [[NSFileManager defaultManager] removeFilesInDirectory:[adium cachesPath]
227                                                                                                 withPrefix:@"MetaContact"
228                                                                                          movingToTrash:NO];
230         [metaContactDictCopy release];
233 //Local Contact List Storage -------------------------------------------------------------------------------------------
234 #pragma mark Local Contact List Storage
235 //Load the contact list
236 - (void)loadContactList
238         //We must load all the groups before loading contacts for the ordering system to work correctly.
239         [self _loadGroupsFromArray:[[adium preferenceController] preferenceForKey:KEY_FLAT_GROUPS
240                                                                                                                                                 group:PREF_GROUP_CONTACT_LIST]];
241         [self _loadMetaContactsFromArray:[[adium preferenceController] preferenceForKey:KEY_FLAT_METACONTACTS
242                                                                                                                                                           group:PREF_GROUP_CONTACT_LIST]];
245 //Save the contact list
246 - (void)saveContactList
248         [[adium preferenceController] setPreference:[self _arrayRepresentationOfListObjects:[groupDict allValues]]
249                                                                                  forKey:KEY_FLAT_GROUPS
250                                                                                   group:PREF_GROUP_CONTACT_LIST];
253 //List objects from flattened array
254 - (void)_loadGroupsFromArray:(NSArray *)array
256         NSEnumerator    *enumerator = [array objectEnumerator];
257         NSDictionary    *infoDict;
259         NSString        *Expanded = @"Expanded";
261         while(infoDict = [enumerator nextObject]) {
262                 AIListObject    *object = nil;
264                 object = [self groupWithUID:[infoDict objectForKey:UID_KEY]];
265                 [(AIListGroup *)object setExpanded:[[infoDict objectForKey:Expanded] boolValue]];
266         }
269 - (void)_loadMetaContactsFromArray:(NSArray *)array
271         NSEnumerator    *enumerator = [array objectEnumerator];
272         NSString                *identifier;
274         while (identifier = [enumerator nextObject]) {
275                 NSNumber *objectID = [NSNumber numberWithInt:[[[identifier componentsSeparatedByString:@"-"] objectAtIndex:1] intValue]];
276                 [self metaContactWithObjectID:objectID];
277         }
280 //Flattened array of the contact list content
281 - (NSArray *)_arrayRepresentationOfListObjects:(NSArray *)listObjects
283         NSMutableArray  *array = [NSMutableArray array];
284         NSEnumerator    *enumerator = [listObjects objectEnumerator];;
285         AIListObject    *object;
287         //Create temporary strings outside the loop
288         NSString        *Group = @"Group";
289         NSString        *Type = @"Type";
290         NSString        *Expanded = @"Expanded";
292         while(object = [enumerator nextObject]) {
293                         [array addObject:[NSDictionary dictionaryWithObjectsAndKeys:
294                                 Group, Type,
295                                 [object UID], UID_KEY,
296                                 [NSNumber numberWithBool:[(AIListGroup *)object isExpanded]], Expanded,
297                                 nil]];
298         }
300         return(array);
304 //Status and Display updates -------------------------------------------------------------------------------------------
305 #pragma mark Status and Display updates
306 //These delay Contact_ListChanged, ListObject_AttributesChanged, Contact_OrderChanged notificationsDelays,
307 //sorting and redrawing to prevent redundancy when making a large number of changes
308 //Explicit delay.  Call endListObjectNotificationsDelay to end
309 - (void)delayListObjectNotifications
311         delayedUpdateRequests++;
312         updatesAreDelayed = YES;
315 //End an explicit delay
316 - (void)endListObjectNotificationsDelay
318         delayedUpdateRequests--;
319         if(delayedUpdateRequests == 0 && !delayedUpdateTimer) {
320                 [self _performDelayedUpdates:nil];
321         }
324 //Delay all list object notifications until a period of inactivity occurs.  This is useful for accounts that do not
325 //know when they have finished connecting but still want to mute events.
326 - (void)delayListObjectNotificationsUntilInactivity
328     if(!delayedUpdateTimer) {
329                 updatesAreDelayed = YES;
330                 delayedUpdateTimer = [[NSTimer scheduledTimerWithTimeInterval:UPDATE_CLUMP_INTERVAL
331                                                                                                                            target:self
332                                                                                                                          selector:@selector(_performDelayedUpdates:)
333                                                                                                                          userInfo:nil
334                                                                                                                           repeats:YES] retain];
335     } else {
336                 //Reset the timer
337                 [delayedUpdateTimer setFireDate:[NSDate dateWithTimeIntervalSinceNow:UPDATE_CLUMP_INTERVAL]];
338         }
341 //Update the status of a list object.  This will update any information that is otherwise too expensive to update
342 //automatically, such as their profile.
343 - (void)updateListContactStatus:(AIListContact *)inContact
345         //If we're dealing with a meta contact, update the status of the contacts contained within it
346         if([inContact isKindOfClass:[AIMetaContact class]]) {
347                 NSEnumerator    *enumerator = [[(AIMetaContact *)inContact listContacts] objectEnumerator];
348                 AIListContact   *contact;
350                 while(contact = [enumerator nextObject]) {
351                         [self updateListContactStatus:contact];
352                 }
354         } else {
355                 AIAccount *account = [inContact account];
356                 if(![account online]) {
357                         account = [[adium accountController] preferredAccountForSendingContentType:CONTENT_MESSAGE_TYPE
358                                                                                                                                                          toContact:inContact];
359                 }
361                 [account updateContactStatus:inContact];
362         }
365 //Called after modifying a contact's status
366 // Silent: Silences all events, notifications, sounds, overlays, etc. that would have been associated with this status change
367 - (void)listObjectStatusChanged:(AIListObject *)inObject modifiedStatusKeys:(NSSet *)inModifiedKeys silent:(BOOL)silent
369     NSSet                       *modifiedAttributeKeys;
371     //Let all observers know the contact's status has changed before performing any sorting or further notifications
372         modifiedAttributeKeys = [self _informObserversOfObjectStatusChange:inObject withKeys:inModifiedKeys silent:silent];
374     //Resort the contact list
375         if(updatesAreDelayed) {
376                 delayedStatusChanges++;
377                 [delayedModifiedStatusKeys unionSet:inModifiedKeys];
378         } else {
379                 //We can safely skip sorting if we know the modified attributes will invoke a resort later
380                 if(![[self activeSortController] shouldSortForModifiedAttributeKeys:modifiedAttributeKeys] &&
381                    [[self activeSortController] shouldSortForModifiedStatusKeys:inModifiedKeys]) {
382                         [self sortListObject:inObject];
383                 }
384         }
386     //Post an attributes changed message (if necessary)
387     if([modifiedAttributeKeys count]) {
388                 [self listObjectAttributesChanged:inObject modifiedKeys:modifiedAttributeKeys];
389     }
392 //Call after modifying an object's display attributes
393 //(When modifying display attributes in response to a status change, this is not necessary)
394 - (void)listObjectAttributesChanged:(AIListObject *)inObject modifiedKeys:(NSSet *)inModifiedKeys
396         if(updatesAreDelayed) {
397                 delayedAttributeChanges++;
398                 [delayedModifiedAttributeKeys unionSet:inModifiedKeys];
399         } else {
400         //Resort the contact list if necessary
401         if([[self activeSortController] shouldSortForModifiedAttributeKeys:inModifiedKeys]) {
402                         [self sortListObject:inObject];
403         }
405         //Post an attributes changed message
406                 [[adium notificationCenter] postNotificationName:ListObject_AttributesChanged
407                                                                                                   object:inObject
408                                                                                                 userInfo:(inModifiedKeys ?
409                                                                                                                   [NSDictionary dictionaryWithObject:inModifiedKeys
410                                                                                                                                                                           forKey:@"Keys"] :
411                                                                                                                   nil)];
412         }
415 //Performs any delayed list object/handle updates
416 - (void)_performDelayedUpdates:(NSTimer *)timer
418         BOOL    updatesOccured = (delayedStatusChanges || delayedAttributeChanges || delayedContactChanges);
420         //Send out global attribute & status changed notifications (to cover any delayed updates)
421         if(updatesOccured) {
422                 BOOL shouldSort = NO;
424                 //Inform observers of any changes
425                 if(delayedContactChanges) {
426 //                      [[adium notificationCenter] postNotificationName:Contact_ListChanged object:nil];
427                         delayedContactChanges = 0;
428                         shouldSort = YES;
429                 }
430                 if (delayedStatusChanges) {
431                         if(!shouldSort &&
432                            [[self activeSortController] shouldSortForModifiedStatusKeys:delayedModifiedStatusKeys]) {
433                                 shouldSort = YES;
434                         }
435                         [delayedModifiedStatusKeys removeAllObjects];
436                         delayedStatusChanges = 0;
437                 }
438                 if(delayedAttributeChanges) {
439                         if(!shouldSort &&
440                            [[self activeSortController] shouldSortForModifiedAttributeKeys:delayedModifiedAttributeKeys]) {
441                                 shouldSort = YES;
442                         }
443                         [[adium notificationCenter] postNotificationName:ListObject_AttributesChanged
444                                                                                                           object:nil
445                                                                                                         userInfo:(delayedModifiedAttributeKeys ?
446                                                                                                                           [NSDictionary dictionaryWithObject:delayedModifiedAttributeKeys
447                                                                                                                                                                                   forKey:@"Keys"] :
448                                                                                                                           nil)];
449                         [delayedModifiedAttributeKeys removeAllObjects];
450                         delayedAttributeChanges = 0;
451                 }
453                 //Sort only if necessary
454                 if (shouldSort) {
455                         [self sortContactList];
456                 }
457         }
459     //If no more updates are left to process, disable the update timer
460         //If there are no delayed update requests, remove the hold
461         if(!delayedUpdateTimer || !updatesOccured) {
462                 if(delayedUpdateTimer) {
463                         [delayedUpdateTimer invalidate];
464                         [delayedUpdateTimer release];
465                         delayedUpdateTimer = nil;
466                 }
467                 if(delayedUpdateRequests == 0) {
468                         updatesAreDelayed = NO;
469                 }
470     }
473 #pragma mark Contact Grouping
474 //Contact Grouping -----------------------------------------------------------------------------------------------------
476 //Redetermine the local grouping of a contact in response to server grouping information or an external change
477 - (void)listObjectRemoteGroupingChanged:(AIListContact *)inContact
479         NSString                        *remoteGroupName = [inContact remoteGroupName];
480         AIListObject            *containingObject;
482         [inContact retain];
484         containingObject = [inContact containingObject];
486         if ([containingObject isKindOfClass:[AIMetaContact class]]) {
488                 //Make sure we traverse metaContacts as close to the root as possible; an automatic metaContact may be within
489                 //a manually created one, for example, in the most common situation.
490                 containingObject = [self parentContactForListObject:containingObject];
492                 //If the object's 'group' is a metaContact, and that metaContact isn't in our list yet
493                 //use the object's own remote grouping (or the contactList root) as our grouping.
494                 if (![containingObject containingObject] && [remoteGroupName length]) {
496                         //If no similar objects exist, we add this contact directly to the list
497                         AIListGroup *targetGroup;
499                         targetGroup = (useContactListGroups ? [self groupWithUID:remoteGroupName] : contactList);
501                         [targetGroup addObject:containingObject];
502                         [self _listChangedGroup:targetGroup object:containingObject];
503                 }
505         } else {
506                 //If we have a remoteGroupName, add the contact locally to the list
507                 if(remoteGroupName) {
508                         AIListGroup *localGroup;
510                         localGroup = (useContactListGroups ? [self groupWithUID:remoteGroupName] : contactList);
511                         [self _moveContactLocally:inContact
512                                                           toGroup:localGroup];
514                 } else {
515                         //If !remoteGroupName, remove the contact from any local groups
516                         if(containingObject) {
517                                 //Remove the object
518                                 [(AIListGroup *)containingObject removeObject:inContact];
520                                 [self _listChangedGroup:(AIListGroup *)containingObject object:inContact];
521                         }
522                 }
523         }
525         BOOL    isCurrentlyAStranger = [inContact isStranger];
526         if((isCurrentlyAStranger && (remoteGroupName != nil)) ||
527            (!isCurrentlyAStranger && (remoteGroupName == nil))){
528                 [inContact setStatusObject:(remoteGroupName ? [NSNumber numberWithBool:YES] : nil)
529                                                         forKey:@"NotAStranger"
530                                                         notify:NotifyLater];
531                 [inContact notifyOfChangedStatusSilently:YES];
532         }
534         [inContact release];
537 - (void)_moveContactLocally:(AIListContact *)listContact toGroup:(AIListGroup *)localGroup
539         AIListObject    *containingObject;
540         AIListObject    *existingObject;
541         BOOL                    performedGrouping = NO;
543         //Protect with a retain while we are removing and adding the contact to our arrays
544         [listContact retain];
546         //Remove this object from any local groups we have it in currently
547         if((containingObject = [listContact containingObject]) &&
548            ([containingObject isKindOfClass:[AIListGroup class]])) {
549                 //Remove the object
550                 [(AIListGroup *)containingObject removeObject:listContact];
551                 [self _listChangedGroup:(AIListGroup *)containingObject object:listContact];
552         }
554         if(existingObject = [localGroup objectWithService:[listContact service] UID:[listContact UID]]) {
555                 //If an object exists in this group with the same UID and serviceID, create a MetaContact
556                 //for the two.
557                 [self groupListContacts:[NSArray arrayWithObjects:listContact,existingObject,nil]];
558                 performedGrouping = YES;
560         } else {
561                 AIMetaContact   *metaContact;
563                 //If no object exists in this group which matches, we should check if there is already
564                 //a MetaContact holding a matching ListContact, since we should include this contact in it
565                 //If we found a metaContact to which we should add, do it.
566                 if (metaContact = [contactToMetaContactLookupDict objectForKey:[listContact internalObjectID]]) {
567                         [self addListObject:listContact toMetaContact:metaContact];
568                         performedGrouping = YES;
569                 }
570         }
572         if (!performedGrouping) {
573                 //If no similar objects exist, we add this contact directly to the list
574                 [localGroup addObject:listContact];
576                 //Add
577                 [self _listChangedGroup:localGroup object:listContact];
578         }
580         //Cleanup
581         [listContact release];
584 - (AIListGroup *)remoteGroupForContact:(AIListContact *)inContact
586         AIListGroup             *group;
588         if ([inContact isKindOfClass:[AIMetaContact class]]) {
589                 //For a metaContact, the closest we have to a remote group is the group it is within locally
590                 group = [inContact parentGroup];
592         } else {
593                 NSString        *remoteGroup = [inContact remoteGroupName];
594                 group = (remoteGroup ? [self groupWithUID:remoteGroup] : nil);
595         }
597         return(group);
600 //Post a list grouping changed notification for the object and group
601 - (void)_listChangedGroup:(AIListObject *)group object:(AIListObject *)object
603         if(updatesAreDelayed) {
604                 delayedContactChanges++;
605         } else {
606                 [[adium notificationCenter] postNotificationName:Contact_ListChanged
607                                                                                                   object:object
608                                                                                                 userInfo:(group ? [NSDictionary dictionaryWithObject:group forKey:@"ContainingGroup"] : nil)];
609         }
612 - (BOOL)useContactListGroups
614         return(useContactListGroups);
617 - (void)setUseContactListGroups:(BOOL)inFlag
619         if (inFlag != useContactListGroups) {
621                 useContactListGroups = inFlag;
623                 [self _performChangeOfUseContactListGroups];
624         }
627 - (void)_performChangeOfUseContactListGroups
629         NSEnumerator    *enumerator;
630         AIListObject    *listObject;
632         [self delayListObjectNotifications];
634         //Store the preference
635         [[adium preferenceController] setPreference:[NSNumber numberWithBool:!useContactListGroups]
636                                                                                  forKey:KEY_HIDE_CONTACT_LIST_GROUPS
637                                                                                   group:PREF_GROUP_CONTACT_LIST_DISPLAY];
639         //Configure the sort controller to force ignoring of groups as appropriate
640         [[self activeSortController] forceIgnoringOfGroups:(useContactListGroups ? NO : YES)];
642         enumerator = [[[[contactList containedObjects] copy] autorelease] objectEnumerator];
644         if (useContactListGroups) { /* We are now using contact list groups, but we weren't before. */
646                 //Restore the grouping of all root-level contacts
647                 while(listObject = [enumerator nextObject]) {
648                         if([listObject isKindOfClass:[AIListContact class]]) {
649                                 [(AIListContact *)listObject restoreGrouping];
650                         }
651                 }
653         } else { /* We are no longer using contact list groups, but we were before. */
655                 while(listObject = [enumerator nextObject]) {
656                         if([listObject isKindOfClass:[AIListGroup class]]) {
657                                 NSArray                 *containedObjects;
658                                 NSEnumerator    *groupEnumerator;
659                                 AIListObject    *containedListObject;
661                                 containedObjects = [[(AIListGroup *)listObject containedObjects] copy];
662                                 groupEnumerator = [containedObjects objectEnumerator];
663                                 while(containedListObject = [groupEnumerator nextObject]) {
664                                         if([containedListObject isKindOfClass:[AIListContact class]]) {
665                                                 [self _moveContactLocally:(AIListContact *)containedListObject
666                                                                                   toGroup:contactList];
667                                         }
668                                 }
669                                 [containedObjects release];
670                         }
671                 }
672         }
674         //Stop delaying object notifications; this will automatically resort the contact list, so we're done.
675         [self endListObjectNotificationsDelay];
678 - (void)prepareShowHideGroups
680         //Load the preference
681         useContactListGroups = ![[[adium preferenceController] preferenceForKey:KEY_HIDE_CONTACT_LIST_GROUPS
682                                                                                                                                           group:PREF_GROUP_CONTACT_LIST_DISPLAY] boolValue];
684         //Show offline contacts menu item
685     showGroupsMenuItem = [[NSMenuItem alloc] initWithTitle:SHOW_GROUPS_MENU_TITLE
686                                                                                                         target:self
687                                                                                                         action:@selector(toggleShowGroups:)
688                                                                                          keyEquivalent:@""];
689         [showGroupsMenuItem setState:useContactListGroups];
690         [[adium menuController] addMenuItem:showGroupsMenuItem toLocation:LOC_View_Toggles];
692         //Toolbar
693         NSToolbarItem   *toolbarItem;
694     toolbarItem = [AIToolbarUtilities toolbarItemWithIdentifier:SHOW_GROUPS_IDENTIFER
695                                                                                                                   label:AILocalizedString(@"Show Groups",nil)
696                                                                                                    paletteLabel:AILocalizedString(@"Toggle Groups Display",nil)
697                                                                                                                 toolTip:AILocalizedString(@"Toggle display of groups",nil)
698                                                                                                                  target:self
699                                                                                                 settingSelector:@selector(setImage:)
700                                                                                                         itemContent:[NSImage imageNamed:(useContactListGroups ?
701                                                                                                                                                                          @"togglegroups_transparent" :
702                                                                                                                                                                          @"togglegroups")
703                                                                                                                                                    forClass:[self class]]
704                                                                                                                  action:@selector(toggleShowGroupsToolbar:)
705                                                                                                                    menu:nil];
706     [[adium toolbarController] registerToolbarItem:toolbarItem forToolbarType:@"ContactList"];
709 - (IBAction)toggleShowGroups:(id)sender
711         //Flip-flop.
712         useContactListGroups = !useContactListGroups;
713         [showGroupsMenuItem setState:useContactListGroups];
715         //Update the contact list.  Do it on the next run loop for better menu responsiveness, as it may be a lengthy procedure.
716         [self performSelector:@selector(_performChangeOfUseContactListGroups)
717                            withObject:nil
718                            afterDelay:0.000001];
721 - (IBAction)toggleShowGroupsToolbar:(id)sender
723         [self toggleShowGroups:sender];
725         [sender setImage:[NSImage imageNamed:(useContactListGroups ?
726                                                                                   @"togglegroups_transparent" :
727                                                                                   @"togglegroups")
728                                                                 forClass:[self class]]];
731 #pragma mark Meta Contacts
732 //Meta Contacts --------------------------------------------------------------------------------------------------------
733 //Returns a metaContact with the specified object ID.  Pass nil to create a new, unique metaContact
734 - (AIMetaContact *)metaContactWithObjectID:(NSNumber *)inObjectID
736         NSString                *metaContactDictKey;
737         AIMetaContact   *metaContact;
738         BOOL                    shouldRestoreContacts = YES;
740         //If no object ID is provided, use the next available object ID
741         //(MetaContacts should always have an individually unique object id)
742         if(!inObjectID) {
743                 int topID = [[[adium preferenceController] preferenceForKey:TOP_METACONTACT_ID
744                                                                                                                           group:PREF_GROUP_CONTACT_LIST] intValue];
745                 inObjectID = [NSNumber numberWithInt:topID];
746                 [[adium preferenceController] setPreference:[NSNumber numberWithInt:([inObjectID intValue] + 1)]
747                                                                                          forKey:TOP_METACONTACT_ID
748                                                                                           group:PREF_GROUP_CONTACT_LIST];
750                 //No reason to waste time restoring contacts when none are in the meta contact yet.
751                 shouldRestoreContacts = NO;
752         }
754         //Look for a metacontact with this object ID.  If none is found, create one
755         //and add its contained contacts to it.
756         metaContactDictKey = [AIMetaContact internalObjectIDFromObjectID:inObjectID];
758         metaContact = [metaContactDict objectForKey:metaContactDictKey];
759         if (!metaContact) {
760                 metaContact = [[AIMetaContact alloc] initWithObjectID:inObjectID];
762                 //Keep track of it in our metaContactDict for retrieval by objectID
763                 [metaContactDict setObject:metaContact forKey:metaContactDictKey];
765                 //Add it to our more general contactDict, as well
766                 [contactDict setObject:metaContact forKey:[metaContact internalUniqueObjectID]];
768                 if (shouldRestoreContacts) {
769                         if(![self _restoreContactsToMetaContact:metaContact updatingPreferences:NO]) {
771                                 //If restoring the metacontact did not actually add any contacts, delete it as it is invalid
772                                 [self breakdownAndRemoveMetaContact:metaContact];
773                                 metaContact = nil;
774                         }
775                 }
776                 
777                 /* As with contactWithService:account:UID, update all attributes so observers are initially informed of
778                  * this object's existence.
779                  */
780                 [self _updateAllAttributesOfObject:metaContact];
781                 
782                 [metaContact release];
783         }
785         return (metaContact);
788 - (BOOL)_restoreContactsToMetaContact:(AIMetaContact *)metaContact updatingPreferences:(BOOL)updatePreferences
790         NSDictionary    *allMetaContactsDict = [[adium preferenceController] preferenceForKey:KEY_METACONTACT_OWNERSHIP
791                                                                                                                                                                         group:PREF_GROUP_CONTACT_LIST];
792         NSArray                 *containedContactsArray = [allMetaContactsDict objectForKey:[metaContact internalObjectID]];
793         BOOL                    restoredContacts;
795         if([containedContactsArray count]) {
796                 [metaContact setDelayContainedObjectSorting:YES];
798                 [self _restoreContactsToMetaContact:metaContact
799                                                                    fromContainedContactsArray:containedContactsArray
800                                                                                   updatingPreferences:updatePreferences];
802                 [metaContact setDelayContainedObjectSorting:NO];
804                 restoredContacts = YES;
806         } else {
807                 restoredContacts = NO;
808         }
810         return restoredContacts;
813 - (void)_restoreContactsToMetaContact:(AIMetaContact *)metaContact fromContainedContactsArray:(NSArray *)containedContactsArray updatingPreferences:(BOOL)updatePreferences
815         NSEnumerator            *enumerator = [containedContactsArray objectEnumerator];
816         NSMutableDictionary     *allMetaContactsDict = nil;
817         NSDictionary            *containedContact;
818         BOOL                            shouldSaveMetaContacts = NO;
820         while(containedContact = [enumerator nextObject]) {
821                 if ([[containedContact objectForKey:KEY_IS_METACONTACT] boolValue]) {
822                         /* As of Adium 0.80, these keys should never be created as we no longer create metaContacts within metaContacts.
823                            This check exists to import old preferences. */
824                         NSArray                 *newContainedContactsArray;
825                         NSString                *objectID;
827                         if(!allMetaContactsDict) {
828                                 allMetaContactsDict = [[[adium preferenceController] preferenceForKey:KEY_METACONTACT_OWNERSHIP
829                                                                                                                                                                 group:PREF_GROUP_CONTACT_LIST] mutableCopy];
830                         }
832                         //Retrieve the containedContactsArray for the metaContact with the specified objectID
833                         objectID = [containedContact objectForKey:KEY_OBJECTID];
834                         newContainedContactsArray = [allMetaContactsDict objectForKey:objectID];
836                         //Recursively call ourself, saving the preferences, to make this merge unnecessary next time
837                         [self _restoreContactsToMetaContact:metaContact
838                                          fromContainedContactsArray:newContainedContactsArray
839                                                         updatingPreferences:YES];
841                         //We should never need this metaContact again.
842                         [allMetaContactsDict removeObjectForKey:objectID];
844                         shouldSaveMetaContacts = YES;
846                 } else {
848                         //Assign this metaContact to the appropriate internalObjectID for containedContact's represented listObject.
849                         //As listObjects are loaded/created/requested which match this internalObjectID, they will be inserted into the metaContact.
850                         [contactToMetaContactLookupDict setObject:metaContact
851                                                                                            forKey:[AIListObject internalObjectIDForServiceID:[containedContact objectForKey:SERVICE_ID_KEY]
852                                                                                                                                                                                          UID:[containedContact objectForKey:UID_KEY]]];
853                 }
854         }
856         if(shouldSaveMetaContacts) {
857                 AILog(@"MetaContacts: Should save for %@",containedContactsArray);
858                 [self _saveMetaContacts:allMetaContactsDict];
859         }
860         
861         //Clean up
862         [allMetaContactsDict release];
866 //Add a list object to a meta contact, setting preferences and such
867 //so the association is lasting across program launches.
868 - (void)addListObject:(AIListObject *)listObject toMetaContact:(AIMetaContact *)metaContact
870         if (listObject != metaContact) {
872                 //If listObject is a metaContact, perform addListObject:toMetaContact: recursively
873                 if([listObject isKindOfClass:[AIMetaContact class]]) {
874                         NSEnumerator    *enumerator = [[[[(AIMetaContact *)listObject containedObjects] copy] autorelease] objectEnumerator];
875                         AIListObject    *someObject;
877                         while(someObject = [enumerator nextObject]) {
879                                 [self addListObject:someObject toMetaContact:metaContact];
880                         }
882                 } else {
883                         AIMetaContact           *oldMetaContact;
885                         //Obtain any metaContact this listObject is currently within, so we can remove it later
886                         oldMetaContact = [contactToMetaContactLookupDict objectForKey:[listObject internalObjectID]];
888                         if ([self _performAddListObject:listObject toMetaContact:metaContact]) {
889                                 //If this listObject was not in this metaContact in any form before, store the change
890                                 if (metaContact != oldMetaContact) {
891                                         //Remove the list object from any other metaContact it is in at present
892                                         if (oldMetaContact) {
893                                                 [self removeListObject:listObject fromMetaContact:oldMetaContact];
894                                         }
896                                         [self _storeListObject:listObject inMetaContact:metaContact];
897                                 }
898                         }
899                 }
900         }
903 - (void)_storeListObject:(AIListObject *)listObject inMetaContact:(AIMetaContact *)metaContact
905         AILog(@"MetaContacts: Storing %@ in %@",listObject, metaContact);
906         NSDictionary            *containedContactDict;
907         NSMutableDictionary     *allMetaContactsDict;
908         NSMutableArray          *containedContactsArray;
910         NSString                        *metaContactInternalObjectID = [metaContact internalObjectID];
912         //Get the dictionary of all metaContacts
913         allMetaContactsDict = [[[adium preferenceController] preferenceForKey:KEY_METACONTACT_OWNERSHIP
914                                                                                                                                         group:PREF_GROUP_CONTACT_LIST] mutableCopy];
915         if (!allMetaContactsDict) {
916                 allMetaContactsDict = [[NSMutableDictionary alloc] init];
917         }
919         //Load the array for the new metaContact
920         containedContactsArray = [[allMetaContactsDict objectForKey:metaContactInternalObjectID] mutableCopy];
921         if (!containedContactsArray) containedContactsArray = [[NSMutableArray alloc] init];
922         containedContactDict = nil;
924         //Create the dictionary describing this list object
925         if ([listObject isKindOfClass:[AIMetaContact class]]) {
926                 containedContactDict = [NSDictionary dictionaryWithObjectsAndKeys:
927                         [NSNumber numberWithBool:YES],KEY_IS_METACONTACT,
928                         [(AIMetaContact *)listObject objectID],KEY_OBJECTID,nil];
930         } else if ([listObject isKindOfClass:[AIListContact class]]) {
931                 containedContactDict = [NSDictionary dictionaryWithObjectsAndKeys:
932                         [[listObject service] serviceID],SERVICE_ID_KEY,
933                         [listObject UID],UID_KEY,nil];
934         }
936         //Only add if this dict isn't already in the array
937         if (containedContactDict && ([containedContactsArray indexOfObject:containedContactDict] == NSNotFound)) {
938                 [containedContactsArray addObject:containedContactDict];
939                 [allMetaContactsDict setObject:containedContactsArray forKey:metaContactInternalObjectID];
941                 //Save
942                 AILog(@"MetaContacts: _storeListObject saving: %@",listObject);
943                 [self _saveMetaContacts:allMetaContactsDict];
945                 [[adium contactAlertsController] mergeAndMoveContactAlertsFromListObject:listObject
946                                                                                                                                   intoListObject:metaContact];
947         }
949         [allMetaContactsDict release];
950         [containedContactsArray release];
953 //Actually adds a list object to a meta contact. No preferences are changed.
954 //Attempts to add the list object, causing group reassignment and updates our contactToMetaContactLookupDict
955 //for quick lookup of the MetaContact given a AIListContact uniqueObjectID if successful.
956 - (BOOL)_performAddListObject:(AIListObject *)listObject toMetaContact:(AIMetaContact *)metaContact
958         AIListObject<AIContainingObject>        *localGroup;
959         BOOL                                                            success;
961         localGroup = [listObject containingObject];
963         //Remove the object from its previous containing group
964         if (localGroup && (localGroup != metaContact)) {
965                 [localGroup removeObject:listObject];
966                 [self _listChangedGroup:localGroup object:listObject];
967         }
969         //AIMetaContact will handle reassigning the list object's grouping to being itself
970         if (success = [metaContact addObject:listObject]) {
971                 [contactToMetaContactLookupDict setObject:metaContact forKey:[listObject internalObjectID]];
973                 [self _listChangedGroup:metaContact object:listObject];
975                 //If the metaContact isn't in a group yet, use the group of the object we just added
976                 if ((![metaContact containingObject]) && localGroup) {
977                         //Add the new meta contact to our list
978                         [(AIMetaContact *)localGroup addObject:metaContact];
979                         [self _listChangedGroup:localGroup object:metaContact];
980                 }
981         }
983         return success;
986 - (void)removeAllListObjectsMatching:(AIListObject *)listObject fromMetaContact:(AIMetaContact *)metaContact
988         NSEnumerator    *enumerator;
989         AIListObject    *theObject;
991         enumerator = [[self allContactsWithService:[listObject service]
992                                                                                   UID:[listObject UID]] objectEnumerator];
994         //Remove from the contactToMetaContactLookupDict first so we don't try to reinsert into this metaContact
995         [contactToMetaContactLookupDict removeObjectForKey:[listObject internalObjectID]];
997         [self delayListObjectNotifications];
998         while (theObject = [enumerator nextObject]) {
999                 [self removeListObject:theObject fromMetaContact:metaContact];
1000         }
1001         [self endListObjectNotificationsDelay];
1004 - (void)removeListObject:(AIListObject *)listObject fromMetaContact:(AIMetaContact *)metaContact
1006         NSEnumerator            *enumerator;
1007         NSArray                         *containedContactsArray;
1008         NSDictionary            *containedContactDict = nil;
1009         NSMutableDictionary     *allMetaContactsDict;
1010         NSString                        *metaContactInternalObjectID = [metaContact internalObjectID];
1012         //Get the dictionary of all metaContacts
1013         allMetaContactsDict = [[adium preferenceController] preferenceForKey:KEY_METACONTACT_OWNERSHIP
1014                                                                                                                                    group:PREF_GROUP_CONTACT_LIST];
1016         //Load the array for the metaContact
1017         containedContactsArray = [allMetaContactsDict objectForKey:metaContactInternalObjectID];
1019         //Enumerate it, looking only for the appropriate type of containedContactDict
1020         enumerator = [containedContactsArray objectEnumerator];
1022         if ([listObject isKindOfClass:[AIMetaContact class]]) {
1023                 NSNumber        *listObjectObjectID = [(AIMetaContact *)listObject objectID];
1025                 while (containedContactDict = [enumerator nextObject]) {
1026                         if (([[containedContactDict objectForKey:KEY_IS_METACONTACT] boolValue]) &&
1027                                 (([(NSNumber *)[containedContactDict objectForKey:KEY_OBJECTID] compare:listObjectObjectID]) == 0)) {
1028                                 break;
1029                         }
1030                 }
1032         } else if ([listObject isKindOfClass:[AIListContact class]]) {
1034                 NSString        *listObjectUID = [listObject UID];
1035                 NSString        *listObjectServiceID = [[listObject service] serviceID];
1037                 while (containedContactDict = [enumerator nextObject]) {
1038                         if ([[containedContactDict objectForKey:UID_KEY] isEqualToString:listObjectUID] &&
1039                                 [[containedContactDict objectForKey:SERVICE_ID_KEY] isEqualToString:listObjectServiceID]) {
1040                                 break;
1041                         }
1042                 }
1043         }
1045         //If we found a matching dict (referring to our contact in the old metaContact), remove it and store the result
1046         if (containedContactDict) {
1047                 NSMutableArray          *newContainedContactsArray;
1048                 NSMutableDictionary     *newAllMetaContactsDict;
1050                 newContainedContactsArray = [containedContactsArray mutableCopy];
1051                 [newContainedContactsArray removeObjectIdenticalTo:containedContactDict];
1053                 newAllMetaContactsDict = [allMetaContactsDict mutableCopy];
1054                 [newAllMetaContactsDict setObject:newContainedContactsArray
1055                                                                    forKey:metaContactInternalObjectID];
1057                 [self _saveMetaContacts:newAllMetaContactsDict];
1059                 [newContainedContactsArray release];
1060                 [newAllMetaContactsDict release];
1061         }
1063         //The listObject can be within the metaContact without us finding a containedContactDict if we are removing multiple
1064         //listContacts referring to the same UID & serviceID combination - that is, on multiple accounts on the same service.
1065         //We therefore request removal of the object regardless of the if (containedContactDict) check above.
1066         [metaContact removeObject:listObject];
1071  * @brief Groups UIDs for services into a single metacontact
1073  * UIDsArray and servicesArray should be a paired set of arrays, with each index corresponding to
1074  * a UID and a service, respectively, which together define a contact which should be included in the grouping.
1076  * Assumption: This is only called after the contact list is finished loading, which occurs via
1077  * -(void)finishIniting above.
1079  * @param UIDsArray NSArray of UIDs
1080  * @param servicesArray NSArray of serviceIDs corresponding to entries in UIDsArray
1081  */
1082 - (AIMetaContact *)groupUIDs:(NSArray *)UIDsArray forServices:(NSArray *)servicesArray
1084         NSMutableArray  *contactsToGroupArray = [NSMutableArray array];
1086         int                             count = [UIDsArray count];
1087         int                             i;
1089         //Build an array of all contacts matching this description (multiple accounts on the same service listing
1090         //the same UID mean that we can have multiple AIListContact objects with a UID/service combination)
1091         for (i = 0; i < count; i++) {
1092                 [contactsToGroupArray addObjectsFromArray:[self allContactsWithServiceID:[servicesArray objectAtIndex:i]
1093                                                                                                                                                          UID:[UIDsArray objectAtIndex:i]]];
1094         }
1096         return([self groupListContacts:contactsToGroupArray]);
1099 /* @brief Group an NSArray of AIListContacts, returning the meta contact into which they are added.
1101  * This will reuse an existing metacontact (for one of the contacts in the array) if possible.
1102  * @param contactsToGroupArray Contacts to group together
1103  */
1104 - (AIMetaContact *)groupListContacts:(NSArray *)contactsToGroupArray
1106         NSEnumerator    *enumerator;
1107         AIListContact   *listContact;
1108         AIMetaContact   *metaContact = nil;
1110         //Look for an existing MetaContact we can use.  The first one we find is the lucky winner.
1111         enumerator = [contactsToGroupArray objectEnumerator];
1112         while ((listContact = [enumerator nextObject]) && (metaContact == nil)) {
1113                 if ([listContact isKindOfClass:[AIMetaContact class]]) {
1114                         metaContact = (AIMetaContact *)listContact;
1115                 } else {
1116                         metaContact = [contactToMetaContactLookupDict objectForKey:[listContact internalObjectID]];
1117                 }
1118         }
1120         //Create a new MetaContact is we didn't find one.
1121         if (!metaContact) {
1122                 metaContact = [self metaContactWithObjectID:nil];
1123         }
1124         
1125         /* Add all these contacts to our MetaContact.
1126                 * Some may already be present, but that's fine, as nothing will happen.
1127                 */
1128         enumerator = [contactsToGroupArray objectEnumerator];
1129         while (listContact = [enumerator nextObject]) {
1130                 [self addListObject:listContact toMetaContact:metaContact];
1131         }
1132         
1133         return(metaContact);
1136 - (void)breakdownAndRemoveMetaContact:(AIMetaContact *)metaContact
1138         //Remove the objects within it from being inside it
1139         NSArray                                                         *containedObjects = [[metaContact containedObjects] copy];
1140         NSEnumerator                                            *metaEnumerator = [containedObjects objectEnumerator];
1141         AIListObject<AIContainingObject>        *containingObject = [metaContact containingObject];
1142         AIListObject                                            *object;
1144         NSMutableDictionary *allMetaContactsDict = [[[adium preferenceController] preferenceForKey:KEY_METACONTACT_OWNERSHIP
1145                                                                                                                                                                                  group:PREF_GROUP_CONTACT_LIST] mutableCopy];
1147         while (object = [metaEnumerator nextObject]) {
1149                 //Remove from the contactToMetaContactLookupDict first so we don't try to reinsert into this metaContact
1150                 [contactToMetaContactLookupDict removeObjectForKey:[object internalObjectID]];
1152                 [self removeListObject:object fromMetaContact:metaContact];
1153         }
1155         //Then, procede to remove the metaContact
1157         //Protect!
1158         [metaContact retain];
1160         //Remove it from its containing group
1161         [containingObject removeObject:metaContact];
1163         NSString        *metaContactInternalObjectID = [metaContact internalObjectID];
1165         //Remove our reference to it internally
1166         [metaContactDict removeObjectForKey:metaContactInternalObjectID];
1168         //Remove it from the preferences dictionary
1169         [allMetaContactsDict removeObjectForKey:metaContactInternalObjectID];
1171         //XXX - contactToMetaContactLookupDict
1173         //Post the list changed notification for the old containingObject
1174         [self _listChangedGroup:containingObject object:metaContact];
1176         //Save the updated allMetaContactsDict which no longer lists the metaContact
1177         [self _saveMetaContacts:allMetaContactsDict];
1179         //Protection is overrated.
1180         [metaContact release];
1181         [containedObjects release];
1182         [allMetaContactsDict release];
1185 - (void)_saveMetaContacts:(NSDictionary *)allMetaContactsDict
1187         AILog(@"MetaContacts: Saving!");
1188         [[adium preferenceController] setPreference:allMetaContactsDict
1189                                                                                  forKey:KEY_METACONTACT_OWNERSHIP
1190                                                                                   group:PREF_GROUP_CONTACT_LIST];
1191         [[adium preferenceController] setPreference:[allMetaContactsDict allKeys]
1192                                                                                  forKey:KEY_FLAT_METACONTACTS
1193                                                                                   group:PREF_GROUP_CONTACT_LIST];
1196 //Sort list objects alphabetically by their display name
1197 int contactDisplayNameSort(AIListObject *objectA, AIListObject *objectB, void *context)
1199         return [[objectA displayName] caseInsensitiveCompare:[objectB displayName]];
1202 //Return a menu of contacts which are within inContact on a given service. Pass nil for service to retrive all contacts.
1203 //The menuItems have a target of target and a selector of @selector(selectContainedContact:).
1204 - (NSMenu *)menuOfContainedContacts:(AIListObject *)inContact forService:(AIService *)service withTarget:(id)target includeOffline:(BOOL)includeOffline
1206         NSMenu          *contactMenu = [[NSMenu alloc] initWithTitle:@""];
1207         NSArray         *contactArray;
1209         if([inContact isKindOfClass:[AIMetaContact class]]) {
1210                 if (service) {
1211                         NSDictionary    *dict;
1213                         dict = [(AIMetaContact *)inContact dictionaryOfServiceClassesAndListContacts];
1214                         contactArray = [dict objectForKey:[service serviceClass]];
1215                 } else {
1216                         //If service is nil, get ALL contained contacts
1217                         contactArray = [(AIMetaContact *)inContact listContacts];
1218                 }
1219         } else {
1220                 contactArray = [NSArray arrayWithObject:inContact];
1221         }
1223         //Sort the array by display name
1224         contactArray = [contactArray sortedArrayUsingFunction:contactDisplayNameSort context:nil];
1226         [self _addMenuItemsFromArray:contactArray
1227                                                   toMenu:contactMenu
1228                                                   target:target
1229                                   offlineContacts:NO];
1231         if (includeOffline) {
1232                 //Separate the online from the offline
1233                 if ([contactMenu numberOfItems] > 0) {
1234                         [contactMenu addItem:[NSMenuItem separatorItem]];
1235                 }
1237                 [self _addMenuItemsFromArray:contactArray
1238                                                           toMenu:contactMenu
1239                                                           target:target
1240                                          offlineContacts:YES];
1241         }
1243         return [contactMenu autorelease];
1246 //Add the contacts from contactArray to the specified menu.  If offlineContacts is NO, only add online ones.
1247 //If offlineContacts is YES, only add offline ones.
1248 - (void)_addMenuItemsFromArray:(NSArray *)contactArray toMenu:(NSMenu *)contactMenu target:(id)target offlineContacts:(BOOL)offlineContacts
1250         NSEnumerator    *enumerator;
1251         AIListContact   *contact;
1253         enumerator = [contactArray objectEnumerator];
1255         while(contact = [enumerator nextObject]) {
1256                 BOOL contactIsOnline = [contact online];
1258                 //If the contact is online and we aren't doing only offline contacts, add it.
1259                 //If the contact is offline, and we are doing only offline contacts, add it if any account is available for sending to it.
1260                 if ((contactIsOnline && !offlineContacts) ||
1261                         (!contactIsOnline &&
1262                          offlineContacts &&
1263                          ([[adium accountController] firstAccountAvailableForSendingContentType:CONTENT_MESSAGE_TYPE
1264                                                                                                                                                   toContact:contact
1265                                                                                                                                          includeOffline:NO] != nil))) {
1266                         NSImage                 *menuServiceImage;
1267                         NSMenuItem              *menuItem;
1269                         menuServiceImage = [AIUserIcons menuUserIconForObject:contact];
1271                         menuItem = [[NSMenuItem alloc] initWithTitle:[contact formattedUID]
1272                                                                                                   target:target
1273                                                                                                   action:@selector(selectContainedContact:)
1274                                                                                    keyEquivalent:@""];
1275                         [menuItem setRepresentedObject:contact];
1276                         [menuItem setImage:menuServiceImage];
1277                         [contactMenu addItem:menuItem];
1279                         [menuItem release];
1280                 }
1281         }
1284 - (NSMenu *)menuOfContainedContacts:(AIListObject *)inContact withTarget:(id)target
1286         return( [self menuOfContainedContacts:inContact forService:nil withTarget:target includeOffline:YES] );
1289 //Return either the highest metaContact containing this list object, or the list object itself.  Appropriate for when
1290 //preferences should be read from/to the most generalized contact possible.
1291 - (AIListObject *)parentContactForListObject:(AIListObject *)listObject
1293         if ([listObject isKindOfClass:[AIListContact class]]) {
1294                 //Find the highest-up metaContact
1295                 AIListObject    *containingObject;
1296                 while ([(containingObject = [listObject containingObject]) isKindOfClass:[AIMetaContact class]]) {
1297                         listObject = (AIMetaContact *)containingObject;
1298                 }
1299         }
1301         return(listObject);
1303 //Contact Info --------------------------------------------------------------------------------
1304 #pragma mark Contact Info
1305 //Show info for the selected contact
1306 - (IBAction)showContactInfo:(id)sender
1308         AIListObject *listObject = nil;
1310         if ((sender == menuItem_getInfoContextualContact) || (sender == menuItem_getInfoContextualGroup)) {
1311                 listObject = [[adium menuController] currentContextMenuObject];
1312         } else {
1313                 listObject = [self selectedListObject];
1314         }
1316         if (listObject) {
1317                 [NSApp activateIgnoringOtherApps:YES];
1318                 [[[AIContactInfoWindowController showInfoWindowForListObject:listObject] window] makeKeyAndOrderFront:nil];
1319         }
1322 #ifdef CONTACTS_INFO_WITH_PROMPT
1323 - (void)showSpecifiedContactInfo:(id)sender
1325         [ESShowContactInfoPromptController showPrompt];
1327 #endif
1329 //Add a contact info view
1330 - (void)addContactInfoPane:(AIContactInfoPane *)inPane
1332     [contactInfoPanes addObject:inPane];
1335 //Prepare the contact info menu and toolbar items
1336 - (void)prepareContactInfo
1338         contactInfoPanes = [[NSMutableArray alloc] init];
1340         //Add our get info contextual menu item
1341         menuItem_getInfoContextualContact = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:VIEW_CONTACTS_INFO
1342                                                                                                                                                                                          target:self
1343                                                                                                                                                                                          action:@selector(showContactInfo:)
1344                                                                                                                                                                           keyEquivalent:@""];
1345         [[adium menuController] addContextualMenuItem:menuItem_getInfoContextualContact
1346                                                                            toLocation:Context_Contact_Manage];
1348         menuItem_getInfoContextualGroup = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:VIEW_CONTACTS_INFO
1349                                                                                                                                                                                    target:self
1350                                                                                                                                                                                    action:@selector(showContactInfo:)
1351                                                                                                                                                                         keyEquivalent:@""];
1352         [[adium menuController] addContextualMenuItem:menuItem_getInfoContextualGroup
1353                                                                            toLocation:Context_Group_Manage];
1355         //Install the standard Get Info menu item which will always be command-shift-I
1356         menuItem_getInfo = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:VIEW_CONTACTS_INFO
1357                                                                                                                                                         target:self
1358                                                                                                                                                         action:@selector(showContactInfo:)
1359                                                                                                                                          keyEquivalent:@"i"];
1360         [menuItem_getInfo setKeyEquivalentModifierMask:GET_INFO_MASK];
1361         [[adium menuController] addMenuItem:menuItem_getInfo toLocation:LOC_Contact_Info];
1363         if([NSApp isOnPantherOrBetter]) {
1364                 /* Install the alternate Get Info menu item which will be alternately command-I and command-shift-I, in the contact list
1365                  * and in all other places, respectively.
1366                  */
1367                 menuItem_getInfoAlternate = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:VIEW_CONTACTS_INFO
1368                                                                                                                                                                                  target:self
1369                                                                                                                                                                                  action:@selector(showContactInfo:)
1370                                                                                                                                                                   keyEquivalent:@"i"];
1371                 [menuItem_getInfoAlternate setKeyEquivalentModifierMask:ALTERNATE_GET_INFO_MASK];
1372                 [menuItem_getInfoAlternate setAlternate:YES];
1373                 [[adium menuController] addMenuItem:menuItem_getInfoAlternate toLocation:LOC_Contact_Info];
1374                 
1375         //Register for the contact list notifications
1376         [[adium notificationCenter] addObserver:self selector:@selector(contactListDidBecomeMain:)
1377                                                                                    name:Interface_ContactListDidBecomeMain
1378                                                                                  object:nil];
1379         [[adium notificationCenter] addObserver:self selector:@selector(contactListDidResignMain:)
1380                                                                                    name:Interface_ContactListDidResignMain
1381                                                                                  object:nil];
1383                 //Watch changes in viewContactInfoMenuItem_alternate's menu so we can maintain its alternate status
1384                 //(it will expand into showing both the normal and the alternate items when the menu changes)
1385                 [[adium notificationCenter] addObserver:self selector:@selector(menuChanged:)
1386                                                                                    name:Menu_didChange
1387                                                                                  object:[menuItem_getInfoAlternate menu]];
1389     }
1391 #ifdef CONTACTS_INFO_WITH_PROMPT
1392         //Install the Get Info (prompting for a contact name) menu item
1393         menuItem_getInfo = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:VIEW_CONTACTS_INFO_WITH_PROMPT
1394                                                                                                                                                         target:self
1395                                                                                                                                                         action:@selector(showSpecifiedContactInfo:)
1396                                                                                                                                          keyEquivalent:@""];
1397         [[adium menuController] addMenuItem:menuItem_getInfo toLocation:LOC_Contact_Info];
1398 #endif
1400         //Add our get info toolbar item
1401         NSToolbarItem   *toolbarItem = [AIToolbarUtilities toolbarItemWithIdentifier:@"ShowInfo"
1402                                                                                                                                                    label:AILocalizedString(@"Info",nil)
1403                                                                                                                                         paletteLabel:TITLE_SHOW_INFO
1404                                                                                                                                                  toolTip:TITLE_SHOW_INFO
1405                                                                                                                                                   target:self
1406                                                                                                                                  settingSelector:@selector(setImage:)
1407                                                                                                                                          itemContent:[NSImage imageNamed:@"info" forClass:[self class]]
1408                                                                                                                                                   action:@selector(showContactInfo:)
1409                                                                                                                                                         menu:nil];
1410         [[adium toolbarController] registerToolbarItem:toolbarItem forToolbarType:@"ListObject"];
1413 //Always be able to show the inspector
1414 - (BOOL)validateMenuItem:(id <NSMenuItem>)menuItem
1416         if((menuItem == menuItem_getInfo) || (menuItem == menuItem_getInfoAlternate)) {
1417                 return([self selectedListObject] != nil);
1419         } else if((menuItem == menuItem_getInfoContextualContact) || (menuItem == menuItem_getInfoContextualGroup)) {
1420                 return([[adium menuController] currentContextMenuObject] != nil);
1422         }
1424         return YES;
1428 - (NSArray *)contactInfoPanes
1430         return(contactInfoPanes);
1433 - (void)contactListDidBecomeMain:(NSNotification *)notification
1435     [[adium menuController] removeItalicsKeyEquivalent];
1436     [menuItem_getInfoAlternate setKeyEquivalentModifierMask:(NSCommandKeyMask)];
1437         [menuItem_getInfoAlternate setAlternate:YES];
1440 - (void)contactListDidResignMain:(NSNotification *)notification
1442     //set our alternate modifier mask back to the obscure combination
1443     [menuItem_getInfoAlternate setKeyEquivalent:@"i"];
1444     [menuItem_getInfoAlternate setKeyEquivalentModifierMask:ALTERNATE_GET_INFO_MASK];
1445     [menuItem_getInfoAlternate setAlternate:YES];
1447     //Now give the italics its combination back
1448     [[adium menuController] restoreItalicsKeyEquivalent];
1451 - (void)menuChanged:(NSNotification *)notification
1453         [NSMenu updateAlternateMenuItem:menuItem_getInfoAlternate];
1457 //Selected contact ------------------------------------------------
1458 #pragma mark Selected contact
1459 //Returns the "selected"(represented) contact (By finding the first responder that returns a contact)
1460 //If no listObject is found, try to find a list object selected in a group chat
1461 - (AIListObject *)selectedListObject
1463         AIListObject *listObject = [self _performSelectorOnFirstAvailableResponder:@selector(listObject)];
1464         if( !listObject) {
1465                 listObject = [self _performSelectorOnFirstAvailableResponder:@selector(preferredListObject)];
1466         }
1467         return listObject;
1469 - (AIListObject *)selectedListObjectInContactList
1471         return([self _performSelectorOnFirstAvailableResponder:@selector(listObject) conformingToProtocol:@protocol(ContactListOutlineView)]);
1473 - (NSArray *)arrayOfSelectedListObjectsInContactList
1475         return([self _performSelectorOnFirstAvailableResponder:@selector(arrayOfListObjects) conformingToProtocol:@protocol(ContactListOutlineView)]);
1478 - (id)_performSelectorOnFirstAvailableResponder:(SEL)selector
1480     NSResponder *responder = [[[NSApplication sharedApplication] mainWindow] firstResponder];
1481     //Check the first responder
1482     if([responder respondsToSelector:selector]) {
1483         return([responder performSelector:selector]);
1484     }
1486     //Search the responder chain
1487     do{
1488         responder = [responder nextResponder];
1489         if([responder respondsToSelector:selector]) {
1490             return([responder performSelector:selector]);
1491         }
1493     } while(responder != nil);
1495     //None found, return nil
1496     return(nil);
1498 - (id)_performSelectorOnFirstAvailableResponder:(SEL)selector conformingToProtocol:(Protocol *)protocol
1500         NSResponder *responder = [[[NSApplication sharedApplication] mainWindow] firstResponder];
1501         //Check the first responder
1502         if([responder conformsToProtocol:protocol] && [responder respondsToSelector:selector]) {
1503                 return([responder performSelector:selector]);
1504         }
1506     //Search the responder chain
1507     do{
1508         responder = [responder nextResponder];
1509         if([responder conformsToProtocol:protocol] && [responder respondsToSelector:selector]) {
1510             return([responder performSelector:selector]);
1511         }
1513     } while(responder != nil);
1515     //None found, return nil
1516     return(nil);
1519 //Contact Sorting --------------------------------------------------------------------------------
1520 #pragma mark Contact Sorting
1521 //Register sorting code
1522 - (void)registerListSortController:(AISortController *)inController
1524     [sortControllerArray addObject:inController];
1526 - (NSArray *)sortControllerArray
1528     return(sortControllerArray);
1531 //Set and get the active sort controller
1532 - (void)setActiveSortController:(AISortController *)inController
1534     activeSortController = inController;
1536         [activeSortController didBecomeActive];
1538         //The newly-active sort controller needs to know whether it should be forced to ignore groups
1539         [[self activeSortController] forceIgnoringOfGroups:(useContactListGroups ? NO : YES)];
1541     //Resort the list
1542     [self sortContactList];
1544 - (AISortController *)activeSortController
1546     return(activeSortController);
1549 //Sort the entire contact list
1550 - (void)sortContactList
1552     [contactList sortGroupAndSubGroups:YES sortController:activeSortController];
1553         [[adium notificationCenter] postNotificationName:Contact_OrderChanged object:nil];
1556 //Sort an individual object
1557 - (void)sortListObject:(AIListObject *)inObject
1559         if(updatesAreDelayed) {
1560                 delayedContactChanges++;
1561         } else {
1562                 AIListObject            *group = [inObject containingObject];
1564                 if([group isKindOfClass:[AIListGroup class]]) {
1565                         //Sort the groups containing this object
1566                         [(AIListGroup *)group sortListObject:inObject sortController:activeSortController];
1567                         [[adium notificationCenter] postNotificationName:Contact_OrderChanged object:inObject];
1568                 }
1569         }
1572 //List object observers ------------------------------------------------------------------------------------------------
1573 #pragma mark List object observers
1574 //Registers code to observe handle status changes
1575 - (void)registerListObjectObserver:(id <AIListObjectObserver>)inObserver
1577         //Add the observer
1578     [contactObservers addObject:[NSValue valueWithNonretainedObject:inObserver]];
1580     //Let the new observer process all existing objects
1581         [self updateAllListObjectsForObserver:inObserver];
1584 - (void)unregisterListObjectObserver:(id)inObserver
1586     [contactObservers removeObject:[NSValue valueWithNonretainedObject:inObserver]];
1589 //Instructs a controller to update all available list objects
1590 - (void)updateAllListObjectsForObserver:(id <AIListObjectObserver>)inObserver
1592         NSEnumerator    *enumerator;
1593         AIListObject    *listObject;
1595         [self delayListObjectNotifications];
1597     //Reset all contacts
1598         enumerator = [contactDict objectEnumerator];
1599         while(listObject = [enumerator nextObject]) {
1600                 NSSet   *attributes = [inObserver updateListObject:listObject keys:nil silent:YES];
1601                 if(attributes) [self listObjectAttributesChanged:listObject modifiedKeys:attributes];
1603                 //If this contact is within a meta contact, update the meta contact too
1604                 AIListObject    *containingObject = [listObject containingObject];
1605                 if(containingObject && [containingObject isKindOfClass:[AIMetaContact class]]) {
1606                         NSSet   *attributes = [inObserver updateListObject:containingObject
1607                                                                                                                   keys:nil
1608                                                                                                                 silent:YES];
1609                         if(attributes) [self listObjectAttributesChanged:containingObject
1610                                                                                                 modifiedKeys:attributes];
1611                 }
1612         }
1614     //Reset all groups
1615         enumerator = [groupDict objectEnumerator];
1616         while(listObject = [enumerator nextObject]) {
1617                 NSSet   *attributes = [inObserver updateListObject:listObject keys:nil silent:YES];
1618                 if(attributes) [self listObjectAttributesChanged:listObject modifiedKeys:attributes];
1619         }
1621         //Reset all accounts
1622         enumerator = [[[adium accountController] accountArray] objectEnumerator];
1623         while(listObject = [enumerator nextObject]) {
1624                 NSSet   *attributes = [inObserver updateListObject:listObject keys:nil silent:YES];
1625                 if(attributes) [self listObjectAttributesChanged:listObject modifiedKeys:attributes];
1626         }
1628         //
1629         [self endListObjectNotificationsDelay];
1632 //Notify observers of a status change.  Returns the modified attribute keys
1633 - (NSSet *)_informObserversOfObjectStatusChange:(AIListObject *)inObject withKeys:(NSSet *)modifiedKeys silent:(BOOL)silent
1635         NSMutableSet    *attrChange = nil;
1636         NSEnumerator    *enumerator;
1637         NSValue                 *observerValue;
1638         
1639         //Let our observers know
1640         enumerator = [contactObservers objectEnumerator];
1641         while((observerValue = [enumerator nextObject])) {
1642                 id <AIListObjectObserver>       observer;
1643                 NSSet                                           *newKeys;
1645                 observer = [observerValue nonretainedObjectValue];
1646                 if((newKeys = [observer updateListObject:inObject keys:modifiedKeys silent:silent])) {
1647                         if (!attrChange) attrChange = [NSMutableSet set];
1648                         [attrChange unionSet:newKeys];
1649                 }
1650         }
1652         //Send out the notification for other observers
1653         [[adium notificationCenter] postNotificationName:ListObject_StatusChanged
1654                                                                                           object:inObject
1655                                                                                         userInfo:(modifiedKeys ? [NSDictionary dictionaryWithObject:modifiedKeys
1656                                                                                                                                                                                                  forKey:@"Keys"] : nil)];
1658         return(attrChange);
1661 //Command all observers to apply their attributes to an object
1662 - (void)_updateAllAttributesOfObject:(AIListObject *)inObject
1664         NSEnumerator    *enumerator = [contactObservers objectEnumerator];
1665         NSValue                 *observerValue;
1667         while((observerValue = [enumerator nextObject])) {
1668                 id <AIListObjectObserver>       observer;
1669                 
1670                 observer = [observerValue nonretainedObjectValue];
1672                 [observer updateListObject:inObject keys:nil silent:YES];
1673         }
1678 //Contact List Access --------------------------------------------------------------------------------------------------
1679 #pragma mark Contact List Access
1680 //Returns the main contact list group
1681 - (AIListGroup *)contactList
1683     return(contactList);
1686 //Return a flat array of all the objects in a group on an account (and all subgroups, if desired)
1687 - (NSMutableArray *)allContactsInGroup:(AIListGroup *)inGroup subgroups:(BOOL)subGroups onAccount:(AIAccount *)inAccount
1689         NSMutableArray  *contactArray = [NSMutableArray array];
1690         NSEnumerator    *enumerator;
1691     AIListObject        *object;
1693         if(inGroup == nil) inGroup = contactList;  //Passing nil scans the entire contact list
1695         enumerator = [inGroup objectEnumerator];
1696     while((object = [enumerator nextObject])) {
1697         if([object isMemberOfClass:[AIMetaContact class]] || [object isMemberOfClass:[AIListGroup class]]) {
1698             if(subGroups) {
1699                                 [contactArray addObjectsFromArray:[self allContactsInGroup:(AIListGroup *)object
1700                                                                                                                                  subgroups:subGroups
1701                                                                                                                                  onAccount:inAccount]];
1702                         }
1703                 } else if([object isMemberOfClass:[AIListContact class]]) {
1704                         if(!inAccount ||
1705                            ([(AIListContact *)object account] == inAccount)) {
1706                                 [contactArray addObject:object];
1707                         }
1708                 }
1709         }
1711         return(contactArray);
1714 //Contact List Menus- --------------------------------------------------------------------------------------------------
1715 #pragma mark Contact List Menus
1717 //Returns a menu containing all the groups within a group
1718 //- Selector called on group selection is selectGroup:
1719 //- The menu items represented object is the group it represents
1720 - (NSMenu *)menuOfAllGroupsInGroup:(AIListGroup *)inGroup withTarget:(id)target
1722         NSMenu  *menu = [[NSMenu alloc] initWithTitle:@""];
1724         [menu setAutoenablesItems:NO];
1725         [self _menuOfAllGroups:menu forGroup:inGroup withTarget:target level:0];
1727         return [menu autorelease];
1729 - (void)_menuOfAllGroups:(NSMenu *)menu forGroup:(AIListGroup *)group withTarget:(id)target level:(int)level
1731         NSEnumerator    *enumerator;
1732         AIListObject    *object;
1734         //Passing nil scans the entire contact list
1735         if(group == nil) group = contactList;
1737         //Enumerate this group and process all groups we find within it
1738         enumerator = [group objectEnumerator];
1739         while(object = [enumerator nextObject]) {
1740                 if([object isKindOfClass:[AIListGroup class]]) {
1741                         NSMenuItem      *menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:[object displayName]
1742                                                                                                                                                                                  target:target
1743                                                                                                                                                                                  action:@selector(selectGroup:)
1744                                                                                                                                                                   keyEquivalent:@""];
1745                         [menuItem setRepresentedObject:object];
1746                         if([menuItem respondsToSelector:@selector(setIndentationLevel:)]) {
1747                                 [menuItem setIndentationLevel:level];
1748                         }
1749                         [menu addItem:menuItem];
1750                         [menuItem release];
1751                         
1752                         [self _menuOfAllGroups:menu forGroup:(AIListGroup *)object withTarget:target level:level+1];
1753                 }
1754         }
1758 //Returns a menu containing all the objects in a group on an account
1759 //- Selector called on contact selection is selectContact:
1760 //- The menu item's represented object is the contact it represents
1761 - (NSMenu *)menuOfAllContactsInContainingObject:(AIListObject<AIContainingObject> *)inObject withTarget:(id)target{
1762         return([self menuOfAllContactsInContainingObject:inObject withTarget:target firstLevel:YES]);
1764 - (NSMenu *)menuOfAllContactsInContainingObject:(AIListObject<AIContainingObject> *)inObject withTarget:(id)target firstLevel:(BOOL)firstLevel
1766     NSEnumerator                                *enumerator;
1767     AIListObject                                *object;
1769         //Prepare our menu
1770         NSMenu *menu = [[NSMenu alloc] init];
1771         [menu setAutoenablesItems:NO];
1773         //Passing nil scans the entire contact list
1774         if(inObject == nil) inObject = contactList;
1776         //The pull down menu needs an extra item at the top of its root menu to handle the selection.
1777         if(firstLevel) [menu addItemWithTitle:@"" action:nil keyEquivalent:@""];
1779         //All menu items for all contained objects
1780         enumerator = [inObject listContactsEnumerator];
1781     while((object = [enumerator nextObject])) {
1782                 NSImage         *menuServiceImage;
1783                 NSMenuItem      *menuItem;
1784                 BOOL            needToCreateSubmenu;
1785                 BOOL            isGroup = [object isKindOfClass:[AIListGroup class]];
1786                 BOOL            isValidGroup = (isGroup &&
1787                                                                         [[(AIListGroup *)object containedObjects] count]);
1789                 //We don't want to include empty groups
1790                 if (!isGroup || isValidGroup) {
1792                         needToCreateSubmenu = (isValidGroup ||
1793                                                                    ([object isKindOfClass:[AIMetaContact class]] && ([[(AIMetaContact *)object listContacts] count] > 1)));
1795                         
1796                         menuServiceImage = [AIUserIcons menuUserIconForObject:object];
1797                         
1798                         menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:(needToCreateSubmenu ?
1799                                                                                                                                                                         [object displayName] :
1800                                                                                                                                                                         [object formattedUID])
1801                                                                                                                                                         target:target
1802                                                                                                                                                         action:@selector(selectContact:)
1803                                                                                                                                          keyEquivalent:@""];
1804                         
1805                         if(needToCreateSubmenu) {
1806                                 [menuItem setSubmenu:[self menuOfAllContactsInContainingObject:(AIListObject<AIContainingObject> *)object withTarget:target firstLevel:NO]];
1807                         }
1809                         [menuItem setRepresentedObject:object];
1810                         [menuItem setImage:menuServiceImage];
1811                         [menu addItem:menuItem];
1812                         [menuItem release];
1813                 }
1814         }
1816         return [menu autorelease];
1819 //Retrieving Specific Contacts -----------------------------------------------------------------------------------------
1820 #pragma mark Retrieving Specific Contacts
1822 //Retrieve a contact from the contact list (Creating if necessary)
1823 - (AIListContact *)contactWithService:(AIService *)inService account:(AIAccount *)inAccount UID:(NSString *)inUID
1825         AIListContact   *contact = nil;
1827         if(inUID && [inUID length] && inService) { //Ignore invalid requests
1828                 NSString                *key = [AIListContact internalUniqueObjectIDForService:inService
1829                                                                                                                                            account:inAccount
1830                                                                                                                                                    UID:inUID];
1832                 contact = [contactDict objectForKey:key];
1833                 if(!contact) {
1834                         //Create
1835                         contact = [[AIListContact alloc] initWithUID:inUID account:inAccount service:inService];
1837                         //Make sure this contact's order index isn't bigger than our current nextOrderIndex we'll vend to new contacts
1838                         float orderIndex = [contact orderIndex];
1839                         if (orderIndex > nextOrderIndex) nextOrderIndex = orderIndex + 1;
1841                         //Do the update thing
1842                         [self _updateAllAttributesOfObject:contact];
1844                         //Check to see if we should add to a metaContact
1845                         AIMetaContact *metaContact = [contactToMetaContactLookupDict objectForKey:[contact internalObjectID]];
1846                         if (metaContact) {
1847                                 /* We already know to add this object to the metaContact, since we did it before with another object,
1848                                    but this particular listContact is new and needs to be added directly to the metaContact
1849                                    (on future launches, the metaContact will obtain it automatically since all contacts matching this UID
1850                                    and serviceID should be included). */
1851                                 [self _performAddListObject:contact toMetaContact:metaContact];
1852                         }
1853                         
1854                         //Set the contact as mobile if it is a phone number
1855                         if([inUID characterAtIndex:0] == '+'){
1856                                 [contact setIsMobile:YES notify:NotifyNever];
1857                         }
1859                         //Add
1860                         [contactDict setObject:contact forKey:key];
1861                         [contact release];
1862                 }
1863         }
1865         return(contact);
1868 - (AIListContact *)existingContactWithService:(AIService *)inService account:(AIAccount *)inAccount UID:(NSString *)inUID
1870         if(inService && [inUID length]) {
1871                 return([contactDict objectForKey:[AIListContact internalUniqueObjectIDForService:inService
1872                                                                                                                                                                  account:inAccount
1873                                                                                                                                                                          UID:inUID]]);
1874         } else {
1875                 return(nil);
1876         }
1879 - (NSArray *)allContactsWithServiceID:(NSString *)inServiceID UID:(NSString *)inUID
1881         return([self allContactsWithService:[[adium accountController] firstServiceWithServiceID:inServiceID]
1882                                                                         UID:inUID]);
1885 - (NSArray *)allContactsWithService:(AIService *)service UID:(NSString *)inUID
1887         NSEnumerator    *enumerator;
1888         AIAccount               *account;
1889         NSMutableArray  *returnContactArray = [NSMutableArray array];
1891         enumerator = [[[adium accountController] accountsWithServiceClassOfService:service] objectEnumerator];
1893         while(account = [enumerator nextObject]) {
1894                 [returnContactArray addObject:[self contactWithService:service
1895                                                                                                            account:account
1896                                                                                                                    UID:inUID]];
1897         }
1899         return (returnContactArray);
1902 - (AIListObject *)existingListObjectWithUniqueID:(NSString *)uniqueID
1904         NSEnumerator    *enumerator;
1905         AIListObject    *listObject;
1907         //Contact
1908         enumerator = [contactDict objectEnumerator];
1909         while(listObject = [enumerator nextObject]) {
1910                 if([[listObject internalObjectID] isEqualToString:uniqueID]) return(listObject);
1911         }
1913         //Group
1914         enumerator = [groupDict objectEnumerator];
1915         while(listObject = [enumerator nextObject]) {
1916                 if([[listObject internalObjectID] isEqualToString:uniqueID]) return(listObject);
1917         }
1919         //Metacontact
1920         enumerator = [metaContactDict objectEnumerator];
1921         while(listObject = [enumerator nextObject]) {
1922                 if([[listObject internalObjectID] isEqualToString:uniqueID]) return(listObject);
1923         }
1925         return(nil);
1928 - (AIListContact *)preferredContactForContentType:(NSString *)inType forListContact:(AIListContact *)inContact
1930         AIListContact   *returnContact = nil;
1931         AIAccount               *account;
1933         if ([inContact isKindOfClass:[AIMetaContact class]]) {
1934                 AIListObject    *preferredContact;
1935                 NSString                *internalObjectID;
1938                 //If we've messaged this object previously, prefer the last contact we sent to if that
1939                 //contact is currently available
1940         internalObjectID = [inContact preferenceForKey:KEY_PREFERRED_DESTINATION_CONTACT
1941                                                                                                  group:OBJECT_STATUS_CACHE];
1943         if((internalObjectID) &&
1944                    (preferredContact = [self existingListObjectWithUniqueID:internalObjectID]) &&
1945                    ([preferredContact isKindOfClass:[AIListContact class]]) &&
1946                    ([preferredContact statusSummary] == AIAvailableStatus)) {
1947                         returnContact = [self preferredContactForContentType:inType
1948                                                                                                   forListContact:(AIListContact *)preferredContact];
1949         }
1951                 //If the last contact we sent to is not available, use the metaContact's preferredContact
1952                 if (!returnContact || ![returnContact online]) {
1953                         //Recurse into metacontacts if necessary
1954                         returnContact = [self preferredContactForContentType:inType
1955                                                                                                   forListContact:[(AIMetaContact *)inContact preferredContact]];
1956                 }
1958         } else {
1960                 //We have a flat contact; find the best account for talking to this contact,
1961                 //and return an AIListContact on that account
1962                 account = [[adium accountController] preferredAccountForSendingContentType:inType
1963                                                                                                                                                  toContact:inContact];
1964                 if (account) {
1965                         if ([inContact account] == account) {
1966                                 returnContact = inContact;
1967                         } else {
1968                                 returnContact = [self contactWithService:[inContact service]
1969                                                                                                  account:account
1970                                                                                                          UID:[inContact UID]];
1971                         }
1972                 }
1973         }
1975         return(returnContact);
1978 //Retrieve a list contact matching the UID and serviceID of the passed contact but on the specified account.
1979 //In many cases this will be the same as inContact.
1980 - (AIListContact *)contactOnAccount:(AIAccount *)account fromListContact:(AIListContact *)inContact
1982         if(account && ([inContact account] != account)) {
1983                 return([self contactWithService:[inContact service] account:account UID:[inContact UID]]);
1984         } else {
1985                 return(inContact);
1986         }
1989 //XXX - This is ridiculous.
1990 - (AIListContact *)preferredContactWithUID:(NSString *)inUID andServiceID:(NSString *)inService forSendingContentType:(NSString *)inType
1992         AIService               *theService = [[adium accountController] firstServiceWithServiceID:inService];
1993         AIListContact   *tempListContact = [[AIListContact alloc] initWithUID:inUID
1994                                                                                                                                 service:theService];
1995         AIAccount               *account = [[adium accountController] preferredAccountForSendingContentType:CONTENT_MESSAGE_TYPE
1996                                                                                                                                                                           toContact:tempListContact
1997                                                                                                                                                                  includeOffline:YES];
1998         [tempListContact release];
2000         return([self contactWithService:theService account:account UID:inUID]);
2004 //Watch outgoing content, remembering the user's choice of destination contact for contacts within metaContacts
2005 - (void)didSendContent:(NSNotification *)notification
2007     AIChat                      *chat = [[notification userInfo] objectForKey:@"AIChat"];
2008     AIListContact       *destContact = [chat listObject];
2009     AIListObject        *metaContact;
2011         //Note: parentContactForListObject returns the passed contact if it is not in a metaContact;
2012         //this is a quick and easy check.
2013     if((chat) &&
2014            (destContact) &&
2015            ((metaContact = [self parentContactForListObject:destContact]) != destContact)) {
2017                 [metaContact setPreference:[destContact internalObjectID]
2018                                                         forKey:KEY_PREFERRED_DESTINATION_CONTACT
2019                                                          group:OBJECT_STATUS_CACHE];
2020     }
2023 //Retrieving Groups ----------------------------------------------------------------------------------------------------
2024 #pragma mark Retrieving Groups
2026 //Retrieve a group from the contact list (Creating if necessary)
2027 - (AIListGroup *)groupWithUID:(NSString *)groupUID
2029         AIListGroup             *group;
2031         if(!groupUID || ![groupUID length] || [groupUID isEqualToString:ADIUM_ROOT_GROUP_NAME]) {
2032                 //Return our root group if it is requested
2033                 group = contactList;
2034         } else {
2035                 if(!(group = [groupDict objectForKey:groupUID])) {
2036                         //Create
2037                         group = [[AIListGroup alloc] initWithUID:groupUID];
2039                         //Place new groups at the bottom of our list (by giving them the largest ordering index)
2040                         float orderIndex = [group orderIndex];
2041                         if (orderIndex > nextOrderIndex) nextOrderIndex = orderIndex + 1;
2043                         //Add
2044                         [self _updateAllAttributesOfObject:group];
2045                         [groupDict setObject:group forKey:groupUID];
2047                         //Add to target group
2048                         [contactList addObject:group];
2049                         [self _listChangedGroup:contactList object:group];
2050                         
2051                         //Add to the contact list
2052                         [contactList addObject:group];
2053                         [self _listChangedGroup:contactList object:group];
2055                         [group release];
2056                 }
2057         }
2059         return(group);
2062 - (AIListGroup *)existingGroupWithUID:(NSString *)groupUID
2064         AIListGroup             *group;
2066         if (!groupUID || ![groupUID length] || [groupUID isEqualToString:ADIUM_ROOT_GROUP_NAME]) {
2067                 //Return our root group if it is requested
2068                 group = contactList;
2069         } else {
2070                 group = [groupDict objectForKey:groupUID];
2071         }
2072         
2073         return group;
2076 //Contact list editing -------------------------------------------------------------------------------------------------
2077 #pragma mark Contact list editing
2078 - (void)removeListObjects:(NSArray *)objectArray
2080         NSEnumerator    *enumerator = [objectArray objectEnumerator];
2081         AIListObject    *listObject;
2083         while(listObject = [enumerator nextObject]) {
2084                 if([listObject isKindOfClass:[AIMetaContact class]]) {
2085                         NSArray *objectsToRemove = nil;
2087                         //If the metaContact only has one listContact, we will remove that contact from all accounts
2088                         if([[(AIMetaContact *)listObject listContacts] count] == 1) {
2089                                 AIListContact   *listContact = [[(AIMetaContact *)listObject listContacts] objectAtIndex:0];
2090                                 
2091                                 objectsToRemove = [self allContactsWithService:[listContact service]
2092                                                                                                                    UID:[listContact UID]];
2093                         }
2095                         //And actually remove the single contact if applicable
2096                         if(objectsToRemove) {
2097                                 [self removeListObjects:objectsToRemove];
2098                         }
2099                         
2100                         //Now break the metaContact down, taking out all contacts and putting them back in the main list
2101                         [self breakdownAndRemoveMetaContact:(AIMetaContact *)listObject];                               
2103                 } else if([listObject isKindOfClass:[AIListGroup class]]) {
2104                         AIListObject    *containingObject = [listObject containingObject];
2105                         NSEnumerator    *enumerator;
2106                         AIAccount               *account;
2108                         //If this is a group, delete all the objects within it
2109                         [self removeListObjects:[(AIListGroup *)listObject containedObjects]];
2111                         //Delete the list off of all active accounts
2112                         enumerator = [[[adium accountController] accountArray] objectEnumerator];
2113                         while (account = [enumerator nextObject]) {
2114                                 if ([account online]) {
2115                                         [account deleteGroup:(AIListGroup *)listObject];
2116                                 }
2117                         }
2119                         //Then, procede to delete the group
2120                         [listObject retain];
2121                         [(AIMetaContact *)containingObject removeObject:listObject];
2122                         [groupDict removeObjectForKey:[listObject UID]];
2123                         [self _listChangedGroup:containingObject object:listObject];
2124                         [listObject release];
2126                 } else {
2127                         AIAccount       *account = [(AIListContact *)listObject account];
2128                         if ([account online]) {
2129                                 [account removeContacts:[NSArray arrayWithObject:listObject]];
2130                         }
2131                 }
2132         }
2135 - (void)addContacts:(NSArray *)contactArray toGroup:(AIListGroup *)group
2137         NSEnumerator    *enumerator;
2138         AIListContact   *listObject;
2140         [self delayListObjectNotifications];
2142         enumerator = [contactArray objectEnumerator];
2143         while(listObject = [enumerator nextObject]) {
2144                 [[listObject account] addContacts:[NSArray arrayWithObject:listObject] toGroup:group];
2145         }
2147         [self endListObjectNotificationsDelay];
2150 - (void)requestAddContactWithUID:(NSString *)contactUID service:(AIService *)inService
2152         NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:contactUID, UID_KEY, inService, @"service",nil];
2153         [[adium notificationCenter] postNotificationName:Contact_AddNewContact
2154                                                                                           object:nil
2155                                                                                         userInfo:userInfo];
2158 - (void)moveListObjects:(NSArray *)objectArray toGroup:(AIListObject<AIContainingObject> *)group index:(int)index
2160         NSEnumerator    *enumerator;
2161         AIListContact   *listContact;
2163         [self delayListObjectNotifications];
2165         if([group respondsToSelector:@selector(setDelayContainedObjectSorting:)]) {
2166                 [(id)group setDelayContainedObjectSorting:YES];
2167         }
2169         enumerator = [objectArray objectEnumerator];
2170         while(listContact = [enumerator nextObject]) {
2171                 [self moveContact:listContact toGroup:group];
2173                 //Set the new index / position of the object
2174                 [self _positionObject:listContact atIndex:index inGroup:group];
2175         }
2177         [self endListObjectNotificationsDelay];
2179         if([group respondsToSelector:@selector(setDelayContainedObjectSorting:)]) {
2180                 [(id)group setDelayContainedObjectSorting:NO];
2181         }
2183         /*
2184          Resort the entire list if we are moving within or between AIListGroup objects
2185          (other containing objects such as metaContacts will handle their own sorting).
2186         */
2187         if([group isKindOfClass:[AIListGroup class]]) {
2188                 [self sortContactList];
2189         }
2192 - (void)moveContact:(AIListContact *)listContact toGroup:(AIListObject<AIContainingObject> *)group
2194         //Move the object to the new group if necessary
2195         if(group != [listContact containingObject]) {
2197                 if ([group isKindOfClass:[AIListGroup class]]) {
2198                         if([listContact isKindOfClass:[AIMetaContact class]]) {
2199                                 //This is a meta contact, move the objects within it.  listContacts will give us a flat array of AIListContacts.
2201                                 /*
2202                                 NSEnumerator    *metaEnumerator;
2203                                 AIListContact   *aContainedContact;
2205                                 metaEnumerator = [[(AIMetaContact *)listContact listContacts] objectEnumerator];
2206                                 while(aContainedContact = [metaEnumerator nextObject]) {
2207                                         NSEnumerator    *allContactsEnumerator;
2208                                         AIListContact   *specificContact;
2210                                         //Leave no contact behind.
2211                                         allContactsEnumerator = [[self allContactsWithService:[aContainedContact service]
2212                                                                                                                                           UID:[aContainedContact UID]] objectEnumerator];
2213                                         while (specificContact = [allContactsEnumerator nextObject]) {
2214                                                 [self _moveObjectServerside:specificContact toGroup:(AIListGroup *)group];
2215                                         }
2216                                 }
2217                                 */
2219                                 [self _moveContactLocally:listContact toGroup:(AIListGroup *)group];
2221                         } else if([listContact isKindOfClass:[AIListContact class]]) {
2222                                 //Move the object
2223                                 [self _moveObjectServerside:listContact toGroup:(AIListGroup *)group];
2224                         }
2226                 } else if ([group isKindOfClass:[AIMetaContact class]]) {
2227                         //Moving a contact into a meta contact
2228                         [self addListObject:listContact toMetaContact:(AIMetaContact *)group];
2229                 }
2230         }
2233 //Move an object to another group
2234 - (void)_moveObjectServerside:(AIListObject *)listObject toGroup:(AIListGroup *)group
2236         AIAccount       *account = [(AIListContact *)listObject account];
2237         if ([account online]) {
2238                 [account moveListObjects:[NSArray arrayWithObject:listObject] toGroup:group];
2239         }
2242 //Rename a group
2243 - (void)_renameGroup:(AIListGroup *)listGroup to:(NSString *)newName
2245         NSEnumerator    *enumerator = [[[adium accountController] accountArray] objectEnumerator];
2246         AIAccount               *account;
2248         //Since Adium has no memory of what accounts a group is on, we have to send this message to all available accounts
2249         //The accounts without this group will just ignore it
2250         while(account = [enumerator nextObject]) {
2251                 [account renameGroup:listGroup to:newName];
2252         }
2254         //Remove the old group if it's empty
2255         if([listGroup containedObjectsCount] == 0) {
2256                 [self removeListObjects:[NSArray arrayWithObject:listGroup]];
2257         }
2260 //Position a list object within a group
2261 - (void)_positionObject:(AIListObject *)listObject atIndex:(int)index inGroup:(AIListObject<AIContainingObject> *)group
2263         if(index == 0) {
2264                 //Moved to the top of a group.  New index is between 0 and the lowest current index
2265                 [listObject setOrderIndex:([group smallestOrder] / 2.0)];
2267         } else if(index >= [group visibleCount]) {
2268                 //Moved to the bottom of a group.  New index is one higher than the highest current index
2269                 [listObject setOrderIndex:([group largestOrder] + 1.0)];
2271         } else {
2272                 //Moved somewhere in the middle.  New index is the average of the next largest and smallest index
2273                 AIListObject    *previousObject = [group objectAtIndex:index-1];
2274                 AIListObject    *nextObject = [group objectAtIndex:index];
2275                 float nextLowest = [previousObject orderIndex];
2276                 float nextHighest = [nextObject orderIndex];
2278                 //
2279                 [listObject setOrderIndex:((nextHighest + nextLowest) / 2.0)];
2280         }
2283 - (float)nextOrderIndex
2285         return nextOrderIndex++;
2288 @end