French updates
[adiumx.git] / Source / AIAccountController.m
blob78a2470d761178b2bec1a39cef32bbaf08835288
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 #import "AIAccountController.h"
20 #import "AIContactController.h"
21 #import "AIContentController.h"
22 #import "AILoginController.h"
23 #import "AIPreferenceController.h"
24 #import "AIStatusController.h"
25 #import "ESAccountPasswordPromptController.h"
26 #import "ESProxyPasswordPromptController.h"
27 #import <AIUtilities/AIDictionaryAdditions.h>
28 #import <AIUtilities/AIKeychain.h>
29 #import <AIUtilities/AIKeychainOld.h>
30 #import <AIUtilities/AIMenuAdditions.h>
31 #import <AIUtilities/CBObjectAdditions.h>
32 #import <AIUtilities/ESImageAdditions.h>
33 #import <Adium/AIAccount.h>
34 #import <Adium/AIChat.h>
35 #import <Adium/AIContentMessage.h>
36 #import <Adium/AIContentObject.h>
37 #import <Adium/AIListContact.h>
38 #import <Adium/AIListObject.h>
39 #import <Adium/AIMetaContact.h>
40 #import <Adium/AIService.h>
41 #import <Adium/AIServiceIcons.h>
42 #import <Adium/AIStatusIcons.h>
44 //Paths and Filenames
45 #define PREF_GROUP_PREFERRED_ACCOUNTS   @"Preferred Accounts"
46 #define ACCOUNT_DEFAULT_PREFS                   @"AccountPrefs"
48 //Preference keys
49 #define TOP_ACCOUNT_ID                                  @"TopAccountID"         //Highest account object ID
50 #define ACCOUNT_LIST                                    @"Accounts"             //Array of accounts
51 #define ACCOUNT_TYPE                                    @"Type"                         //Account type
52 #define ACCOUNT_SERVICE                                 @"Service"                      //Account service
53 #define ACCOUNT_UID                                             @"UID"                          //Account UID
54 #define ACCOUNT_OBJECT_ID                               @"ObjectID"             //Account object ID
56 //Other
57 #define KEY_PREFERRED_SOURCE_ACCOUNT    @"Preferred Account"
59 //In AdiumFramework in 0.9
60 #define NEW_ACCOUNT_DISPLAY_TEXT                        AILocalizedStringFromTable(@"<New Account>", @"AdiumFramework", "Placeholder displayed as the name of a new account")
62 //XXX
63 #import "AIAccountListWindowController.h"
65 @interface AIAccountController (PRIVATE)
66 - (void)loadAccounts;
67 - (void)_addMenuItemsToMenu:(NSMenu *)menu withTarget:(id)target forAccounts:(NSArray *)accounts;
68 - (NSArray *)_accountsForSendingContentType:(NSString *)inType toListObject:(AIListObject *)inObject preferred:(BOOL)inPreferred includeOffline:(BOOL)includeOffline;
70 - (void)_addAccountMenuItemsForPlugin:(id<AccountMenuPlugin>)accountMenuPlugin;
71 - (void)_removeAccountMenuItemsForPlugin:(id<AccountMenuPlugin>)accountMenuPlugin;
72 - (void)_updateMenuItem:(NSMenuItem *)menuItem forAccount:(AIAccount *)account forPlugin:(id<AccountMenuPlugin>)accountMenuPlugin;
73 - (NSMenuItem *)_menuItemForAccount:(AIAccount *)account fromArray:(NSArray *)accountMenuItemArray;
74 - (void)rebuildAllAccountMenuItems;
76 - (BOOL)_account:(AIAccount *)account canSendContentType:(NSString *)inType toListObject:(AIListObject *)inObject preferred:(BOOL)inPreferred includeOffline:(BOOL)includeOffline;
78 - (void)_upgradePasswords;
80 - (void)_prepareAccountMenus;
81 - (NSMenu *)actionsMenuForAccount:(AIAccount *)inAccount;
83 @end
85 @implementation AIAccountController
87 //init
88 - (void)initController
90     availableServiceDict = [[NSMutableDictionary alloc] init];
91         availableServiceTypeDict = [[NSMutableDictionary alloc] init];
92     accountArray = nil;
93     lastAccountIDToSendContent = [[NSMutableDictionary alloc] init];
94         accountMenuItemArraysDict = [[NSMutableDictionary alloc] init];
95         accountMenuPluginsDict = [[NSMutableDictionary alloc] init];
96         _cachedActiveServices = nil;
98         [[adium notificationCenter] addObserver:self
99                                                                    selector:@selector(rebuildAllAccountMenuItems)
100                                                                            name:AIStatusIconSetDidChangeNotification
101                                                                          object:nil];
102         [[adium notificationCenter] addObserver:self
103                                                                    selector:@selector(rebuildAllAccountMenuItems)
104                                                                            name:AIServiceIconSetDidChangeNotification
105                                                                          object:nil];
106         
107         
108         //Default account preferences
109         [[adium preferenceController] registerDefaults:[NSDictionary dictionaryNamed:ACCOUNT_DEFAULT_PREFS forClass:[self class]]
110                                                                                   forGroup:PREF_GROUP_ACCOUNTS];
113 //Finish initialization once other controllers have set themselves up
114 - (void)finishIniting
115 {   
116         //Observe account changes, both list and online/connecting/offline
117     [[adium notificationCenter] addObserver:self
118                                                                    selector:@selector(accountListChanged:)
119                                                                            name:Account_ListChanged
120                                                                          object:nil];
121     [[adium contactController] registerListObjectObserver:self];
122         
123     //Load the user accounts
124     [self loadAccounts];
125     
126     //Observe content (for accountForSendingContentToContact)
127     [[adium notificationCenter] addObserver:self
128                                    selector:@selector(didSendContent:)
129                                        name:CONTENT_MESSAGE_SENT
130                                      object:nil];
132         /* Temporary upgrade code! */
133         NSUserDefaults  *userDefaults = [NSUserDefaults standardUserDefaults];
134         NSNumber        *didPasswordUpgrade = [userDefaults objectForKey:@"Adium:Did Password Upgrade"];
135         if(!didPasswordUpgrade || ![didPasswordUpgrade boolValue]){
136                 [userDefaults setObject:[NSNumber numberWithBool:YES]
137                                                  forKey:@"Adium:Did Password Upgrade"];
138                 [userDefaults synchronize];
140                 if([accountArray count]){
141                         [self _upgradePasswords];
142                 }
143         }
145         [self _prepareAccountMenus];
148 //close
149 - (void)closeController
151     //Disconnect all accounts
152     [self disconnectAllAccounts];
153     
154     //Remove observers (otherwise, every account added will be a duplicate next time around)
155     [[adium notificationCenter] removeObserver:self];
156         [[adium contactController] unregisterListObjectObserver:self];
159 - (void)dealloc
161         //Cleanup
162     [accountArray release];
163         [unloadableAccounts release];
164     [availableServiceDict release];
165     [lastAccountIDToSendContent release];
166         [accountMenuItemArraysDict release];
167         [accountMenuPluginsDict release];
169         [_cachedActiveServices release]; _cachedActiveServices = nil;
170         
171         [super dealloc];
174 //Account Storage ------------------------------------------------------------------------------------------------------
175 #pragma mark Account Storage
176 //Loads the saved accounts
177 - (void)loadAccounts
179     NSArray                     *accountList;
180         NSEnumerator    *enumerator;
181         NSDictionary    *accountDict;
183         //Close down any existing accounts
184         [accountArray release]; accountArray = [[NSMutableArray alloc] init];
185         [unloadableAccounts release]; unloadableAccounts = [[NSMutableArray alloc] init];       
186         
187         accountList = [[adium preferenceController] preferenceForKey:ACCOUNT_LIST group:PREF_GROUP_ACCOUNTS];
188         
189     //Create an instance of every saved account
190         enumerator = [accountList objectEnumerator];
191         while(accountDict = [enumerator nextObject]){
192                 NSString                *serviceID = [accountDict objectForKey:ACCOUNT_TYPE];
193         AIAccount               *newAccount;
194         AIService               *service;
195                 NSString                *accountUID;
196                 NSString                *internalObjectID;
197                 
198                 //TEMPORARY UPGRADE CODE  0.63 -> 0.70 (Account format changed)
199                 //####################################
200                 if([serviceID isEqualToString:@"AIM-LIBGAIM"]){
201                         NSString        *uid = [accountDict objectForKey:ACCOUNT_UID];
202                         if(uid && [uid length]){
203                                 const char      firstCharacter = [uid characterAtIndex:0];
204                                 
205                                 if([uid hasSuffix:@"@mac.com"]){
206                                         serviceID = @"libgaim-oscar-Mac";
207                                 }else if(firstCharacter >= '0' && firstCharacter <= '9'){
208                                         serviceID = @"libgaim-oscar-ICQ";
209                                 }else{
210                                         serviceID = @"libgaim-oscar-AIM";
211                                 }
212                         }
213                 }else if([serviceID isEqualToString:@"GaduGadu-LIBGAIM"]){
214                         serviceID = @"libgaim-Gadu-Gadu";
215                 }else if([serviceID isEqualToString:@"Jabber-LIBGAIM"]){
216                         serviceID = @"libgaim-Jabber";
217                 }else if([serviceID isEqualToString:@"MSN-LIBGAIM"]){
218                         serviceID = @"libgaim-MSN";
219                 }else if([serviceID isEqualToString:@"Napster-LIBGAIM"]){
220                         serviceID = @"libgaim-Napster";
221                 }else if([serviceID isEqualToString:@"Novell-LIBGAIM"]){
222                         serviceID = @"libgaim-GroupWise";
223                 }else if([serviceID isEqualToString:@"Sametime-LIBGAIM"]){
224                         serviceID = @"libgaim-Sametime";
225                 }else if([serviceID isEqualToString:@"Yahoo-LIBGAIM"]){
226                         serviceID = @"libgaim-Yahoo!";
227                 }else if([serviceID isEqualToString:@"Yahoo-Japan-LIBGAIM"]){
228                         serviceID = @"libgaim-Yahoo!-Japan";
229                 }
230                 //####################################
232                 //XXX: Temporary Rendezvous -> Bonjour code
233                 if([serviceID isEqualToString:@"rvous-libezv"]){
234                         serviceID = @"bonjour-libezv";
235                 }
236                 
237                 //Fetch the account service, UID, and ID
238                 service = [self serviceWithUniqueID:serviceID];
239                 accountUID = [accountDict objectForKey:ACCOUNT_UID];
240                 internalObjectID = [accountDict objectForKey:ACCOUNT_OBJECT_ID];
241                 
242         //Create the account and add it to our array
243         if(service && accountUID && [accountUID length]){
244                         if(newAccount = [self createAccountWithService:service UID:accountUID internalObjectID:internalObjectID]){
245                 [accountArray addObject:newAccount];
246             }else{
247                                 [unloadableAccounts addObject:accountDict];
248                         }
249         }
250     }
251         
252         //Broadcast an account list changed notification
253     [[adium notificationCenter] postNotificationName:Account_ListChanged object:nil userInfo:nil];
256 //Save the accounts
257 - (void)saveAccounts
259         NSMutableArray  *flatAccounts = [NSMutableArray array];
260         NSEnumerator    *enumerator;
261         AIAccount               *account;
262         
263         //Build a flattened array of the accounts
264         enumerator = [accountArray objectEnumerator];
265         while(account = [enumerator nextObject]){
266                 NSMutableDictionary             *flatAccount = [NSMutableDictionary dictionary];
267                 
268                 [flatAccount setObject:[[account service] serviceCodeUniqueID] forKey:ACCOUNT_TYPE];    //Unique plugin ID
269                 [flatAccount setObject:[[account service] serviceID] forKey:ACCOUNT_SERVICE];           //Shared service ID
270                 [flatAccount setObject:[account UID] forKey:ACCOUNT_UID];                                                       //Account UID
271                 [flatAccount setObject:[account internalObjectID] forKey:ACCOUNT_OBJECT_ID];                    //Account Object ID
272                 
273                 [flatAccounts addObject:flatAccount];
274         }
275         
276         //Add any unloadable accounts so they're not lost
277         [flatAccounts addObjectsFromArray:unloadableAccounts];
278         
279         //Save
280         [[adium preferenceController] setPreference:flatAccounts forKey:ACCOUNT_LIST group:PREF_GROUP_ACCOUNTS];
281         
282         //Broadcast an account list changed notification
283         [[adium notificationCenter] postNotificationName:Account_ListChanged object:nil userInfo:nil];
286 //Returns a new account of the specified type (Unique service plugin ID)
287 - (AIAccount *)createAccountWithService:(AIService *)service UID:(NSString *)inUID internalObjectID:(NSString *)internalObjectID
288 {       
289         //Filter the UID
290         inUID = [service filterUID:inUID removeIgnoredCharacters:YES];
291         
292         //If no object ID is provided, use the next largest integer
293         if(!internalObjectID){
294                 int     topAccountID = [[[adium preferenceController] preferenceForKey:TOP_ACCOUNT_ID group:PREF_GROUP_ACCOUNTS] intValue];
295                 internalObjectID = [NSString stringWithFormat:@"%i",topAccountID];
296                 [[adium preferenceController] setPreference:[NSNumber numberWithInt:topAccountID + 1]
297                                                                                          forKey:TOP_ACCOUNT_ID
298                                                                                           group:PREF_GROUP_ACCOUNTS];
299         }
300         
301         //Create the account
302         return([service accountWithUID:inUID internalObjectID:internalObjectID]);
306 //Services -------------------------------------------------------------------------------------------------------
307 #pragma mark Services
308 //Sort an array of services alphabetically by their description
309 int _alphabeticalServiceSort(id service1, id service2, void *context)
311         return([(NSString *)[service1 longDescription] caseInsensitiveCompare:(NSString *)[service2 longDescription]]);
314 //Return the available services.  These are used for account creation.
315 - (NSArray *)availableServices
317         return([[availableServiceDict allValues] sortedArrayUsingFunction:_alphabeticalServiceSort context:nil]);
320 //Return the active services (services for which there is an account).  These are used for contact creation and determining if
321 //the service of accounts and contacts should be presented to the user.
322 //Simultaneously determine if any active service can 
323 //If includeCompatible is YES, services not being used but in the same service class as one that is will be included
324 - (NSArray *)activeServicesIncludingCompatibleServices:(BOOL)includeCompatible
326         if(!_cachedActiveServices){
327                 NSMutableArray  *serviceArray = [NSMutableArray array];
328                 NSEnumerator    *enumerator = [accountArray objectEnumerator];
329                 AIAccount               *account;
331                 //Build an array of all currently used services
332                 while(account = [enumerator nextObject]){
333                         if(includeCompatible){
334                                 NSEnumerator    *serviceEnumerator;
335                                 AIService               *accountService;
336                                 
337                                 //Add all services that are of the same class as the user's account (So, all compatible services)
338                                 serviceEnumerator = [[self servicesWithServiceClass:[[account service] serviceClass]] objectEnumerator];
339                                 while(accountService = [serviceEnumerator nextObject]){
340                                         //Prevent any service from going in twice
341                                         if(![serviceArray containsObject:accountService]){
342                                                 [serviceArray addObject:accountService];
343                                         }
344                                         
345                                 }
346                         }else{
347                                 if(![serviceArray containsObject:[account service]]){
348                                         [serviceArray addObject:[account service]];
349                                 }
350                         }
351                 }
352                 
353                 //Sort
354                 _cachedActiveServices = [[serviceArray sortedArrayUsingFunction:_alphabeticalServiceSort context:nil] retain];
355         }
356         
357         return(_cachedActiveServices);
360 //Returns the specified service controller
361 - (AIService *)serviceWithUniqueID:(NSString *)identifier
363     return([availableServiceDict objectForKey:identifier]);
366 //Return the first service with the specified serviceID
367 - (AIService *)firstServiceWithServiceID:(NSString *)serviceID
369         NSEnumerator    *enumerator = [availableServiceDict objectEnumerator];
370         AIService               *service;
371         
372         while(service = [enumerator nextObject]){
373                 if([[service serviceID] isEqualToString:serviceID]) break;
374         }
375         
376         return(service);
379 - (NSArray *)servicesWithServiceClass:(NSString *)serviceClass
381         NSEnumerator    *enumerator = [availableServiceDict objectEnumerator];
382         AIService               *service;
383         NSMutableArray  *servicesArray = [NSMutableArray array];
384                 
385         while(service = [enumerator nextObject]){
386                 if([[service serviceClass] isEqualToString:serviceClass]) [servicesArray addObject:service];
387         }
388         
389         return(servicesArray);
392 - (BOOL)serviceWithUniqueIDIsOnline:(NSString *)identifier
394         AIService               *service = [self serviceWithUniqueID:identifier];
395     NSEnumerator        *enumerator = [accountArray objectEnumerator];
396     AIAccount           *account;
397     
398     while((account = [enumerator nextObject])){
399                 if(([account service] == service) &&
400                    [account online]) return YES;
401     }
402     
403     return(NO);
406 //Register service code
407 - (void)registerService:(AIService *)inService
409     [availableServiceDict setObject:inService forKey:[inService serviceCodeUniqueID]];
410         
411         [availableServiceTypeDict setObject:inService forKey:[inService serviceID]];
414 //Returns a menu of all services.
415 //- Selector called on service selection is selectAccount:
416 //- The menu item's represented objects are the service controllers they represent
417 //- Format allows the description to be placed within a format string. If it is nil, the description alone will be used.
418 - (NSMenu *)menuOfServicesWithTarget:(id)target activeServicesOnly:(BOOL)activeServicesOnly longDescription:(BOOL)longDescription format:(NSString *)format
420         AIServiceImportance     importance;
421         unsigned                        numberOfItems = 0;
422         NSArray                         *serviceArray;
423         
424         //Prepare our menu
425         NSMenu *menu = [[NSMenu alloc] init];
427         serviceArray = (activeServicesOnly ? [self activeServicesIncludingCompatibleServices:YES] : [self availableServices]);
428         
429         //Divide our menu into sections.  This helps separate less important services from the others (sorry guys!)
430         for(importance = AIServicePrimary; importance <= AIServiceUnsupported; importance++){
431                 NSEnumerator    *enumerator;
432                 AIService               *service;
433                 unsigned                currentNumberOfItems;
434                 BOOL                    addedDivider = NO;
435                 
436                 //Divider
437                 currentNumberOfItems = [menu numberOfItems];
438                 if (currentNumberOfItems > numberOfItems){
439                         [menu addItem:[NSMenuItem separatorItem]];
440                         numberOfItems = currentNumberOfItems + 1;
441                         addedDivider = YES;
442                 }
444                 //Insert a menu item for each service of this importance
445                 enumerator = [serviceArray objectEnumerator];
446                 while((service = [enumerator nextObject])){
447                         if([service serviceImportance] == importance){
448                                 NSString        *description = (longDescription ?
449                                                                                         [service longDescription] :
450                                                                                         [service shortDescription]);
451                                 
452                                 NSMenuItem      *menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:(format ? 
453                                                                                                                                                                                                          [NSString stringWithFormat:format,description] :
454                                                                                                                                                                                                          description)
455                                                                                                                                                                                          target:target 
456                                                                                                                                                                                          action:@selector(selectServiceType:) 
457                                                                                                                                                                           keyEquivalent:@""];
458                                 [menuItem setRepresentedObject:service];
459                                 [menuItem setImage:[AIServiceIcons serviceIconForService:service
460                                                                                                                                         type:AIServiceIconSmall
461                                                                                                                            direction:AIIconNormal]];
462                                 [menu addItem:menuItem];
463                                 [menuItem release];
464                         }
465                 }
466                 
467                 //If we added a divider but didn't add any items, remove it
468                 currentNumberOfItems = [menu numberOfItems];
469                 if (addedDivider && (currentNumberOfItems <= numberOfItems) && (currentNumberOfItems > 0)){
470                         [menu removeItemAtIndex:(currentNumberOfItems-1)];
471                 }
472         }
473         
474         return([menu autorelease]);
475 }       
477 //Accounts -------------------------------------------------------------------------------------------------------
478 #pragma mark Accounts
479 //Returns all available accounts
480 - (NSArray *)accountArray
482     return(accountArray);
485 //Searches the account list for the specified account
486 - (AIAccount *)accountWithInternalObjectID:(NSString *)objectID
488     NSEnumerator        *enumerator = [accountArray objectEnumerator];
489     AIAccount           *account = nil;
490         
491         //XXX - Temporary Upgrade code for account internalObjectIDs stored as NSNumbers 0.7x -> 0.8 -ai
492         if(![objectID isKindOfClass:[NSString class]]){
493                 if([objectID isKindOfClass:[NSNumber class]]){
494                         objectID = [NSString stringWithFormat:@"%i",[(NSNumber *)objectID intValue]];
495                 }else{
496                         objectID = nil; //Unrecognizable, ignore
497                 }
498         }
499     
500     while(objectID && (account = [enumerator nextObject])){
501         if([objectID isEqualToString:[account internalObjectID]]) break;
502     }
503     
504     return(account);
507 //Searches the account list for accounts with the specified service ID
508 - (NSArray *)accountsWithService:(AIService *)service
510         NSMutableArray  *array = [NSMutableArray array];
511     NSEnumerator        *enumerator = [accountArray objectEnumerator];
512     AIAccount           *account;
513     
514     while((account = [enumerator nextObject])){
515                 if([account service] == service) [array addObject:account];
516     }
517     
518     return(array);
521 //Searches the account list for accounts with the specified service's serviceClass
522 //This could use other methods but that would require alloc'ing significantly more NSArrays, so we consolidate
523 //for efficiency.
524 - (NSArray *)accountsWithServiceClassOfService:(AIService *)service
526         return([self accountsWithServiceClass:[service serviceClass]]);
529 - (NSArray *)accountsWithServiceClass:(NSString *)serviceClass
531         NSMutableArray  *accountsArray = [NSMutableArray array];
532         NSEnumerator    *enumerator = [availableServiceDict objectEnumerator];
533     AIService           *aService;
534         
535         //Enumerate all available services
536         while(aService = [enumerator nextObject]){
537                 //Find matching serviceClasses
538                 if([[aService serviceClass] isEqualToString:serviceClass]){
539                         
540                         //Find matching accounts
541                         NSEnumerator    *enumerator = [accountArray objectEnumerator];
542                         AIAccount               *account;
543                         
544                         while((account = [enumerator nextObject])){
545                                 if ([account service] == aService){
546                                         [accountsArray addObject:account];
547                                 }
548                         }
549                 }
550         }
551         
552         return(accountsArray);
555 - (AIAccount *)firstAccountWithService:(AIService *)service
557     NSEnumerator        *enumerator = [accountArray objectEnumerator];
558     AIAccount           *account;
559     
560     while((account = [enumerator nextObject])){
561                 if([account service] == service) break;
562     }
563     
564     return(account);
567 //Returns a new default account
568 - (AIAccount *)defaultAccount
570         return([self createAccountWithService:[self serviceWithUniqueID:@"libgaim-oscar-AIM"] UID:@"" internalObjectID:nil]);
573 - (BOOL)anOnlineAccountCanCreateGroupChats
575         NSEnumerator    *enumerator;
576         AIAccount               *account;
577         BOOL                    anOnlineAccountCanCreateGroupChats;
578         
579         anOnlineAccountCanCreateGroupChats = NO;
580         
581     enumerator = [accountArray objectEnumerator];
582     while(account = [enumerator nextObject]){   
583                 if ([account online] && [[account service] canCreateGroupChats]){
584                         anOnlineAccountCanCreateGroupChats = YES;
585                         break;
586                 }
587         }
588         
589         return(anOnlineAccountCanCreateGroupChats);
592 //Account Editing ------------------------------------------------------------------------------------------------------
593 #pragma mark Account Editing
594 //Create a new default account
595 - (AIAccount *)newAccountAtIndex:(int)index
597         if(index == -1) index = [accountArray count];
598         
599     NSParameterAssert(accountArray != nil);
600     NSParameterAssert(index >= 0 && index <= [accountArray count]);
601     
602     AIAccount   *newAccount = [self defaultAccount];
603     
604         [self insertAccount:newAccount atIndex:index save:YES];
605                 
606     return(newAccount);
609 - (AIAccount *)newAccountAtIndex:(int)index forService:(AIService *)service
611         if(index == -1) index = [accountArray count];
613     NSParameterAssert(accountArray != nil);
614     NSParameterAssert(index >= 0 && index <= [accountArray count]);
616         AIAccount *newAccount;
617         
618         if(service){
619                 newAccount = [self createAccountWithService:service     UID:@"" internalObjectID:nil];
620         }else{
621                 newAccount = [self defaultAccount];
622         }
623         
624         [self insertAccount:newAccount atIndex:index save:YES];
625         
626     return(newAccount);
629 //Insert an account
630 - (void)insertAccount:(AIAccount *)inAccount atIndex:(int)index save:(BOOL)shouldSave
631 {    
632         if(index == -1) index = [accountArray count];
634     NSParameterAssert(inAccount != nil);
635     NSParameterAssert(accountArray != nil);
636     NSParameterAssert(index >= 0 && index <= [accountArray count]);
637     
638     //Insert the account
639         if ([accountArray count]){
640                 [accountArray insertObject:inAccount atIndex:index];
641         }else{
642                 [accountArray addObject:inAccount];
643         }
644         
645         if (shouldSave){
646                 [self saveAccounts];
647         }
650 //Delete an account
651 - (void)deleteAccount:(AIAccount *)inAccount save:(BOOL)shouldSave
653     NSParameterAssert(inAccount != nil);
654     NSParameterAssert(accountArray != nil);
656         //Don't let the account dealloc until we have a chance to notify everyone that it's gone
657         [inAccount retain]; 
659         //Let the account take any action it wants before being deleted, such as disconnecting
660         [inAccount willBeDeleted];
661         
662         //Remove from our array
663         [accountArray removeObject:inAccount];
664         
665         //Clean up the keychain -- forget the stored password
666         [self forgetPasswordForAccount:inAccount];
667         
668         //Save if appropriate
669         if (shouldSave){
670                 [self saveAccounts];
671         }
672         
673         //Cleanup
674         [inAccount release];
677 //Re-order an account on the list
678 - (int)moveAccount:(AIAccount *)account toIndex:(int)destIndex
680     int sourceIndex = [accountArray indexOfObject:account];
681     
682     //Remove the account
683     [account retain];
684     [accountArray removeObject:account];
685     
686     //Re-insert the account
687     if(destIndex > sourceIndex){
688         destIndex -= 1;
689     }
690     [accountArray insertObject:account atIndex:destIndex];
691     [account release];
692     
693     [self saveAccounts];
694     
695     return(destIndex);
698 //The account list changed.
699 - (void)accountListChanged:(NSNotification *)notification
701         //Clear our cached active service types
702         [_cachedActiveServices release]; _cachedActiveServices = nil;
704         // Perform a full rebuild rather than trying to figure out what is different.
705         [self rebuildAllAccountMenuItems];
708 //Preferred Source Accounts --------------------------------------------------------------------------------------------
709 #pragma mark Preferred Source Accounts
710 //Returns the preferred choice for sending content to the passed list object
711 //When presenting the user with a list of accounts, this should be the one selected by default
712 - (AIAccount *)preferredAccountForSendingContentType:(NSString *)inType toContact:(AIListContact *)inContact 
714         return ([self preferredAccountForSendingContentType:inType toContact:inContact includeOffline:NO]);
717 - (AIAccount *)preferredAccountForSendingContentType:(NSString *)inType toContact:(AIListContact *)inContact includeOffline:(BOOL)includeOffline
719         AIAccount               *account;
720         
721         //If passed a contact, we have a few better ways to determine the account than just using the first
722     if(inContact){
723                 //If we've messaged this object previously, and the account we used to message it is online, return that account
724         NSString *accountID = [inContact preferenceForKey:KEY_PREFERRED_SOURCE_ACCOUNT
725                                                                                                         group:PREF_GROUP_PREFERRED_ACCOUNTS];
726         if(accountID && (account = [self accountWithInternalObjectID:accountID])){
727             if([account availableForSendingContentType:inType toContact:inContact]){
728                 return(account);
729             }
730         }
731                 
732                 //If inObject is an AIListContact return the account the object is on
733                 if(account = [inContact account]){
734                         if([account availableForSendingContentType:inType toContact:inContact]){
735                                 return(account);
736                         }
737                 }
738                 
739                 //Return the last account used to message someone on this service
740                 NSString        *lastAccountID = [lastAccountIDToSendContent objectForKey:[[inContact service] serviceID]];
741                 if(lastAccountID && (account = [self accountWithInternalObjectID:lastAccountID])){
742                         if([account availableForSendingContentType:inType toContact:nil] || includeOffline){
743                                 return(account);
744                         }
745                 }
746                 
747                 if (includeOffline){
748                         //If inObject is an AIListContact return the account the object is on even if the account is offline
749                         if(account = [inContact account]){
750                                 return(account);
751                         }
752                 }
753         }
754         
755         //If the previous attempts failed, or we weren't passed a contact, use the first appropraite account
756         return([self firstAccountAvailableForSendingContentType:inType
757                                                                                                   toContact:inContact
758                                                                                          includeOffline:includeOffline]);
761 - (AIAccount *)firstAccountAvailableForSendingContentType:(NSString *)inType toContact:(AIListContact *)inContact includeOffline:(BOOL)includeOffline
763         AIAccount               *account;
764         NSEnumerator    *enumerator;
765         
766     if(inContact){
767                 //First available account in our list of the correct service type
768                 enumerator = [accountArray objectEnumerator];
769                 while(account = [enumerator nextObject]){
770                         if([inContact service] == [account service] &&
771                            ([account availableForSendingContentType:inType toContact:nil] || includeOffline)){
772                                 return(account);
773                         }
774                 }
775                 
776                 //First available account in our list of a compatible service type
777                 enumerator = [accountArray objectEnumerator];
778                 while(account = [enumerator nextObject]){
779                         if([[inContact serviceClass] isEqualToString:[account serviceClass]] &&
780                            ([account availableForSendingContentType:inType toContact:nil] || includeOffline)){
781                                 return(account);
782                         }
783                 }
784         }else{
785                 //First available account in our list
786                 enumerator = [accountArray objectEnumerator];
787                 while(account = [enumerator nextObject]){
788                         if([account availableForSendingContentType:inType toContact:nil] || includeOffline){
789                                 return(account);
790                         }
791                 }
792         }
793         
794         
795         //Can't find anything
796         return(nil);
799 //Returns a menu of all accounts returned by menuItemsForAccountsWithTarget
800 - (NSMenu *)menuOfAccountsWithTarget:(id)target includeOffline:(BOOL)includeOffline
802         return ([self menuOfAccountsWithTarget:target includeOffline:includeOffline onlyIfCreatingGroupChatIsSupported:NO]);
805 //Returns a menu of all accounts (optionally, which can create a group chat) returned by menuItemsForAccountsWithTarget
806 - (NSMenu *)menuOfAccountsWithTarget:(id)target includeOffline:(BOOL)includeOffline onlyIfCreatingGroupChatIsSupported:(BOOL)groupChatCreator
808         NSMenu                  *menu = [[NSMenu alloc] init];
809         NSEnumerator    *enumerator;
810         NSMenuItem              *menuItem;
811         
812         enumerator = [[self menuItemsForAccountsWithTarget:target includeOffline:includeOffline] objectEnumerator];
813         while(menuItem = [enumerator nextObject]){
814                 if (!groupChatCreator || [[[menuItem representedObject] service] canCreateGroupChats]){
815                         [menu addItem:menuItem];
816                 }
817         }
819         if(!target) [menu setAutoenablesItems:NO];
821         return([menu autorelease]);
824 //Returns an array containing menu items for all accounts. 
825 //- Accounts not available for sending content are disabled.
826 //- Selector called on account selection is selectAccount:
827 //- The menu item's represented objects are the AIAccounts they represent
828 - (NSArray *)menuItemsForAccountsWithTarget:(id)target includeOffline:(BOOL)includeOffline
830         NSMutableArray  *menuItems = [[NSMutableArray alloc] init];
831         NSEnumerator    *enumerator;
832         AIAccount               *account;
833         
834         //We don't show service types unless the user is using multiple services
835         BOOL    multipleServices = ([[self activeServicesIncludingCompatibleServices:NO] count] > 1);
836         
837     //Insert a menu item for each available account
838     enumerator = [accountArray objectEnumerator];
839     while(account = [enumerator nextObject]){
840                 BOOL available = [[adium contentController] availableForSendingContentType:CONTENT_MESSAGE_TYPE
841                                                                                                                                                  toContact:nil 
842                                                                                                                                                  onAccount:account];
843                 
844                 if(available || includeOffline){
845                         NSMenuItem      *menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:(multipleServices ?
846                                                                                                                                                                                                   [NSString stringWithFormat:@"%@ (%@)", [account formattedUID], [[account service] shortDescription]] :
847                                                                                                                                                                                                   [account formattedUID])
848                                                                                                                                                                                   target:target
849                                                                                                                                                                                   action:@selector(selectAccount:)
850                                                                                                                                                                    keyEquivalent:@""];
851                         [menuItem setRepresentedObject:account];
852                         [menuItem setImage:[AIServiceIcons serviceIconForObject:account
853                                                                                                                            type:AIServiceIconSmall
854                                                                                                                   direction:AIIconNormal]];
855                         [menuItem setEnabled:available];
856                         
857                         [menuItems addObject:menuItem];
858                         [menuItem release];
859                 }
860     }
861         
862         return([menuItems autorelease]);
865 //Returns a menu of all accounts available for sending content to a list object
866 //- Preferred choices are placed at the top of the menu.
867 //- Selector called on account selection is selectAccount:
868 //- The menu item's represented objects are the AIAccounts they represent
869 - (NSMenu *)menuOfAccountsForSendingContentType:(NSString *)inType
870                                                                    toListObject:(AIListObject *)inObject
871                                                                          withTarget:(id)target
872                                                                  includeOffline:(BOOL)includeOffline
874         NSMenu          *menu;
875         NSArray         *topAccounts, *bottomAccounts;
876                 
877         //Get the list of accounts for each section of our menu
878         topAccounts = [self _accountsForSendingContentType:CONTENT_MESSAGE_TYPE
879                                                                                   toListObject:inObject
880                                                                                          preferred:YES
881                                                                                 includeOffline:includeOffline];
882         bottomAccounts = [self _accountsForSendingContentType:CONTENT_MESSAGE_TYPE
883                                                                                          toListObject:inObject
884                                                                                                 preferred:NO
885                                                                                    includeOffline:includeOffline];
887         //Build the menu
888         menu = [[NSMenu alloc] init];
889         [menu setAutoenablesItems:NO];
890         if([topAccounts count]) [self _addMenuItemsToMenu:menu withTarget:target forAccounts:topAccounts];
891         if([topAccounts count] &&  [bottomAccounts count]) [menu addItem:[NSMenuItem separatorItem]];
892         if([bottomAccounts count]) [self _addMenuItemsToMenu:menu withTarget:target forAccounts:bottomAccounts];
893         
894         return([menu autorelease]);
897 - (void)_addMenuItemsToMenu:(NSMenu *)menu withTarget:(id)target forAccounts:(NSArray *)accounts
899         NSEnumerator    *enumerator = [accounts objectEnumerator];
900         AIAccount               *account;
901         
902         while(account = [enumerator nextObject]){
903                 NSMenuItem      *menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:[account formattedUID]
904                                                                                                                                                                          target:target
905                                                                                                                                                                          action:@selector(selectAccount:)
906                                                                                                                                                           keyEquivalent:@""];
907                 [menuItem setRepresentedObject:account];
908                 [menuItem setImage:[AIServiceIcons serviceIconForObject:account
909                                                                                                                    type:AIServiceIconSmall
910                                                                                                           direction:AIIconNormal]];
911                 [menu addItem:menuItem];
912                 [menuItem release];
913         }
916 - (NSArray *)_accountsForSendingContentType:(NSString *)inType toListObject:(AIListObject *)inObject preferred:(BOOL)inPreferred includeOffline:(BOOL)includeOffline
918         NSMutableArray  *sourceAccounts = [NSMutableArray array];
919         NSEnumerator    *enumerator = [[[adium accountController] accountArray] objectEnumerator];
920         AIAccount               *account;
921         
922         while(account = [enumerator nextObject]){
923                 if((!inObject && !inPreferred) || 
924                    ([self _account:account canSendContentType:inType toListObject:inObject preferred:inPreferred includeOffline:includeOffline])){
925                         
926                         [sourceAccounts addObject:account];
927                 }
928         }
929         
930         return(sourceAccounts);
933 - (BOOL)_account:(AIAccount *)account canSendContentType:(NSString *)inType toListObject:(AIListObject *)inObject preferred:(BOOL)inPreferred includeOffline:(BOOL)includeOffline
935         BOOL                    canSend = NO;
936         
937         if ([inObject isKindOfClass:[AIMetaContact class]]){            
938                 NSEnumerator    *enumerator = [[(AIMetaContact *)inObject listContacts] objectEnumerator];
939                 AIListObject    *containedObject;
940                 
941                 //canSend is YES if any of the contained contacts of the meta contact return YES
942                 while (containedObject = [enumerator nextObject]){
943                         if ([self _account:account canSendContentType:inType toListObject:containedObject preferred:inPreferred includeOffline:includeOffline]){
944                                 
945                                 canSend = YES;
946                                 break;
947                         }
948                 }
950         }else{
951                 if ([[inObject serviceClass] isEqualToString:[account serviceClass]]){
952                         BOOL                    knowsObject = NO;
953                         BOOL                    canFindObject = NO;
954                         AIListContact   *contactForAccount = [[adium contactController] existingContactWithService:[inObject service]
955                                                                                                                                                                                            account:account
956                                                                                                                                                                                                    UID:[inObject UID]];
957                         
958                         //Does the account know this object?
959                         if(contactForAccount){
960                                 knowsObject = [account availableForSendingContentType:CONTENT_MESSAGE_TYPE
961                                                                                                                         toContact:contactForAccount];
962                         }
963                         
964                         //Could the account find this object?
965                         canFindObject = [account availableForSendingContentType:CONTENT_MESSAGE_TYPE toContact:nil];
966                         
967                         if((inPreferred && knowsObject) ||                                              //Online and can see the object
968                            (!inPreferred && !knowsObject && canFindObject) ||   //Online and may be able to see the object
969                            (!inPreferred && !knowsObject && includeOffline)){   //Offline, but may be able to see the object if online
970                                 canSend = YES;
971                         }
972                 }
973         }
974         
975         return(canSend);
978 //Watch outgoing content, remembering the user's choice of source account
979 - (void)didSendContent:(NSNotification *)notification
981         NSDictionary    *userInfo = [notification userInfo];
982     AIChat                      *chat = [userInfo objectForKey:@"AIChat"];
983     AIListObject        *destObject = [chat listObject];
984     
985     if(chat && destObject){
986         AIContentObject *contentObject = [userInfo objectForKey:@"AIContentObject"];
987         AIAccount               *sourceAccount = (AIAccount *)[contentObject source];
988         
989         [destObject setPreference:[sourceAccount internalObjectID]
990                            forKey:KEY_PREFERRED_SOURCE_ACCOUNT
991                             group:PREF_GROUP_PREFERRED_ACCOUNTS];
992         
993         [lastAccountIDToSendContent setObject:[sourceAccount internalObjectID] forKey:[[destObject service] serviceID]];
994     }
998 //Connection convenience methods ---------------------------------------------------------------------------------------
999 #pragma mark Connection Convenience Methods
1000 //Connects all the accounts
1001 - (void)connectAllAccounts
1003     NSEnumerator                *enumerator;
1004     AIAccount                   *account;
1005     
1006     enumerator = [accountArray objectEnumerator];
1007     while((account = [enumerator nextObject])){
1008         if([[account supportedPropertyKeys] containsObject:@"Online"]){
1009             [account setPreference:[NSNumber numberWithBool:YES] forKey:@"Online" group:GROUP_ACCOUNT_STATUS];
1010         }
1011     }
1014 //Disconnects all the accounts
1015 - (void)disconnectAllAccounts
1017     NSEnumerator                *enumerator;
1018     AIAccount                   *account;
1020     enumerator = [accountArray objectEnumerator];
1021     while((account = [enumerator nextObject])){
1022         if([[account supportedPropertyKeys] containsObject:@"Online"] &&
1023                    [[account preferenceForKey:@"Online" group:GROUP_ACCOUNT_STATUS] boolValue]){
1024             [account setPreference:nil forKey:@"Online" group:GROUP_ACCOUNT_STATUS];
1025         }
1026     }
1029 - (BOOL)oneOrMoreConnectedAccounts
1031         NSEnumerator            *enumerator;
1032     AIAccount                   *account;
1034     enumerator = [accountArray objectEnumerator];
1035     while((account = [enumerator nextObject])){
1036         if([account online]){
1037                         return YES;
1038         }
1039     }   
1040         
1041         return NO;
1044 - (BOOL)oneOrMoreConnectedOrConnectingAccounts
1046         NSEnumerator            *enumerator;
1047     AIAccount                   *account;
1048         
1049     enumerator = [accountArray objectEnumerator];
1050     while((account = [enumerator nextObject])){
1051         if([account online] || [account integerStatusObjectForKey:@"Connecting"]){
1052                         return YES;
1053         }
1054     }   
1056         return NO;      
1059 //Password Storage -----------------------------------------------------------------------------------------------------
1060 #pragma mark Password Storage
1062 - (NSString *)_accountNameForAccount:(AIAccount *)inAccount{
1063         return([NSString stringWithFormat:@"%@.%@",[[inAccount service] serviceID],[inAccount internalObjectID]]);
1065 - (NSString *)_passKeyForAccount:(AIAccount *)inAccount{
1066         if([[[adium loginController] userArray] count] > 1){
1067                 return([NSString stringWithFormat:@"Adium.%@.%@",[[adium loginController] currentUser],[self _accountNameForAccount:inAccount]]);
1068         }else{
1069                 return([NSString stringWithFormat:@"Adium.%@",[self _accountNameForAccount:inAccount]]);
1070         }
1072 - (NSString *)_accountNameForProxyServer:(NSString *)proxyServer userName:(NSString *)userName{
1073         return([NSString stringWithFormat:@"%@.%@",proxyServer,userName]);
1075 - (NSString *)_passKeyForProxyServer:(NSString *)proxyServer{
1076         if([[[adium loginController] userArray] count] > 1){
1077                 return([NSString stringWithFormat:@"Adium.%@.%@",[[adium loginController] currentUser],proxyServer]);
1078         }else{
1079                 return([NSString stringWithFormat:@"Adium.%@",proxyServer]);    
1080         }
1084 //Save an account password
1085 - (void)setPassword:(NSString *)inPassword forAccount:(AIAccount *)inAccount
1087     if(inPassword){
1088         [AIKeychain putPasswordInKeychainForService:[self _passKeyForAccount:inAccount]
1089                                             account:[self _accountNameForAccount:inAccount]
1090                                                                                    password:inPassword];
1091     }
1094 //Fetches a saved account password (returns nil if no password is saved)
1095 - (NSString *)passwordForAccount:(AIAccount *)inAccount
1097     NSString    *password = [AIKeychain getPasswordFromKeychainForService:[self _passKeyForAccount:inAccount]
1098                                                                   account:[self _accountNameForAccount:inAccount]];
1099     return(password);
1102 //Fetches a saved account password (Prompts the user to enter if no password is saved)
1103 - (void)passwordForAccount:(AIAccount *)inAccount notifyingTarget:(id)inTarget selector:(SEL)inSelector context:(id)inContext
1105     NSString    *password;
1106     
1107     //check the keychain for this password
1108     password = [AIKeychain getPasswordFromKeychainForService:[self _passKeyForAccount:inAccount]
1109                                                      account:[self _accountNameForAccount:inAccount]];
1110     
1111     if(password && [password length] != 0){
1112         //Invoke the target right away
1113         [inTarget performSelector:inSelector withObject:password withObject:inContext afterDelay:0.0001];
1114     }else{
1115         //Prompt the user for their password
1116         [ESAccountPasswordPromptController showPasswordPromptForAccount:inAccount
1117                                                                                                                 notifyingTarget:inTarget
1118                                                                                                                            selector:inSelector
1119                                                                                                                                 context:inContext];
1120     }
1123 //Forget a saved password
1124 - (void)forgetPasswordForAccount:(AIAccount *)inAccount
1126     [AIKeychain removePasswordFromKeychainForService:[self _passKeyForAccount:inAccount]
1127                                                                                          account:[self _accountNameForAccount:inAccount]];
1130 - (NSString *)passwordForProxyServer:(NSString *)server userName:(NSString *)userName
1132         NSString *password = [AIKeychain getPasswordFromKeychainForService:[self _passKeyForProxyServer:server]
1133                                                                                                                            account:[self _accountNameForProxyServer:server 
1134                                                                                                                                                                                            userName:userName]];
1135         return password;
1138 - (void)passwordForProxyServer:(NSString *)server userName:(NSString *)userName notifyingTarget:(id)inTarget selector:(SEL)inSelector context:(id)inContext
1140         NSString        *password;
1141     
1142     //check the keychain for this password
1143     password = [AIKeychain getPasswordFromKeychainForService:[self _passKeyForProxyServer:server]
1144                                                      account:[self _accountNameForProxyServer:server userName:userName]];
1146     if(password && [password length] != 0){
1147         //Invoke the target right away
1148         [inTarget performSelector:inSelector withObject:password withObject:inContext afterDelay:0.0001];    
1149     }else{
1150         //Prompt the user for their password
1151         [ESProxyPasswordPromptController showPasswordPromptForProxyServer:server
1152                                                                                                                                  userName:userName
1153                                                                                                                   notifyingTarget:inTarget
1154                                                                                                                                  selector:inSelector
1155                                                                                                                                   context:inContext];
1156     }
1158 //Save a proxy server password
1159 - (void)setPassword:(NSString *)inPassword forProxyServer:(NSString *)server userName:(NSString *)userName
1161     if(inPassword){
1162         [AIKeychain putPasswordInKeychainForService:[self _passKeyForProxyServer:server]
1163                                             account:[self _accountNameForProxyServer:server 
1164                                                                                                                                                         userName:userName] password:inPassword];
1165     }else{
1166                 [AIKeychain removePasswordFromKeychainForService:[self _passKeyForProxyServer:server]
1167                                                                                                  account:[self _accountNameForProxyServer:server userName:userName]];
1168         }
1171 - (void)_upgradePasswords
1173         AIAccount               *account;
1174         NSEnumerator    *enumerator = [accountArray objectEnumerator];
1175         
1176         NSRunInformationalAlertPanel(@"Adium Version Upgrade", @"This version of Adium fixes a common crash related to secure storage of your instant messaging passwords.  When you press OK below, Adium will automatically update any stored passwords to the new, more stable system.\n\nThis process will only occur once and will take a moment; you may be prompted to allow access for one or more passwords.\n\nIf Adium crashes during this upgrade, simply relaunch Adium; the process will not occur again.",nil,nil,nil);
1178         while(account = [enumerator nextObject]){
1179                 NSString        *passKey = [self _passKeyForAccount:account];
1180                 NSString        *accountName = [self _accountNameForAccount:account];
1181                 
1182                 //Get from old
1183                 NSString        *password = [AIKeychainOld getPasswordFromKeychainForService:passKey
1184                                                                                                                                                  account:accountName];
1185                 
1186                 //Store in new
1187                 if(password){
1188                         [AIKeychain putPasswordInKeychainForService:passKey
1189                                                                                                 account:accountName
1190                                                                                            password:password];
1191                 }
1192         }
1195 // Account Connection menus -----------------------------------------------
1196 #pragma mark Account Connection menus
1198 #define MENU_IMAGE_FRACTION_ONLINE              1.00
1199 #define MENU_IMAGE_FRACTION_CONNECTING          0.60
1200 #define MENU_IMAGE_FRACTION_OFFLINE             0.30
1202 //In AdiumFramework in 0.9
1203 #define ACCOUNT_CONNECT_MENU_TITLE                      AILocalizedStringFromTable(@"Connect: %@", @"AdiumFramework", "Connect account prefix")
1204 #define ACCOUNT_DISCONNECT_MENU_TITLE           AILocalizedStringFromTable(@"Disconnect: %@", @"AdiumFramework", "Disconnect account prefix")
1205 #define ACCOUNT_CONNECTING_MENU_TITLE           AILocalizedStringFromTable(@"Cancel: %@", @"AdiumFramework", "Cancel current account activity prefix")
1207 #define ACCOUNT_DISCONNECTING_MENU_TITLE        ACCOUNT_CONNECTING_MENU_TITLE
1209 - (void)_prepareAccountMenus
1211         [[adium statusController] registerStateMenuPlugin:self];
1214 - (void)registerAccountMenuPlugin:(id<AccountMenuPlugin>)accountMenuPlugin
1216         NSNumber        *identifier = [NSNumber numberWithInt:[accountMenuPlugin hash]];
1217         
1218         [accountMenuItemArraysDict setObject:[NSMutableArray array]
1219                                                                   forKey:identifier];
1220         [accountMenuPluginsDict setObject:accountMenuPlugin
1221                                                                 forKey:identifier];
1222         
1223         [self _addAccountMenuItemsForPlugin:accountMenuPlugin];
1224         
1225         //Immediately add the status submenu items by asking the statusController to rebuilt our state menus.
1226         [[adium statusController] rebuildAllStateMenusForPlugin:self];
1228 - (void)unregisterAccountMenuPlugin:(id<AccountMenuPlugin>)accountMenuPlugin
1230         NSNumber        *identifier = [NSNumber numberWithInt:[accountMenuPlugin hash]];
1232         [self _removeAccountMenuItemsForPlugin:accountMenuPlugin];      
1233         [accountMenuItemArraysDict removeObjectForKey:identifier];
1234         [accountMenuPluginsDict removeObjectForKey:identifier];
1237 //Togle the connection of the selected account (called by the connect/disconnnect menu item)
1238 //MUST be called by a menu item with an account as its represented object!
1239 - (IBAction)toggleConnection:(id)sender
1241     AIAccount   *account = [sender representedObject];
1242     BOOL        online = [[account statusObjectForKey:@"Online"] boolValue];
1243         BOOL            connecting = [[account statusObjectForKey:@"Connecting"] boolValue];
1244         
1245         // !(online || connecting) means:
1246         //              If neither online nor connecting, YES - initiate a connection
1247         //              If either currently online or currently in the process of connecting, NO - disconnect
1248         //
1249         // Setting the preference is enough to trigger the cascade which will lead to the account taking action
1250         [account setPreference:[NSNumber numberWithBool:!(online || connecting)] 
1251                                         forKey:@"Online"
1252                                          group:GROUP_ACCOUNT_STATUS];
1255 //Create a new menu item for each account, updating it immediately to the proper current state.
1256 //Store these menu items in a mutableArray associated with the plugin.
1257 //Then, inform the plugin of the existence of the menu items so it can add them to a menu.
1258 - (void)_addAccountMenuItemsForPlugin:(id<AccountMenuPlugin>)accountMenuPlugin
1260         NSNumber                *identifier = [NSNumber numberWithInt:[accountMenuPlugin hash]];
1261         NSMutableArray  *menuItemArray = [accountMenuItemArraysDict objectForKey:identifier];
1262         
1263         NSEnumerator    *enumerator;
1264     AIAccount           *account;
1265     NSMenuItem          *menuItem;
1266         
1267     //Create a menuitem for each account
1268     enumerator = [[self accountArray] objectEnumerator];
1269     while((account = [enumerator nextObject])){
1270                 
1271                 if([[account supportedPropertyKeys] containsObject:@"Online"]){
1272                         //Create the account's menu item (the title will be set by_updateMenuItem:forAccount:
1273                         menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:@""
1274                                                                                                                                                         target:self
1275                                                                                                                                                         action:@selector(toggleConnection:)
1276                                                                                                                                          keyEquivalent:@""];
1277                         [menuItem setRepresentedObject:account];
1278                         [menuItemArray addObject:menuItem];
1279                         [menuItem release];
1280                         
1281                         [self _updateMenuItem:menuItem forAccount:account forPlugin:accountMenuPlugin];
1282                 }
1283     }
1284         
1285         //Now that we are done creating the menu items, tell the plugin about them
1286         [accountMenuPlugin addAccountMenuItems:menuItemArray];
1289 //Retrieve the menu items for a given plugin.  Tell it we want to remove them, which should trigger
1290 //removal from whatever menu the program is using them in.  Then, remove them from our tracking array.
1291 - (void)_removeAccountMenuItemsForPlugin:(id<AccountMenuPlugin>)accountMenuPlugin
1293         NSNumber                *identifier = [NSNumber numberWithInt:[accountMenuPlugin hash]];
1294         NSMutableArray  *menuItemArray = [accountMenuItemArraysDict objectForKey:identifier];
1296         //Inform the plugin that we are removing the items in the this array 
1297         [accountMenuPlugin removeAccountMenuItems:menuItemArray];
1298         
1299         //Now clear the array
1300         [menuItemArray removeAllObjects];
1303 // Connected / Connecting / Disconnecting / Disconnected update
1304 - (void)_updateMenuItem:(NSMenuItem *)menuItem forAccount:(AIAccount *)account forPlugin:(id<AccountMenuPlugin>)accountMenuPlugin
1306         if(menuItem){
1307                 NSString        *accountTitle = [account formattedUID];
1308                 NSString        *titleFormat = nil;
1310                 //Default to <New Account> if a name is not available
1311                 if(!accountTitle || ![accountTitle length]) accountTitle = NEW_ACCOUNT_DISPLAY_TEXT;
1313                 //Get the status and service icons for this account so we can add them to the menu
1314                 NSImage *statusIcon = [AIStatusIcons statusIconForListObject:account type:AIStatusIconList direction:AIIconNormal];
1315                 NSImage *serviceIcon = [AIServiceIcons serviceIconForObject:account type:AIServiceIconSmall direction:AIIconNormal];
1317                 //Show connecting and disconnecting in the title as appropriate
1318                 if([[account statusObjectForKey:@"Connecting"] boolValue]){
1319                         titleFormat = ACCOUNT_CONNECTING_MENU_TITLE;
1321                 }else if([[account statusObjectForKey:@"Disconnecting"] boolValue]){
1322                         titleFormat = ACCOUNT_DISCONNECTING_MENU_TITLE;
1323                         
1324                 }else if(![accountMenuPlugin showStatusSubmenu]){
1325                         //Only show Connect or Disconnect before the name if we aren't showing the status menu
1326                         if([account online]){
1327                                 titleFormat = ACCOUNT_DISCONNECT_MENU_TITLE;
1328                         }else{
1329                                 titleFormat = ACCOUNT_CONNECT_MENU_TITLE;                               
1330                         }
1331                 }
1333                 //Composite the status icon and service icon together
1334                 //We're only allowed one icon per menu item, so we need to combine our separate icons into a single image
1335                 NSSize  statusSize = [statusIcon size];
1336                 NSSize  serviceSize = [serviceIcon size];
1337                 NSSize  compositeSize = NSMakeSize(statusSize.width + serviceSize.width + 1,
1338                                                                                    statusSize.height > serviceSize.height ? statusSize.height : serviceSize.height);
1339                 NSRect  compositeRect = NSMakeRect(0, 0, compositeSize.width, compositeSize.height);
1341                 //Render the composite image
1342                 NSImage *composite = [[NSImage alloc] initWithSize:compositeSize];
1343                 [composite lockFocus];
1344                 [statusIcon drawInRect:compositeRect atSize:[statusIcon size] position:IMAGE_POSITION_LEFT fraction:1.0];
1345                 [serviceIcon drawInRect:compositeRect atSize:[serviceIcon size] position:IMAGE_POSITION_RIGHT fraction:1.0];
1346                 [composite unlockFocus];
1348                 //Update the menu item
1349                 [[menuItem menu] setMenuChangedMessagesEnabled:NO];
1350                 [menuItem setTitle:(titleFormat ? [NSString stringWithFormat:titleFormat,accountTitle] : accountTitle)];
1351                 [menuItem setImage:composite];          
1352                 [menuItem setEnabled:/*(![[account statusObjectForKey:@"Connecting"] boolValue] &&
1353                                                           ![[account statusObjectForKey:@"Disconnecting"] boolValue])*/YES];
1354                 [[menuItem menu] setMenuChangedMessagesEnabled:YES];
1356                 //Cleanup
1357                 [composite release];
1358         }
1361 //Remove all current account menu items for all account menu item plugins, then create a new, current set.
1362 - (void)rebuildAllAccountMenuItems
1364         NSEnumerator                    *enumerator = [accountMenuPluginsDict objectEnumerator];
1365         id<AccountMenuPlugin>   accountMenuPlugin;
1366         while (accountMenuPlugin = [enumerator nextObject]) {
1367                 [self _removeAccountMenuItemsForPlugin:accountMenuPlugin];
1368                 [self _addAccountMenuItemsForPlugin:accountMenuPlugin];
1369         }
1370         
1371         //Now recreate the status submenus
1372         [[adium statusController] rebuildAllStateMenusForPlugin:self];
1375 //Account status changed, update our menu
1376 - (NSSet *)updateListObject:(AIListObject *)inObject keys:(NSSet *)inModifiedKeys silent:(BOOL)silent
1378     if([inObject isKindOfClass:[AIAccount class]] && 
1379                 ([inModifiedKeys containsObject:@"Online"] ||
1380                  [inModifiedKeys containsObject:@"Connecting"] ||
1381                  [inModifiedKeys containsObject:@"Disconnecting"] ||
1382                  [inModifiedKeys containsObject:@"IdleSince"] ||
1383                  [inModifiedKeys containsObject:@"StatusState"])){
1384                 
1385                 //Enumerate all arrays of menu items (for all plugins)
1386                 NSEnumerator    *enumerator;
1387                 NSArray                 *accountMenuItemArray;
1388                 NSMenu                  *actionsSubmenu = nil;          
1389                 NSNumber                *identifier;
1390                 BOOL                    onlineChanged = [inModifiedKeys containsObject:@"Online"];
1392                 if(onlineChanged){
1393                         //If we are online, find the actions submenu if appropriate; if not, it remains nil to clear the submenu
1394                         if([inObject online]){
1395                                 actionsSubmenu = [self actionsMenuForAccount:(AIAccount *)inObject];
1396                         }
1397                 }
1398                 
1399                 enumerator = [accountMenuItemArraysDict keyEnumerator];
1400                 while(identifier = [enumerator nextObject]){
1401                         id<AccountMenuPlugin>   accountMenuPlugin;
1402                         NSMenuItem                              *menuItem;
1404                         accountMenuItemArray = [accountMenuItemArraysDict objectForKey:identifier];
1405                         accountMenuPlugin = [accountMenuPluginsDict objectForKey:identifier];
1406                         
1407                         //Find the menu item for this account in this array
1408                         menuItem = [self _menuItemForAccount:(AIAccount *)inObject
1409                                                                            fromArray:accountMenuItemArray];
1410                         //Update it
1411                         [self _updateMenuItem:menuItem forAccount:(AIAccount *)inObject forPlugin:accountMenuPlugin];
1413                         //Update the actions submenu if the online state changed and this is not a status-submenu-showing menuItem
1414                         if(onlineChanged && ![accountMenuPlugin showStatusSubmenu]){
1415                                 [menuItem setSubmenu:[[actionsSubmenu copy] autorelease]];
1416                         }
1417                 }
1418     }
1419         
1420     //We don't change any keys
1421     return(nil);
1424 //Given a target account and an array of menu items, find the menu item for that account
1425 - (NSMenuItem *)_menuItemForAccount:(AIAccount *)account fromArray:(NSArray *)accountMenuItemArray
1427         NSEnumerator    *enumerator;
1428         NSMenuItem              *menuItem;
1429     NSMenuItem          *targetMenuItem = nil;
1430         
1431         //Find the menu
1432         enumerator = [accountMenuItemArray objectEnumerator];
1433         while((menuItem = [enumerator nextObject])){    
1434                 if([menuItem representedObject] == account){
1435                         targetMenuItem = menuItem;
1436                         break;
1437                 }
1438         }
1439         
1440         return targetMenuItem;  
1443 - (NSMenu *)actionsMenuForAccount:(AIAccount *)inAccount
1445         NSArray *accountActionMenuItems = [inAccount accountActionMenuItems];
1446         NSMenu  *actionsSubmenu = nil;
1448         //Only proceed if we got at least one menu item
1449         if(accountActionMenuItems && [accountActionMenuItems count]){
1450                 NSEnumerator    *enumerator;
1451                 NSMenuItem              *menuItem;
1453                 actionsSubmenu = [[[NSMenu allocWithZone:[NSMenu zone]] init] autorelease];
1454                 [actionsSubmenu setTitle:@"Actions"]; //Not localized, just used internally
1456                 //Now add each item to the submenu
1457                 enumerator = [accountActionMenuItems objectEnumerator];
1458                 while(menuItem = [enumerator nextObject]){
1459                         [actionsSubmenu addItem:menuItem];
1460                 }
1461         }
1463         return actionsSubmenu;
1464 }       
1467  * @brief Add the passed state menu items; also take this opportunity to add account-specific items for online accounts
1469  * When given menu items, make a submenu out of them for each account with a represented object referring to that account.
1470  * We only add the status menu items in a submenu if more than one account exists, as with only one account it would be redundant.
1471  */
1472 - (void)addStateMenuItems:(NSArray *)menuItemArray
1474         NSArray                         *originalMenuItemArray;
1475         NSMutableDictionary     *temporaryMenuDict = [NSMutableDictionary dictionary];
1476         NSEnumerator            *enumerator;
1477         AIAccount                       *account;
1478         NSArray                         *accountMenuItemArray;
1479         int                                     accountArrayCount;
1480         
1481         accountArrayCount = [accountArray count];
1482         
1483         //Hold on to this array
1484         originalMenuItemArray = [menuItemArray copy];
1485         
1486         //Remove the menu items which we've been passed... we'll be creating our own based off of them.
1487         [[adium statusController] removeAllMenuItemsForPlugin:self];
1488         
1489         //Enumerate our accounts array
1490         enumerator = [accountArray objectEnumerator];
1491         while((account = [enumerator nextObject])){             
1492                 NSMenu                  *accountSubmenu = [[[NSMenu allocWithZone:[NSMenu zone]] init] autorelease];
1493                 [accountSubmenu setMenuChangedMessagesEnabled:NO];
1494                 
1495                 //Add status items if we have more than one account
1496                 if(accountArrayCount > 1){
1497                         NSEnumerator    *menuItemEnumerator = [originalMenuItemArray objectEnumerator];
1498                         NSMenuItem              *menuItem;
1499                         NSMenuItem              *accountMenuItem;
1500                         
1501                         //Enumerate all the menu items we were originally passed
1502                         while(menuItem = [menuItemEnumerator nextObject]){
1503                                 AIStatus                *status;
1504                                 NSDictionary    *newRepresentedObject;
1505                                 
1506                                 //Set the represented object to indicate both the right status and the right account
1507                                 if(status = [[menuItem representedObject] objectForKey:@"AIStatus"]){
1508                                         newRepresentedObject = [NSDictionary dictionaryWithObjectsAndKeys:
1509                                                 status, @"AIStatus",
1510                                                 account, @"AIAccount",
1511                                                 nil];
1512                                 }else{
1513                                         newRepresentedObject = [NSDictionary dictionaryWithObject:account
1514                                                                                                                                            forKey:@"AIAccount"];
1515                                 }
1516                                 
1517                                 accountMenuItem = [[menuItem copy] autorelease];
1518                                 [accountMenuItem setRepresentedObject:newRepresentedObject];
1519                                 
1520                                 //Add to our menu
1521                                 [accountSubmenu addItem:accountMenuItem];
1522                         }
1523                 }
1525                 if([accountSubmenu numberOfItems] > 0){
1526                         [temporaryMenuDict setObject:accountSubmenu
1527                                                                   forKey:[account internalObjectID]];
1528                         
1529                 }
1530                 
1531                 [accountSubmenu setMenuChangedMessagesEnabled:YES];
1532         }
1533         
1534         //Enumerate all arrays of menu items (for all plugins)
1535         NSNumber        *identifier;
1536         
1537         enumerator = [accountMenuItemArraysDict keyEnumerator];
1538         while(identifier = [enumerator nextObject]){
1539                 id<AccountMenuPlugin> accountMenuPlugin = [accountMenuPluginsDict objectForKey:identifier];
1540                 
1541                 if([accountMenuPlugin showStatusSubmenu]){
1542                         NSEnumerator    *accountMenuItemEnumerator;
1543                         NSMenuItem              *accountMenuItem;
1544                         
1545                         accountMenuItemArray = [accountMenuItemArraysDict objectForKey:identifier];
1546                         
1547                         //Enumerate each menu item in this array (the array corresponds to one plugin's menu items; each menu item
1548                         //will be for a distinct AIAccount).
1549                         accountMenuItemEnumerator = [accountMenuItemArray objectEnumerator];
1550                         while(accountMenuItem = [accountMenuItemEnumerator nextObject]){
1551                                 AIAccount       *account = [accountMenuItem representedObject];
1552                                 NSMenu          *generatedAccountSubmenu;
1553                                 
1554                                 if(generatedAccountSubmenu = [temporaryMenuDict objectForKey:[account internalObjectID]]){
1555                                         NSMenu          *accountSubmenu = [generatedAccountSubmenu copy];
1556                                         
1557                                         //Tell the status controller to update these items as necessary
1558                                         [[adium statusController] plugin:self didAddMenuItems:[accountSubmenu itemArray]];
1559                                         
1560                                         //Set the submenu
1561                                         [accountMenuItem setSubmenu:accountSubmenu];
1562                                         [accountSubmenu release];
1563                                 }
1564                         }
1565                 }
1566         }
1567         
1568         //Clean up
1569         [originalMenuItemArray release];
1572 - (void)dummyAction:(id)sender {};
1575  * @brief Remove the passed state menu items
1577  * When asked to remove state menu items, we remove all submenus off all accounts.  This is much quicker than testing
1578  * each of the duplicate menu items... this ties the accountController's implementation to the current statusController's
1579  * implementation, but is worthwhile.
1581  * @param ignoredMenuItemArray The menu items to remove... ignored in accountController's implementation
1582  */
1583 - (void)removeStateMenuItems:(NSArray *)ignoredMenuItemArray
1585         NSEnumerator    *enumerator = [accountMenuItemArraysDict objectEnumerator];
1586         NSArray                 *ourMenuItemArray;
1587         
1588         while(ourMenuItemArray = [enumerator nextObject]){
1589                 [ourMenuItemArray makeObjectsPerformSelector:@selector(setSubmenu:)
1590                                                                                   withObject:nil];
1591         }
1594 - (BOOL)validateMenuItem:(id <NSMenuItem>)menuItem
1596         return(YES);
1599 @end
1601 @implementation AIAccountController (AIAccountControllerObjectSpecifier)
1602 - (NSScriptObjectSpecifier *) objectSpecifier {
1603         id classDescription = [NSClassDescription classDescriptionForClass:[NSApplication class]];
1604         NSScriptObjectSpecifier *container = [[NSApplication sharedApplication] objectSpecifier];
1605         return [[[NSPropertySpecifier alloc] initWithContainerClassDescription:classDescription containerSpecifier:container key:@"accountController"] autorelease];
1607 @end