2 * Adium is the legal property of its developers, whose names are listed in the copyright file included
3 * with this source distribution.
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.
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.
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.
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>
45 #define PREF_GROUP_PREFERRED_ACCOUNTS @"Preferred Accounts"
46 #define ACCOUNT_DEFAULT_PREFS @"AccountPrefs"
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
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")
63 #import "AIAccountListWindowController.h"
65 @interface AIAccountController (PRIVATE)
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;
85 @implementation AIAccountController
88 - (void)initController
90 availableServiceDict = [[NSMutableDictionary alloc] init];
91 availableServiceTypeDict = [[NSMutableDictionary alloc] init];
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
102 [[adium notificationCenter] addObserver:self
103 selector:@selector(rebuildAllAccountMenuItems)
104 name:AIServiceIconSetDidChangeNotification
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
116 //Observe account changes, both list and online/connecting/offline
117 [[adium notificationCenter] addObserver:self
118 selector:@selector(accountListChanged:)
119 name:Account_ListChanged
121 [[adium contactController] registerListObjectObserver:self];
123 //Load the user accounts
126 //Observe content (for accountForSendingContentToContact)
127 [[adium notificationCenter] addObserver:self
128 selector:@selector(didSendContent:)
129 name:CONTENT_MESSAGE_SENT
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];
145 [self _prepareAccountMenus];
149 - (void)closeController
151 //Disconnect all accounts
152 [self disconnectAllAccounts];
154 //Remove observers (otherwise, every account added will be a duplicate next time around)
155 [[adium notificationCenter] removeObserver:self];
156 [[adium contactController] unregisterListObjectObserver:self];
162 [accountArray release];
163 [unloadableAccounts release];
164 [availableServiceDict release];
165 [lastAccountIDToSendContent release];
166 [accountMenuItemArraysDict release];
167 [accountMenuPluginsDict release];
169 [_cachedActiveServices release]; _cachedActiveServices = nil;
174 //Account Storage ------------------------------------------------------------------------------------------------------
175 #pragma mark Account Storage
176 //Loads the saved accounts
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];
187 accountList = [[adium preferenceController] preferenceForKey:ACCOUNT_LIST group:PREF_GROUP_ACCOUNTS];
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;
195 NSString *accountUID;
196 NSString *internalObjectID;
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];
205 if([uid hasSuffix:@"@mac.com"]){
206 serviceID = @"libgaim-oscar-Mac";
207 }else if(firstCharacter >= '0' && firstCharacter <= '9'){
208 serviceID = @"libgaim-oscar-ICQ";
210 serviceID = @"libgaim-oscar-AIM";
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";
230 //####################################
232 //XXX: Temporary Rendezvous -> Bonjour code
233 if([serviceID isEqualToString:@"rvous-libezv"]){
234 serviceID = @"bonjour-libezv";
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];
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];
247 [unloadableAccounts addObject:accountDict];
252 //Broadcast an account list changed notification
253 [[adium notificationCenter] postNotificationName:Account_ListChanged object:nil userInfo:nil];
259 NSMutableArray *flatAccounts = [NSMutableArray array];
260 NSEnumerator *enumerator;
263 //Build a flattened array of the accounts
264 enumerator = [accountArray objectEnumerator];
265 while(account = [enumerator nextObject]){
266 NSMutableDictionary *flatAccount = [NSMutableDictionary dictionary];
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
273 [flatAccounts addObject:flatAccount];
276 //Add any unloadable accounts so they're not lost
277 [flatAccounts addObjectsFromArray:unloadableAccounts];
280 [[adium preferenceController] setPreference:flatAccounts forKey:ACCOUNT_LIST group:PREF_GROUP_ACCOUNTS];
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
290 inUID = [service filterUID:inUID removeIgnoredCharacters:YES];
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];
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];
331 //Build an array of all currently used services
332 while(account = [enumerator nextObject]){
333 if(includeCompatible){
334 NSEnumerator *serviceEnumerator;
335 AIService *accountService;
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];
347 if(![serviceArray containsObject:[account service]]){
348 [serviceArray addObject:[account service]];
354 _cachedActiveServices = [[serviceArray sortedArrayUsingFunction:_alphabeticalServiceSort context:nil] retain];
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];
372 while(service = [enumerator nextObject]){
373 if([[service serviceID] isEqualToString:serviceID]) break;
379 - (NSArray *)servicesWithServiceClass:(NSString *)serviceClass
381 NSEnumerator *enumerator = [availableServiceDict objectEnumerator];
383 NSMutableArray *servicesArray = [NSMutableArray array];
385 while(service = [enumerator nextObject]){
386 if([[service serviceClass] isEqualToString:serviceClass]) [servicesArray addObject:service];
389 return(servicesArray);
392 - (BOOL)serviceWithUniqueIDIsOnline:(NSString *)identifier
394 AIService *service = [self serviceWithUniqueID:identifier];
395 NSEnumerator *enumerator = [accountArray objectEnumerator];
398 while((account = [enumerator nextObject])){
399 if(([account service] == service) &&
400 [account online]) return YES;
406 //Register service code
407 - (void)registerService:(AIService *)inService
409 [availableServiceDict setObject:inService forKey:[inService serviceCodeUniqueID]];
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;
425 NSMenu *menu = [[NSMenu alloc] init];
427 serviceArray = (activeServicesOnly ? [self activeServicesIncludingCompatibleServices:YES] : [self availableServices]);
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;
433 unsigned currentNumberOfItems;
434 BOOL addedDivider = NO;
437 currentNumberOfItems = [menu numberOfItems];
438 if (currentNumberOfItems > numberOfItems){
439 [menu addItem:[NSMenuItem separatorItem]];
440 numberOfItems = currentNumberOfItems + 1;
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]);
452 NSMenuItem *menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:(format ?
453 [NSString stringWithFormat:format,description] :
456 action:@selector(selectServiceType:)
458 [menuItem setRepresentedObject:service];
459 [menuItem setImage:[AIServiceIcons serviceIconForService:service
460 type:AIServiceIconSmall
461 direction:AIIconNormal]];
462 [menu addItem:menuItem];
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)];
474 return([menu autorelease]);
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;
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]];
496 objectID = nil; //Unrecognizable, ignore
500 while(objectID && (account = [enumerator nextObject])){
501 if([objectID isEqualToString:[account internalObjectID]]) break;
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];
514 while((account = [enumerator nextObject])){
515 if([account service] == service) [array addObject:account];
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
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];
535 //Enumerate all available services
536 while(aService = [enumerator nextObject]){
537 //Find matching serviceClasses
538 if([[aService serviceClass] isEqualToString:serviceClass]){
540 //Find matching accounts
541 NSEnumerator *enumerator = [accountArray objectEnumerator];
544 while((account = [enumerator nextObject])){
545 if ([account service] == aService){
546 [accountsArray addObject:account];
552 return(accountsArray);
555 - (AIAccount *)firstAccountWithService:(AIService *)service
557 NSEnumerator *enumerator = [accountArray objectEnumerator];
560 while((account = [enumerator nextObject])){
561 if([account service] == service) break;
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;
577 BOOL anOnlineAccountCanCreateGroupChats;
579 anOnlineAccountCanCreateGroupChats = NO;
581 enumerator = [accountArray objectEnumerator];
582 while(account = [enumerator nextObject]){
583 if ([account online] && [[account service] canCreateGroupChats]){
584 anOnlineAccountCanCreateGroupChats = YES;
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];
599 NSParameterAssert(accountArray != nil);
600 NSParameterAssert(index >= 0 && index <= [accountArray count]);
602 AIAccount *newAccount = [self defaultAccount];
604 [self insertAccount:newAccount atIndex:index save:YES];
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;
619 newAccount = [self createAccountWithService:service UID:@"" internalObjectID:nil];
621 newAccount = [self defaultAccount];
624 [self insertAccount:newAccount atIndex:index save:YES];
630 - (void)insertAccount:(AIAccount *)inAccount atIndex:(int)index save:(BOOL)shouldSave
632 if(index == -1) index = [accountArray count];
634 NSParameterAssert(inAccount != nil);
635 NSParameterAssert(accountArray != nil);
636 NSParameterAssert(index >= 0 && index <= [accountArray count]);
639 if ([accountArray count]){
640 [accountArray insertObject:inAccount atIndex:index];
642 [accountArray addObject:inAccount];
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
659 //Let the account take any action it wants before being deleted, such as disconnecting
660 [inAccount willBeDeleted];
662 //Remove from our array
663 [accountArray removeObject:inAccount];
665 //Clean up the keychain -- forget the stored password
666 [self forgetPasswordForAccount:inAccount];
668 //Save if appropriate
677 //Re-order an account on the list
678 - (int)moveAccount:(AIAccount *)account toIndex:(int)destIndex
680 int sourceIndex = [accountArray indexOfObject:account];
684 [accountArray removeObject:account];
686 //Re-insert the account
687 if(destIndex > sourceIndex){
690 [accountArray insertObject:account atIndex: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
721 //If passed a contact, we have a few better ways to determine the account than just using the first
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]){
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]){
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){
748 //If inObject is an AIListContact return the account the object is on even if the account is offline
749 if(account = [inContact account]){
755 //If the previous attempts failed, or we weren't passed a contact, use the first appropraite account
756 return([self firstAccountAvailableForSendingContentType:inType
758 includeOffline:includeOffline]);
761 - (AIAccount *)firstAccountAvailableForSendingContentType:(NSString *)inType toContact:(AIListContact *)inContact includeOffline:(BOOL)includeOffline
764 NSEnumerator *enumerator;
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)){
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)){
785 //First available account in our list
786 enumerator = [accountArray objectEnumerator];
787 while(account = [enumerator nextObject]){
788 if([account availableForSendingContentType:inType toContact:nil] || includeOffline){
795 //Can't find anything
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;
812 enumerator = [[self menuItemsForAccountsWithTarget:target includeOffline:includeOffline] objectEnumerator];
813 while(menuItem = [enumerator nextObject]){
814 if (!groupChatCreator || [[[menuItem representedObject] service] canCreateGroupChats]){
815 [menu addItem:menuItem];
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;
834 //We don't show service types unless the user is using multiple services
835 BOOL multipleServices = ([[self activeServicesIncludingCompatibleServices:NO] count] > 1);
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
844 if(available || includeOffline){
845 NSMenuItem *menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:(multipleServices ?
846 [NSString stringWithFormat:@"%@ (%@)", [account formattedUID], [[account service] shortDescription]] :
847 [account formattedUID])
849 action:@selector(selectAccount:)
851 [menuItem setRepresentedObject:account];
852 [menuItem setImage:[AIServiceIcons serviceIconForObject:account
853 type:AIServiceIconSmall
854 direction:AIIconNormal]];
855 [menuItem setEnabled:available];
857 [menuItems addObject:menuItem];
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
875 NSArray *topAccounts, *bottomAccounts;
877 //Get the list of accounts for each section of our menu
878 topAccounts = [self _accountsForSendingContentType:CONTENT_MESSAGE_TYPE
879 toListObject:inObject
881 includeOffline:includeOffline];
882 bottomAccounts = [self _accountsForSendingContentType:CONTENT_MESSAGE_TYPE
883 toListObject:inObject
885 includeOffline:includeOffline];
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];
894 return([menu autorelease]);
897 - (void)_addMenuItemsToMenu:(NSMenu *)menu withTarget:(id)target forAccounts:(NSArray *)accounts
899 NSEnumerator *enumerator = [accounts objectEnumerator];
902 while(account = [enumerator nextObject]){
903 NSMenuItem *menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:[account formattedUID]
905 action:@selector(selectAccount:)
907 [menuItem setRepresentedObject:account];
908 [menuItem setImage:[AIServiceIcons serviceIconForObject:account
909 type:AIServiceIconSmall
910 direction:AIIconNormal]];
911 [menu addItem:menuItem];
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];
922 while(account = [enumerator nextObject]){
923 if((!inObject && !inPreferred) ||
924 ([self _account:account canSendContentType:inType toListObject:inObject preferred:inPreferred includeOffline:includeOffline])){
926 [sourceAccounts addObject:account];
930 return(sourceAccounts);
933 - (BOOL)_account:(AIAccount *)account canSendContentType:(NSString *)inType toListObject:(AIListObject *)inObject preferred:(BOOL)inPreferred includeOffline:(BOOL)includeOffline
937 if ([inObject isKindOfClass:[AIMetaContact class]]){
938 NSEnumerator *enumerator = [[(AIMetaContact *)inObject listContacts] objectEnumerator];
939 AIListObject *containedObject;
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]){
951 if ([[inObject serviceClass] isEqualToString:[account serviceClass]]){
952 BOOL knowsObject = NO;
953 BOOL canFindObject = NO;
954 AIListContact *contactForAccount = [[adium contactController] existingContactWithService:[inObject service]
958 //Does the account know this object?
959 if(contactForAccount){
960 knowsObject = [account availableForSendingContentType:CONTENT_MESSAGE_TYPE
961 toContact:contactForAccount];
964 //Could the account find this object?
965 canFindObject = [account availableForSendingContentType:CONTENT_MESSAGE_TYPE toContact:nil];
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
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];
985 if(chat && destObject){
986 AIContentObject *contentObject = [userInfo objectForKey:@"AIContentObject"];
987 AIAccount *sourceAccount = (AIAccount *)[contentObject source];
989 [destObject setPreference:[sourceAccount internalObjectID]
990 forKey:KEY_PREFERRED_SOURCE_ACCOUNT
991 group:PREF_GROUP_PREFERRED_ACCOUNTS];
993 [lastAccountIDToSendContent setObject:[sourceAccount internalObjectID] forKey:[[destObject service] serviceID]];
998 //Connection convenience methods ---------------------------------------------------------------------------------------
999 #pragma mark Connection Convenience Methods
1000 //Connects all the accounts
1001 - (void)connectAllAccounts
1003 NSEnumerator *enumerator;
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];
1014 //Disconnects all the accounts
1015 - (void)disconnectAllAccounts
1017 NSEnumerator *enumerator;
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];
1029 - (BOOL)oneOrMoreConnectedAccounts
1031 NSEnumerator *enumerator;
1034 enumerator = [accountArray objectEnumerator];
1035 while((account = [enumerator nextObject])){
1036 if([account online]){
1044 - (BOOL)oneOrMoreConnectedOrConnectingAccounts
1046 NSEnumerator *enumerator;
1049 enumerator = [accountArray objectEnumerator];
1050 while((account = [enumerator nextObject])){
1051 if([account online] || [account integerStatusObjectForKey:@"Connecting"]){
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]]);
1069 return([NSString stringWithFormat:@"Adium.%@",[self _accountNameForAccount:inAccount]]);
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]);
1079 return([NSString stringWithFormat:@"Adium.%@",proxyServer]);
1084 //Save an account password
1085 - (void)setPassword:(NSString *)inPassword forAccount:(AIAccount *)inAccount
1088 [AIKeychain putPasswordInKeychainForService:[self _passKeyForAccount:inAccount]
1089 account:[self _accountNameForAccount:inAccount]
1090 password:inPassword];
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]];
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
1107 //check the keychain for this password
1108 password = [AIKeychain getPasswordFromKeychainForService:[self _passKeyForAccount:inAccount]
1109 account:[self _accountNameForAccount:inAccount]];
1111 if(password && [password length] != 0){
1112 //Invoke the target right away
1113 [inTarget performSelector:inSelector withObject:password withObject:inContext afterDelay:0.0001];
1115 //Prompt the user for their password
1116 [ESAccountPasswordPromptController showPasswordPromptForAccount:inAccount
1117 notifyingTarget:inTarget
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]];
1138 - (void)passwordForProxyServer:(NSString *)server userName:(NSString *)userName notifyingTarget:(id)inTarget selector:(SEL)inSelector context:(id)inContext
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];
1150 //Prompt the user for their password
1151 [ESProxyPasswordPromptController showPasswordPromptForProxyServer:server
1153 notifyingTarget:inTarget
1158 //Save a proxy server password
1159 - (void)setPassword:(NSString *)inPassword forProxyServer:(NSString *)server userName:(NSString *)userName
1162 [AIKeychain putPasswordInKeychainForService:[self _passKeyForProxyServer:server]
1163 account:[self _accountNameForProxyServer:server
1164 userName:userName] password:inPassword];
1166 [AIKeychain removePasswordFromKeychainForService:[self _passKeyForProxyServer:server]
1167 account:[self _accountNameForProxyServer:server userName:userName]];
1171 - (void)_upgradePasswords
1174 NSEnumerator *enumerator = [accountArray objectEnumerator];
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];
1183 NSString *password = [AIKeychainOld getPasswordFromKeychainForService:passKey
1184 account:accountName];
1188 [AIKeychain putPasswordInKeychainForService:passKey
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]];
1218 [accountMenuItemArraysDict setObject:[NSMutableArray array]
1220 [accountMenuPluginsDict setObject:accountMenuPlugin
1223 [self _addAccountMenuItemsForPlugin:accountMenuPlugin];
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];
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
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)]
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];
1263 NSEnumerator *enumerator;
1265 NSMenuItem *menuItem;
1267 //Create a menuitem for each account
1268 enumerator = [[self accountArray] objectEnumerator];
1269 while((account = [enumerator nextObject])){
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:@""
1275 action:@selector(toggleConnection:)
1277 [menuItem setRepresentedObject:account];
1278 [menuItemArray addObject:menuItem];
1281 [self _updateMenuItem:menuItem forAccount:account forPlugin:accountMenuPlugin];
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];
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
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;
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;
1329 titleFormat = ACCOUNT_CONNECT_MENU_TITLE;
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];
1357 [composite release];
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];
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"])){
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"];
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];
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];
1407 //Find the menu item for this account in this array
1408 menuItem = [self _menuItemForAccount:(AIAccount *)inObject
1409 fromArray:accountMenuItemArray];
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]];
1420 //We don't change any keys
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;
1432 enumerator = [accountMenuItemArray objectEnumerator];
1433 while((menuItem = [enumerator nextObject])){
1434 if([menuItem representedObject] == account){
1435 targetMenuItem = menuItem;
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];
1463 return actionsSubmenu;
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.
1472 - (void)addStateMenuItems:(NSArray *)menuItemArray
1474 NSArray *originalMenuItemArray;
1475 NSMutableDictionary *temporaryMenuDict = [NSMutableDictionary dictionary];
1476 NSEnumerator *enumerator;
1478 NSArray *accountMenuItemArray;
1479 int accountArrayCount;
1481 accountArrayCount = [accountArray count];
1483 //Hold on to this array
1484 originalMenuItemArray = [menuItemArray copy];
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];
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];
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;
1501 //Enumerate all the menu items we were originally passed
1502 while(menuItem = [menuItemEnumerator nextObject]){
1504 NSDictionary *newRepresentedObject;
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",
1513 newRepresentedObject = [NSDictionary dictionaryWithObject:account
1514 forKey:@"AIAccount"];
1517 accountMenuItem = [[menuItem copy] autorelease];
1518 [accountMenuItem setRepresentedObject:newRepresentedObject];
1521 [accountSubmenu addItem:accountMenuItem];
1525 if([accountSubmenu numberOfItems] > 0){
1526 [temporaryMenuDict setObject:accountSubmenu
1527 forKey:[account internalObjectID]];
1531 [accountSubmenu setMenuChangedMessagesEnabled:YES];
1534 //Enumerate all arrays of menu items (for all plugins)
1535 NSNumber *identifier;
1537 enumerator = [accountMenuItemArraysDict keyEnumerator];
1538 while(identifier = [enumerator nextObject]){
1539 id<AccountMenuPlugin> accountMenuPlugin = [accountMenuPluginsDict objectForKey:identifier];
1541 if([accountMenuPlugin showStatusSubmenu]){
1542 NSEnumerator *accountMenuItemEnumerator;
1543 NSMenuItem *accountMenuItem;
1545 accountMenuItemArray = [accountMenuItemArraysDict objectForKey:identifier];
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;
1554 if(generatedAccountSubmenu = [temporaryMenuDict objectForKey:[account internalObjectID]]){
1555 NSMenu *accountSubmenu = [generatedAccountSubmenu copy];
1557 //Tell the status controller to update these items as necessary
1558 [[adium statusController] plugin:self didAddMenuItems:[accountSubmenu itemArray]];
1561 [accountMenuItem setSubmenu:accountSubmenu];
1562 [accountSubmenu release];
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
1583 - (void)removeStateMenuItems:(NSArray *)ignoredMenuItemArray
1585 NSEnumerator *enumerator = [accountMenuItemArraysDict objectEnumerator];
1586 NSArray *ourMenuItemArray;
1588 while(ourMenuItemArray = [enumerator nextObject]){
1589 [ourMenuItemArray makeObjectsPerformSelector:@selector(setSubmenu:)
1594 - (BOOL)validateMenuItem:(id <NSMenuItem>)menuItem
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];