Merged [13645] and [13646]: Fixed one of the longest-standing Adium bugs: The Color...
[adiumx.git] / Source / AIStatusController.m
blob4661c773ea8665fe0f73e47c4232520335a7b7a1
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 #import "AIAccountController.h"
18 #import "AIContactController.h"
19 #import "AIEditStateWindowController.h"
20 #import "AIPreferenceController.h"
21 #import "AIStatusController.h"
22 #import <AIUtilities/AIMenuAdditions.h>
23 #import <AIUtilities/AIArrayAdditions.h>
24 #import <AIUtilities/AIAttributedStringAdditions.h>
25 #import <AIUtilities/AIEventAdditions.h>
26 #import <AIUtilities/AIStringAdditions.h>
27 #import <AIUtilities/CBObjectAdditions.h>
28 #import <AIUtilities/CBApplicationAdditions.h>
29 #import <Adium/AIAccount.h>
30 #import <Adium/AIService.h>
31 #import <Adium/AIStatusIcons.h>
33 //State menu
34 #define STATE_TITLE_MENU_LENGTH         30
35 #define STATUS_TITLE_CUSTOM                     [AILocalizedString(@"Custom",nil) stringByAppendingEllipsis]
36 #define STATUS_TITLE_OFFLINE            AILocalizedString(@"Offline",nil)
38 #define BUILT_IN_STATE_ARRAY            @"BuiltInStatusStates"
40 #define TOP_STATUS_STATE_ID                     @"TopStatusID"
42 @interface AIStatusController (PRIVATE)
43 - (NSArray *)builtInStateArray;
45 - (void)_saveStateArrayAndNotifyOfChanges;
46 - (void)_upgradeSavedAwaysToSavedStates;
47 - (void)_setMachineIsIdle:(BOOL)inIdle;
48 - (void)_addStateMenuItemsForPlugin:(id <StateMenuPlugin>)stateMenuPlugin;
49 - (void)_removeStateMenuItemsForPlugin:(id <StateMenuPlugin>)stateMenuPlugin;
50 - (BOOL)removeIfNecessaryTemporaryStatusState:(AIStatus *)originalState;
51 - (NSString *)_titleForMenuDisplayOfState:(AIStatus *)statusState;
53 - (NSArray *)_menuItemsForStatusesOfType:(AIStatusType)type forServiceCodeUniqueID:(NSString *)inServiceCodeUniqueID withTarget:(id)target;
54 - (void)_addMenuItemsForStatusOfType:(AIStatusType)type
55                                                   withTarget:(id)target
56                                                          fromSet:(NSSet *)sourceArray
57                                                          toArray:(NSMutableArray *)menuItems
58                                   alreadyAddedTitles:(NSMutableSet *)alreadyAddedTitles;
59 - (void)buildBuiltInStatusTypes;
61 @end
63 /*!
64  * @class AIStatusController
65  * @brief Core status & state methods
66  *
67  * This class provides a foundation for Adium's status and status state systems.
68  */
69 @implementation AIStatusController
71 static  NSMutableSet                    *temporaryStateArray = nil;
73 /*!
74  * Init the status controller
75  */
76 - (void)initController
78         stateMenuItemArraysDict = [[NSMutableDictionary alloc] init];
79         stateMenuPluginsArray = [[NSMutableArray alloc] init];
80         stateMenuItemsNeedingUpdating = [[NSMutableSet alloc] init];
81         stateMenuUpdateDelays = 0;
82         _sortedFullStateArray = nil;
83         _activeStatusState = nil;
84         _allActiveStatusStates = nil;
85         temporaryStateArray = [[NSMutableSet alloc] init];
86         
87         accountsToConnect = [[NSMutableSet alloc] init];
88         isProcessingGlobalChange = NO;
90         //Init
91         [self _setMachineIsIdle:NO];
93         NSNotificationCenter *adiumNotificationCenter = [adium notificationCenter];
95         //Update our state menus when the state array or status icon set changes
96         [adiumNotificationCenter addObserver:self
97                                                                    selector:@selector(rebuildAllStateMenus)
98                                                                            name:AIStatusStateArrayChangedNotification
99                                                                          object:nil];
100         [adiumNotificationCenter addObserver:self
101                                                                 selector:@selector(rebuildAllStateMenus)
102                                                                         name:AIStatusIconSetDidChangeNotification
103                                                                   object:nil];
104         [[adium contactController] registerListObjectObserver:self];
106         //Watch account status preference changes for our accountsToConnect set
107         [[adium preferenceController] registerPreferenceObserver:self forGroup:GROUP_ACCOUNT_STATUS];
109         [self buildBuiltInStatusTypes];
113  * @brief Finish initing the status controller
115  * Set our initial status state, and restore our array of accounts to connect when a global state is selected.
116  */
117 - (void)finishIniting
119         /* Load our array of accounts which were connected when we quit; these will be the accounts to connect if an online
120          * status is selected with no accounts online. */
121         NSArray *savedAccountsToConnect = [[adium preferenceController] preferenceForKey:@"SavedAccountsToConnect"
122                                                                                                                                                            group:GROUP_ACCOUNT_STATUS];
123         NSEnumerator    *enumerator;
124         AIAccount               *account;
126         if(savedAccountsToConnect){
127                 NSString                *internalObjectID;
129                 enumerator = [savedAccountsToConnect objectEnumerator];
130                 while(internalObjectID = [enumerator nextObject]){
131                         AIAccount       *account = [[adium accountController] accountWithInternalObjectID:internalObjectID];
132                         if(account) [accountsToConnect addObject:account];
133                 }
134         }else{
135                 /* First launch situation.  Use auto connect if possible to avoid signing on all accounts. */
136                 enumerator = [[[adium accountController] accountArray] objectEnumerator];
137                 while(account = [enumerator nextObject]){
138                         if([[account preferenceForKey:@"AutoConnect" group:GROUP_ACCOUNT_STATUS] boolValue]){
139                                 [accountsToConnect addObject:account];
140                         }
141                 }
142         }
143         
144         //Put each account into the status it was in last time we quit.
145         BOOL            needToRebuildMenus = NO;
146         enumerator = [[[adium accountController] accountArray] objectEnumerator];
147         while(account = [enumerator nextObject]){
148                 NSData          *lastStatusData = [account preferenceForKey:@"LastStatus"
149                                                                                                                   group:GROUP_ACCOUNT_STATUS];
150                 AIStatus        *lastStatus = nil;
151                 if(lastStatusData){
152                         lastStatus = [NSKeyedUnarchiver unarchiveObjectWithData:lastStatusData];
153                 }
155                 if(lastStatus){
156                         AIStatus        *existingStatus;
157                         
158                         /* We want to use a loaded status instance if one exists.  This will be the case if the account
159                          * was last in a built-in or user defined and saved state.  If the last state was unsaved, existingStatus
160                          * will be nil.
161                          */
162                         existingStatus = [self statusStateWithUniqueStatusID:[lastStatus uniqueStatusID]];
163                         
164                         if(existingStatus){
165                                 lastStatus = existingStatus;
166                         }else{
167                                 //Add to our temporary status array
168                                 [temporaryStateArray addObject:lastStatus];
169                                 
170                                 //And clear our full array so it will reflect this newly loaded status when next used
171                                 [_sortedFullStateArray release]; _sortedFullStateArray = nil;
172                                 needToRebuildMenus = YES;
173                         }
174                         
175                         [account setStatusStateAndRemainOffline:lastStatus];
176                 }
177         }
178         
179         if(needToRebuildMenus){
180                 //Clear the sorted menu items array since our state array changed.
181                 [_sortedFullStateArray release]; _sortedFullStateArray = nil;
182                 
183                 //Now rebuild our menus to include this temporary item
184                 [self rebuildAllStateMenus];
185         }
189  * @brief Begin closing the status controller
191  * Save the online accounts; they will be the accounts connected by a global status change
193  * Also save the current status state of each account so it can be restored on next launch.
195  * Note: accountsToConnect is not the same as online accounts. It may, for example, have a single entry which is
196  * the last account to have been connected (if no accounts are currently online).
197  */
198 - (void)beginClosing
200         NSMutableArray  *savedAccountsToConnect = [NSMutableArray array];
201         NSEnumerator    *enumerator;
202         AIAccount               *account;
204         enumerator = [[[adium accountController] accountArray] objectEnumerator];
205         while(account = [enumerator nextObject]){
206                 
207                 //If this account is online, we'll want to save its internalObjectID.
208                 if([account online]){
209                         [savedAccountsToConnect addObject:[account internalObjectID]];
210                 }
211                 
212                 /* Store the current status state for use on next launch.
213                  *
214                  * We use the statusObjectForKey:@"StatusState" accessor rather than [account statusState]
215                  * because we don't want anything besides the account's actual status state.  That is, we don't
216                  * want the default available state if the account doesn't have a state yet, and we want the
217                  * real last-state-which-was-set (not the offline one) if the account is offline. */
218                 AIStatus        *currentStatus = [account statusObjectForKey:@"StatusState"];
219                 [account setPreference:((currentStatus && (currentStatus != offlineStatusState)) ?
220                                                                 [NSKeyedArchiver archivedDataWithRootObject:currentStatus] :
221                                                                 nil)
222                                                 forKey:@"LastStatus"
223                                                  group:GROUP_ACCOUNT_STATUS];
224         }
226         [[adium preferenceController] setPreference:savedAccountsToConnect
227                                                                                  forKey:@"SavedAccountsToConnect"
228                                                                                   group:GROUP_ACCOUNT_STATUS];
229         
230         [[adium preferenceController] setPreference:[NSKeyedArchiver archivedDataWithRootObject:[self stateArray]]
231                                                                                  forKey:KEY_SAVED_STATUS
232                                                                                   group:PREF_GROUP_SAVED_STATUS];
236  * @brief Close the status controller
237  */
238 - (void)closeController
240         [[adium notificationCenter] removeObserver:self];
241         [[adium preferenceController] unregisterPreferenceObserver:self];
242         [[adium contactController] unregisterListObjectObserver:self];
246  * @brief Deallocate
247  */
248 - (void)dealloc
250         [stateArray release]; stateArray = nil;
251         [_sortedFullStateArray release]; _sortedFullStateArray = nil;
252         [super dealloc];
255 #pragma mark Status registration
257  * @brief Register a status for a service
259  * Implementation note: Each AIStatusType has its own NSMutableDictionary, statusDictsByServiceCodeUniqueID.
260  * statusDictsByServiceCodeUniqueID is keyed by serviceCodeUniqueID; each object is an NSMutableSet of NSDictionaries.
261  * Each of these dictionaries has KEY_STATUS_NAME, KEY_STATUS_DESCRIPTION, and KEY_STATUS_TYPE.
263  * @param statusName A name which will be passed back to accounts of this service.  Internal use only.  Use the AIStatusController.h #defines where appropriate.
264  * @param description A human-readable localized description which will be shown to the user.  Use the AIStatusController.h #defines where appropriate.
265  * @param type An AIStatusType, the general type of this status.
266  * @param service The AIService for which to register the status
267  */
268 - (void)registerStatus:(NSString *)statusName withDescription:(NSString *)description ofType:(AIStatusType)type forService:(AIService *)service
270         NSMutableSet    *statusDicts;
271         NSString                *serviceCodeUniqueID = [service serviceCodeUniqueID];
273         //Create the set if necessary
274         if(!statusDictsByServiceCodeUniqueID[type]) statusDictsByServiceCodeUniqueID[type] = [[NSMutableDictionary alloc] init];
275         if(!(statusDicts = [statusDictsByServiceCodeUniqueID[type] objectForKey:serviceCodeUniqueID])){
276                 statusDicts = [NSMutableSet set];
277                 [statusDictsByServiceCodeUniqueID[type] setObject:statusDicts
278                                                                                                    forKey:serviceCodeUniqueID];
279         }
281         //Create a dictionary for this status entry
282         NSDictionary *statusDict = [NSDictionary dictionaryWithObjectsAndKeys:
283                 statusName, KEY_STATUS_NAME,
284                 description, KEY_STATUS_DESCRIPTION,
285                 [NSNumber numberWithInt:type], KEY_STATUS_TYPE,
286                 nil];
288         [statusDicts addObject:statusDict];
291 #pragma mark Status menus
293  * @brief Generate and return a menu of status types (Away, Be right back, etc.)
295  * @param service The service for which to return a specific list of types, or nil to return all available types
296  * @param target The target for the menu items, which will have an action of @selector(selectStatus:)
298  * @result The menu of statuses, separated by available and away status types
299  */
300 - (NSMenu *)menuOfStatusesForService:(AIService *)service withTarget:(id)target
302         NSMenu                  *menu = [[NSMenu allocWithZone:[NSMenu menuZone]] init];
303         NSEnumerator    *enumerator;
304         NSMenuItem              *menuItem;
305         NSString                *serviceCodeUniqueID = [service serviceCodeUniqueID];
306         AIStatusType    type;
308         for(type = AIAvailableStatusType ; type < STATUS_TYPES_COUNT ; type++){
309                 NSArray         *menuItemArray;
311                 menuItemArray = [self _menuItemsForStatusesOfType:type
312                                                                    forServiceCodeUniqueID:serviceCodeUniqueID
313                                                                                            withTarget:target];
315                 //Add a separator between each type after available
316                 if ((type > AIAvailableStatusType) && [menuItemArray count]){
317                         [menu addItem:[NSMenuItem separatorItem]];
318                 }
320                 //Add the items for this type
321                 enumerator = [menuItemArray objectEnumerator];
322                 while(menuItem = [enumerator nextObject]){
323                         [menu addItem:menuItem];
324                 }
325         }
327         return [menu autorelease];
331  * @brief Sort status menu items
333  * Sort alphabetically by title.
334  */
335 int statusMenuItemSort(id menuItemA, id menuItemB, void *context)
337         return [[menuItemA title] caseInsensitiveCompare:[menuItemB title]];
341  * @brief Return an array of menu items for an AIStatusType and service
343  * @pram type The AIStatusType for which to return statuses
344  * @param inServiceCodeUniqueID The service for which to return active statuses.  If nil, return all statuses for online services.
345  * @param target The target for the menu items
347  * @result An <tt>NSArray</tt> of <tt>NSMenuItem</tt> objects.
348  */
349 - (NSArray *)_menuItemsForStatusesOfType:(AIStatusType)type forServiceCodeUniqueID:(NSString *)inServiceCodeUniqueID withTarget:(id)target
351         //Quick efficiency: If asked for the offline status type, just return nil as we have no offline statuses at present.
352         if(type == AIOfflineStatusType) return nil;
354         NSMutableArray  *menuItems = [[NSMutableArray alloc] init];
355         NSMutableSet    *alreadyAddedTitles = [NSMutableSet set];
357         //First, add our built-in items (so they will be at the top of the array and service-specific 'copies' won't replace them)
358         [self _addMenuItemsForStatusOfType:type
359                                                         withTarget:target
360                                                            fromSet:builtInStatusTypes[type]
361                                                            toArray:menuItems
362                                         alreadyAddedTitles:alreadyAddedTitles];
364         //Now, add items for this service, or from all available services, as appropriate
365         if(inServiceCodeUniqueID){
366                 NSSet   *statusDicts;
368                 //Obtain the status dicts for this type and service code unique ID
369                 if(statusDicts = [statusDictsByServiceCodeUniqueID[type] objectForKey:inServiceCodeUniqueID]){
370                         //And add them
371                         [self _addMenuItemsForStatusOfType:type
372                                                                         withTarget:target
373                                                                            fromSet:statusDicts
374                                                                            toArray:menuItems
375                                                         alreadyAddedTitles:alreadyAddedTitles];
376                 }
378         }else{
379                 NSEnumerator    *enumerator;
380                 NSString                *serviceCodeUniqueID;
381                 BOOL                    oneOrMoreConnectedAccounts = [[adium accountController] oneOrMoreConnectedAccounts];
383                 //Insert a menu item for each available account
384                 enumerator = [statusDictsByServiceCodeUniqueID[type] keyEnumerator];
385                 while(serviceCodeUniqueID = [enumerator nextObject]){
386                         /* Obtain the status dicts for this type and service code unique ID if it is online or
387                          * if no accounts are online but an account of this service code is configured*/
388                         if([[adium accountController] serviceWithUniqueIDIsOnline:serviceCodeUniqueID] ||
389                                 (!oneOrMoreConnectedAccounts && [[adium accountController] firstAccountWithService:[[adium accountController] serviceWithUniqueID:serviceCodeUniqueID]])){
390                                 NSSet   *statusDicts;
392                                 //Obtain the status dicts for this type and service code unique ID
393                                 if(statusDicts = [statusDictsByServiceCodeUniqueID[type] objectForKey:serviceCodeUniqueID]){
394                                         //And add them
395                                         [self _addMenuItemsForStatusOfType:type
396                                                                                         withTarget:target
397                                                                                            fromSet:statusDicts
398                                                                                            toArray:menuItems
399                                                                         alreadyAddedTitles:alreadyAddedTitles];
400                                 }
401                         }
402                 }
403         }
405         [menuItems sortUsingFunction:statusMenuItemSort context:nil];
407         return [menuItems autorelease];
411  * @brief Add menu items for a particular type of status
413  * @param type The AIStatusType, used for determining the icon of the menu items
414  * @param target The target of the created menu items
415  * @param statusDicts An NSSet of NSDictionary objects, which should each represent a status of the passed type
416  * @param menuItems The NSMutableArray to which to add the menuItems
417  * @param alreadyAddedTitles NSMutableSet of NSString titles which have already been added and should not be duplicated. Will be updated as items are added.
418  */
419 - (void)_addMenuItemsForStatusOfType:(AIStatusType)type
420                                                   withTarget:(id)target
421                                                          fromSet:(NSSet *)statusDicts
422                                                          toArray:(NSMutableArray *)menuItems
423                                   alreadyAddedTitles:(NSMutableSet *)alreadyAddedTitles
425         NSEnumerator    *statusDictEnumerator = [statusDicts objectEnumerator];
426         NSDictionary    *statusDict;
428         //Enumerate the status dicts
429         while(statusDict = [statusDictEnumerator nextObject]){
430                 NSString        *title = [statusDict objectForKey:KEY_STATUS_DESCRIPTION];
432                 /*
433                  * Only add if it has not already been added by another service.... Services need to use unique titles if they have
434                  * unique state names, but are welcome to share common name/description combinations, which is why the #defines
435                  * exist.
436                  */
437                 if(![alreadyAddedTitles containsObject:title]){
438                         NSImage         *image;
439                         NSMenuItem      *menuItem;
441                         menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:title
442                                                                                                                                                         target:target
443                                                                                                                                                         action:@selector(selectStatus:)
444                                                                                                                                          keyEquivalent:@""];
446                         image = [[[AIStatusIcons statusIconForStatusName:[statusDict objectForKey:KEY_STATUS_NAME]
447                                                                                                   statusType:type
448                                                                                                         iconType:AIStatusIconList
449                                                                                                    direction:AIIconNormal] copy] autorelease];
451                         [menuItem setRepresentedObject:statusDict];
452                         [menuItem setImage:image];
453                         [menuItem setEnabled:YES];
454                         [menuItems addObject:menuItem];
455                         [menuItem release];
457                         [alreadyAddedTitles addObject:title];
458                 }
459         }
462 #pragma mark Status State Descriptions
464  * @brief Return the localized description for the sate of the passed status
466  * This could be stored with the statusState, but that would break if the locale changed.  This way, the nonlocalized
467  * string is used to look up the appropriate localized one.
469  * @result A localized description such as @"Away" or @"Out to Lunch" of the state used by statusState
470  */
471 - (NSString *)descriptionForStateOfStatus:(AIStatus *)statusState
473         NSString                *statusName = [statusState statusName];
474         AIStatusType    statusType = [statusState statusType];
475         NSEnumerator    *enumerator = [statusDictsByServiceCodeUniqueID[statusType] objectEnumerator];
476         NSSet                   *set;
477         while(set = [enumerator nextObject]){
478                 NSEnumerator    *statusDictsEnumerator = [set objectEnumerator];
479                 NSDictionary    *statusDict;
480                 while(statusDict = [statusDictsEnumerator nextObject]){
481                         if([[statusDict objectForKey:KEY_STATUS_NAME] isEqualToString:statusName]){
482                                 return [statusDict objectForKey:KEY_STATUS_DESCRIPTION];
483                         }
484                 }
485         }
487         return nil;
490 - (NSString *)localizedDescriptionForCoreStatusName:(NSString *)statusName
492         static NSDictionary     *coreLocalizedStatusDescriptions = nil;
493         if(!coreLocalizedStatusDescriptions){
494                 coreLocalizedStatusDescriptions = [[NSDictionary dictionaryWithObjectsAndKeys:
495                         STATUS_DESCRIPTION_AVAILABLE, STATUS_NAME_AVAILABLE,
496                         STATUS_DESCRIPTION_FREE_FOR_CHAT, STATUS_NAME_FREE_FOR_CHAT,
497                         STATUS_DESCRIPTION_AVAILABLE_FRIENDS_ONLY, STATUS_NAME_AVAILABLE_FRIENDS_ONLY,
498                         STATUS_DESCRIPTION_AWAY, STATUS_NAME_AWAY,
499                         STATUS_DESCRIPTION_EXTENDED_AWAY, STATUS_NAME_EXTENDED_AWAY,
500                         STATUS_DESCRIPTION_AWAY_FRIENDS_ONLY, STATUS_NAME_AWAY_FRIENDS_ONLY,
501                         STATUS_DESCRIPTION_DND, STATUS_NAME_DND,
502                         STATUS_DESCRIPTION_NOT_AVAILABLE, STATUS_NAME_NOT_AVAILABLE,
503                         STATUS_DESCRIPTION_OCCUPIED, STATUS_NAME_OCCUPIED,
504                         STATUS_DESCRIPTION_BRB, STATUS_NAME_BRB,
505                         STATUS_DESCRIPTION_BUSY, STATUS_NAME_BUSY,
506                         STATUS_DESCRIPTION_PHONE, STATUS_NAME_PHONE,
507                         STATUS_DESCRIPTION_LUNCH, STATUS_NAME_LUNCH,
508                         STATUS_DESCRIPTION_NOT_AT_HOME, STATUS_NAME_NOT_AT_HOME,
509                         STATUS_DESCRIPTION_NOT_AT_DESK, STATUS_NAME_NOT_AT_DESK,
510                         STATUS_DESCRIPTION_NOT_IN_OFFICE, STATUS_NAME_NOT_IN_OFFICE,
511                         STATUS_DESCRIPTION_VACATION, STATUS_NAME_VACATION,
512                         STATUS_DESCRIPTION_STEPPED_OUT, STATUS_NAME_STEPPED_OUT,
513                         STATUS_DESCRIPTION_INVISIBLE, STATUS_NAME_INVISIBLE,
514                         STATUS_DESCRIPTION_OFFLINE, STATUS_NAME_OFFLINE,
515                         nil] retain];
516         }
518         return([coreLocalizedStatusDescriptions objectForKey:statusName]);
524  * @brief The status name to use by default for a passed type
526  * This is the name which will be used for new AIStatus objects of this type.
527  */
528 - (NSString *)defaultStatusNameForType:(AIStatusType)statusType
530         //Set the default status name
531         switch(statusType){
532                 case AIAvailableStatusType:
533                         return STATUS_NAME_AVAILABLE;
534                         break;
535                 case AIAwayStatusType:
536                         return STATUS_NAME_AWAY;
537                         break;
538                 case AIInvisibleStatusType:
539                         return STATUS_NAME_INVISIBLE;
540                         break;
541                 case AIOfflineStatusType:
542                         return STATUS_NAME_OFFLINE;
543                         break;
544         }
546         return nil;
549 #pragma mark Setting Status States
551  * @brief Set the active status state
553  * Sets the currently active status state.  This applies throughout Adium and to all accounts.  The state will become
554  * effective immediately.
555  */
556 - (void)setActiveStatusState:(AIStatus *)statusState
558         //Apply the state to our accounts and notify (delay to the next run loop to improve perceived speed)
559         [self performSelector:@selector(applyState:toAccounts:)
560                            withObject:statusState
561                            withObject:[[adium accountController] accountArray]
562                            afterDelay:0];
566  * @brief Return the <tt>AIStatus</tt> to be used by accounts as they are created
567  */
568 - (AIStatus *)defaultInitialStatusState
570         return [[self builtInStateArray] objectAtIndex:0];
574  * @brief Reset the active status state
576  * All active status states cache will also reset.  Posts an active status changed notification.  The active state
577  * will be regenerated the next time it is requested.
578  */
579 - (void)_resetActiveStatusState
581         //Clear the active status state.  It will be rebuilt next time it is requested
582         [_activeStatusState release]; _activeStatusState = nil;
583         [_allActiveStatusStates release]; _allActiveStatusStates = nil;
585         //Let observers know the active state has changed
586         [[adium notificationCenter] postNotificationName:AIStatusActiveStateChangedNotification object:nil];
590  * @brief Apply a state to multiple accounts
591  */
592 - (void)applyState:(AIStatus *)statusState toAccounts:(NSArray *)accountArray
594         NSEnumerator    *enumerator;
595         AIAccount               *account;
596         AIStatus                *aStatusState;
597         BOOL                    shouldRebuild = NO;
598         BOOL                    noConnectedAccounts = ![[adium accountController] oneOrMoreConnectedAccounts];
600         //We should connect all accounts if our accounts to connect array is empty and there are no connected accounts
601         BOOL                    shouldConnectAllAccounts = (([accountsToConnect count] == 0) &&
602                                                                                                 noConnectedAccounts);
604         isProcessingGlobalChange = YES;
605         [self setDelayStateMenuUpdates:YES];
606         
607         enumerator = [accountArray objectEnumerator];
608         while(account = [enumerator nextObject]){
609                 if([account online] ||
610                    (noConnectedAccounts && [accountsToConnect containsObject:account]) || 
611                    (shouldConnectAllAccounts)){
612                         /* If this account is online, or no accounts are online and this is an account to connect,
613                          * or we should be connecting all accounts, set the status completely. */
614                         [account setStatusState:statusState];
615                 }else{
616                         //If this account should not have its state set now, perform internal bookkeeping so a future sign-on
617                         //will be to the most appropriate state
618                         [account setStatusStateAndRemainOffline:statusState];
619                 }
620         }
621         
622         //Any objects in the temporary state array which aren't the state we just set should now be removed.
623         enumerator = [[[temporaryStateArray copy] autorelease] objectEnumerator];
624         while(aStatusState = [enumerator nextObject]){
625                 if(aStatusState != statusState){
626                         [temporaryStateArray removeObject:aStatusState];
627                         shouldRebuild = YES;
628                 }
629         }
631         isProcessingGlobalChange = NO;
633         if(shouldRebuild){
634                 //Manually decrease the update delays counter as we don't want to call [self updateAllStateMenuSelections]
635                 stateMenuUpdateDelays--;
636                 [self rebuildAllStateMenus];
637         }else{
638                 /* Allow setDelayStateMenuUpdates to decreate the counter and call
639                  * [self updateAllStateMenuSelections] as appropriate.
640                  */
641                 [self setDelayStateMenuUpdates:NO];                             
642         }
645 #pragma mark Retrieving Status States
647  * @brief Access to Adium's user-defined states
649  * Returns an array of available user-defined states, which are AIStatus objects
650  */
651 - (NSArray *)stateArray
653         if(!stateArray){
654                 NSData  *savedStateArrayData = [[adium preferenceController] preferenceForKey:KEY_SAVED_STATUS
655                                                                                                                                                                 group:PREF_GROUP_SAVED_STATUS];
656                 if(savedStateArrayData){
657                         stateArray = [[NSKeyedUnarchiver unarchiveObjectWithData:savedStateArrayData] mutableCopy];
658                 }
660                 if(!stateArray) stateArray = [[NSMutableArray alloc] init];
662                 //Upgrade Adium 0.7x away messages
663                 [self _upgradeSavedAwaysToSavedStates];
664         }
666         return(stateArray);
670  * @brief Return the array of built-in states
672  * These are basic Available and Away states which should always be visible and are (by convention) immutable.
673  * The first state in BUILT_IN_STATE_ARRAY will be used as the default for accounts as they are created.
674  */
675 - (NSArray *)builtInStateArray
677         if(!builtInStateArray){
678                 NSArray                 *savedBuiltInStateArray = [NSArray arrayNamed:BUILT_IN_STATE_ARRAY forClass:[self class]];
679                 NSEnumerator    *enumerator;
680                 NSDictionary    *dict;
682                 builtInStateArray = [[NSMutableArray alloc] initWithCapacity:[savedBuiltInStateArray count]];
684                 enumerator = [savedBuiltInStateArray objectEnumerator];
685                 while(dict = [enumerator nextObject]){
686                         AIStatus        *status = [AIStatus statusWithDictionary:dict];
687                         [builtInStateArray addObject:status];
689                         //Store a reference to our offline state if we just loaded it
690                         if([status statusType] == AIOfflineStatusType){
691                                 [offlineStatusState release];
692                                 offlineStatusState = [status retain];
693                         }
694                 }
695         }
697         return(builtInStateArray);
700 - (AIStatus *)offlineStatusState
702         //Ensure the built in states have been loaded
703         [self builtInStateArray];
705         NSAssert(offlineStatusState != nil, @"Nil offline status state");
706         return offlineStatusState;
709 //Sort the status array
710 int _statusArraySort(id objectA, id objectB, void *context)
712         AIStatusType statusTypeA = [objectA statusType];
713         AIStatusType statusTypeB = [objectB statusType];
715         //We treat Invisible statuses as being the same as Away for purposes of the menu
716         if(statusTypeA == AIInvisibleStatusType) statusTypeA = AIAwayStatusType;
717         if(statusTypeB == AIInvisibleStatusType) statusTypeB = AIAwayStatusType;
719         if(statusTypeA > statusTypeB){
720                 return NSOrderedDescending;
721         }else if(statusTypeB > statusTypeA){
722                 return NSOrderedAscending;
723         }else{
724                 BOOL isLockedMutabilityTypeA = ([objectA mutabilityType] == AILockedStatusState);
725                 BOOL isLockedMutabilityTypeB = ([objectB mutabilityType] == AILockedStatusState);
727                 //Put locked (built in) statuses at the top
728                 if(isLockedMutabilityTypeA && !isLockedMutabilityTypeB){
729                         return NSOrderedAscending;
730                         
731                 }else if(!isLockedMutabilityTypeA && isLockedMutabilityTypeB){
732                         return NSOrderedDescending;
734                 }else{
735                         /* Check to see if either is temporary; temporary items go above saved ones and below
736                          * built-in ones.
737                          */
738                         BOOL    isTemporaryA = [temporaryStateArray containsObject:objectA];
739                         BOOL    isTemporaryB = [temporaryStateArray containsObject:objectB];
741                         if(isTemporaryA && !isTemporaryB){
742                                 return NSOrderedAscending;
743                                 
744                         }else if(isTemporaryB && !isTemporaryA){
745                                 return NSOrderedDescending;
746                                 
747                         }else{
748                                 NSArray *originalArray = (NSArray *)context;
749                                 
750                                 //Return them in the same relative order as the original array if they are of the same type
751                                 int indexA = [originalArray indexOfObjectIdenticalTo:objectA];
752                                 int indexB = [originalArray indexOfObjectIdenticalTo:objectB];
753                                 
754                                 if(indexA > indexB){
755                                         return NSOrderedDescending;
756                                 }else{
757                                         return NSOrderedAscending;
758                                 }
759                         }
760                 }
761         }
765  * @brief Return a sorted state array for use in menu item creation
767  * The array is created by adding the built in states to the user states, then sorting using _statusArraySort
769  * @result A cached NSArray which is sorted by status type (available, away), built-in vs. user-made, and then original ordering.
770  */
771 - (NSArray *)sortedFullStateArray
773         if(!_sortedFullStateArray){
774                 NSArray                 *originalStateArray = [self stateArray];
775                 NSMutableArray  *tempArray = [originalStateArray mutableCopy];
776                 [tempArray addObjectsFromArray:[self builtInStateArray]];
777                 [tempArray addObjectsFromArray:[temporaryStateArray allObjects]];
778                 
779                 //Pass the original array so its indexes can be used for comparison of saved state ordering
780                 [tempArray sortUsingFunction:_statusArraySort context:originalStateArray];
782                 _sortedFullStateArray = tempArray;
783         }
785         return _sortedFullStateArray;
789  * @brief Retrieve active status state
791  * @result The currently active status state.
793  * This is defined as the status state which the most accounts are currently using.  The behavior in case of a tie
794  * is currently undefined but will yield one of the tying states.
795  */
796 - (AIStatus *)activeStatusState
798         if(!_activeStatusState){
799                 NSEnumerator            *enumerator = [[[adium accountController] accountArray] objectEnumerator];
800                 NSCountedSet            *statusCounts = [NSCountedSet set];
801                 AIAccount                       *account;
802                 AIStatus                        *statusState;
803                 unsigned                         highestCount = 0;
804                 BOOL                             accountsAreOnline = [[adium accountController] oneOrMoreConnectedOrConnectingAccounts];
806                 if(accountsAreOnline){
807                         AIStatus        *bestStatusState = nil;
809                         while((account = [enumerator nextObject])) {
810                                 if([account online]){
811                                         AIStatus *accountStatusState = [account statusState];
812                                         [statusCounts addObject:(accountStatusState ?
813                                                                                          accountStatusState :
814                                                                                          [self defaultInitialStatusState])];
815                                 }
816                         }
818                         enumerator = [statusCounts objectEnumerator];
819                         while((statusState = [enumerator nextObject])) {
820                                 unsigned thisCount = [statusCounts countForObject:statusState];
821                                 if(thisCount > highestCount){
822                                         bestStatusState = statusState;
823                                         highestCount = thisCount;
824                                 }
825                         }
827                         _activeStatusState = [bestStatusState retain];
828                 } else {
829                         _activeStatusState = [offlineStatusState retain];
830                 }
831         }
833         return _activeStatusState;
837  * @brief Find the 'active' AIStatusType
839  * The active type is the one used by the largest number of accounts.  In case of a tie, the order of the AIStatusType
840  * enum is respected
842  * @param invisibleIsAway If YES, AIInvisibleStatusType is trated as AIAwayStatusType
843  * @result The active AIStatusType for online accounts, or AIOfflineStatusType if all accounts are  offline
844  */
845 - (AIStatusType)activeStatusTypeTreatingInvisibleAsAway:(BOOL)invisibleIsAway
847         NSEnumerator            *enumerator = [[[adium accountController] accountArray] objectEnumerator];
848         AIAccount                       *account;
849         int                                     statusTypeCount[STATUS_TYPES_COUNT];
850         AIStatusType            activeStatusType = AIOfflineStatusType;
851         unsigned                        highestCount = 0;
853         unsigned i;
854         for(i = 0 ; i < STATUS_TYPES_COUNT ; i++){
855                 statusTypeCount[i] = 0;
856         }
858         while(account = [enumerator nextObject]){
859                 if([account online] || [account integerStatusObjectForKey:@"Connecting"]){
860                         AIStatusType statusType = [[account statusState] statusType];
862                         //If invisibleIsAway, pretend that invisible is away
863                         if (invisibleIsAway && (statusType == AIInvisibleStatusType)) statusType = AIAwayStatusType;
865                         statusTypeCount[statusType]++;
866                 }
867         }
869         for(i = 0 ; i < STATUS_TYPES_COUNT ; i++){
870                 if(statusTypeCount[i] > highestCount){
871                         activeStatusType = i;
872                         highestCount = statusTypeCount[i];
873                 }
874         }
876         return activeStatusType;
880  * @brief All active status states
882  * A status state is active if any online account is currently in that state.
884  * The return value of this method is cached.
886  * @result An <tt>NSSet</tt> of <tt>AIStatus</tt> objects
887  */
888 - (NSSet *)allActiveStatusStates
890         if(!_allActiveStatusStates){
891                 _allActiveStatusStates = [[NSMutableSet alloc] init];
892                 NSEnumerator            *enumerator = [[[adium accountController] accountArray] objectEnumerator];
893                 AIAccount                       *account;
895                 while(account = [enumerator nextObject]){
896                         if([account online] || [account integerStatusObjectForKey:@"Connecting"]){
897                                 [_allActiveStatusStates addObject:[account statusState]];
898                         }
899                 }
900         }
902         return _allActiveStatusStates;
906  * @brief Return the set of all unavailable statuses in use by online or connection accounts
908  * @param activeUnvailableStatusType Pointer to an AIStatusType; returns by reference the most popular unavailable type
909  * @param activeUnvailableStatusName Pointer to an NSString*; returns by reference a status name if all states are in the same name, or nil if they differ
910  * @param allOnlineAccountsAreUnvailable Pointer to a BOOL; returns by reference YES is all online accounts are unavailable, NO if one or more is available
911  */
912 - (NSSet *)activeUnavailableStatusesAndType:(AIStatusType *)activeUnvailableStatusType withName:(NSString **)activeUnvailableStatusName allOnlineAccountsAreUnvailable:(BOOL *)allOnlineAccountsAreUnvailable
914         NSEnumerator            *enumerator = [[[adium accountController] accountArray] objectEnumerator];
915         AIAccount                       *account;
916         NSMutableSet            *activeUnvailableStatuses = [NSMutableSet set];
917         BOOL                            foundStatusName = NO;
918         int                                     statusTypeCount[STATUS_TYPES_COUNT];
920         statusTypeCount[AIAwayStatusType] = 0;
921         statusTypeCount[AIInvisibleStatusType] = 0;
922         
923         //Assume all accounts are unavailable until proven otherwise
924         if(allOnlineAccountsAreUnvailable != NULL){
925                 *allOnlineAccountsAreUnvailable = YES;
926         }
927         
928         while(account = [enumerator nextObject]){
929                 if([account online] || [account integerStatusObjectForKey:@"Connecting"]){
930                         AIStatus        *statusState = [account statusState];
931                         AIStatusType statusType = [statusState statusType];
932                         
933                         if((statusType == AIAwayStatusType) || (statusType == AIInvisibleStatusType)){
934                                 NSString        *statusName = [statusState statusName];
935                                 
936                                 [activeUnvailableStatuses addObject:statusState];
937                                 
938                                 statusTypeCount[statusType]++;
939                                 
940                                 if(foundStatusName){
941                                         //Once we find a status name, we only want to return it if all our status names are the same.
942                                         if((activeUnvailableStatusName != NULL) &&
943                                            (*activeUnvailableStatusName != nil) && 
944                                            ![*activeUnvailableStatusName isEqualToString:statusName]){
945                                                 *activeUnvailableStatusName = nil;
946                                         }
947                                 }else{
948                                         //We haven't found a status name yet, so store this one as the active status name
949                                         if(activeUnvailableStatusName != NULL){
950                                                 *activeUnvailableStatusName = [statusState statusName];
951                                         }
952                                         foundStatusName = YES;
953                                 }
954                         }else{
955                                 //An online account isn't unavailable
956                                 if(allOnlineAccountsAreUnvailable != NULL){
957                                         *allOnlineAccountsAreUnvailable = NO;
958                                 }
959                         }
960                 }
961         }
962         
963         if(activeUnvailableStatusType != NULL){
964                 if(statusTypeCount[AIAwayStatusType] > statusTypeCount[AIInvisibleStatusType]){
965                         *activeUnvailableStatusType = AIAwayStatusType;
966                 }else{
967                         *activeUnvailableStatusType = AIInvisibleStatusType;            
968                 }
969         }
970         
971         return activeUnvailableStatuses;
976  * @brief Next available unique status ID
977  */
978 - (NSNumber *)nextUniqueStatusID
980         NSNumber        *nextUniqueStatusID;
982         //Retain and autorelease since we'll be replacing this value (and therefore releasing it) via the preferenceController.
983         nextUniqueStatusID = [[[[adium preferenceController] preferenceForKey:TOP_STATUS_STATE_ID
984                                                                                                                                   group:PREF_GROUP_SAVED_STATUS] retain] autorelease];
985         if(!nextUniqueStatusID) nextUniqueStatusID = [NSNumber numberWithInt:1];
987         [[adium preferenceController] setPreference:[NSNumber numberWithInt:([nextUniqueStatusID intValue] + 1)]
988                                                                                  forKey:TOP_STATUS_STATE_ID
989                                                                                   group:PREF_GROUP_SAVED_STATUS];
991         return nextUniqueStatusID;
995  * @brief Find the status state with the requested uniqueStatusID
996  */
997 - (AIStatus *)statusStateWithUniqueStatusID:(NSNumber *)uniqueStatusID
999         AIStatus                *statusState = nil;
1001         if(uniqueStatusID){
1002                 NSEnumerator    *enumerator = [[self sortedFullStateArray] objectEnumerator];
1004                 while(statusState = [enumerator nextObject]){
1005                         if([[statusState uniqueStatusID] compare:uniqueStatusID] == NSOrderedSame)
1006                                 break;
1007                 }
1008         }
1010         return statusState;
1013 //State Editing --------------------------------------------------------------------------------------------------------
1014 #pragma mark State Editing
1016  * @brief Add a state
1018  * Add a new state to Adium's state array.
1019  * @param state AIState to add
1020  */
1021 - (void)addStatusState:(AIStatus *)statusState
1023         [stateArray addObject:statusState];
1024         [self _saveStateArrayAndNotifyOfChanges];
1028  * @brief Remove a state
1030  * Remove a new state from Adium's state array.
1031  * @param state AIStatus to remove
1032  */
1033 - (void)removeStatusState:(AIStatus *)statusState
1035         [stateArray removeObject:statusState];
1036         [self _saveStateArrayAndNotifyOfChanges];
1040  * @brief Move a state
1042  * Move a state that already exists in Adium's state array to another index
1043  * @param state AIStatus to move
1044  * @param destIndex Destination index
1045  */
1046 - (int)moveStatusState:(AIStatus *)statusState toIndex:(int)destIndex
1048     int sourceIndex = [stateArray indexOfObjectIdenticalTo:statusState];
1050     //Remove the state
1051     [statusState retain];
1052     [stateArray removeObject:statusState];
1054     //Re-insert the state
1055     if(destIndex > sourceIndex) destIndex -= 1;
1056     [stateArray insertObject:statusState atIndex:destIndex];
1057     [statusState release];
1059         [self _saveStateArrayAndNotifyOfChanges];
1061         return(destIndex);
1065  * @brief Replace a state
1067  * Replace a state in Adium's state array with another state.
1068  * @param oldState AIStatus state that is in Adium's state array
1069  * @param newState AIStatus state with which to replace oldState
1070  */
1071 - (void)replaceExistingStatusState:(AIStatus *)oldStatusState withStatusState:(AIStatus *)newStatusState
1073         if(oldStatusState != newStatusState){
1074                 int index = [stateArray indexOfObject:oldStatusState];
1076                 if(index >= 0 && index < [stateArray count]){
1077                         [stateArray replaceObjectAtIndex:index withObject:newStatusState];
1078                 }
1079         }
1081         [self _saveStateArrayAndNotifyOfChanges];
1085  * @brief Save changes to the state array and notify observers
1087  * Saves any outstanding changes to the state array.  There should be no need to call this manually, since all the
1088  * state array modifying methods in this class call it automatically after making changes.
1090  * After the state array is saved, observers are notified that is has changed.  Call after making any changes to the
1091  * state array from within the controller.
1092  */
1093 - (void)_saveStateArrayAndNotifyOfChanges
1095         //Clear the sorted menu items array since our state array changed.
1096         [_sortedFullStateArray release]; _sortedFullStateArray = nil;
1098         [[adium preferenceController] setPreference:[NSKeyedArchiver archivedDataWithRootObject:[self stateArray]]
1099                                                                                  forKey:KEY_SAVED_STATUS
1100                                                                                   group:PREF_GROUP_SAVED_STATUS];
1101         [[adium notificationCenter] postNotificationName:AIStatusStateArrayChangedNotification object:nil];
1104 - (void)statusStateDidSetUniqueStatusID
1106         [[adium preferenceController] setPreference:[NSKeyedArchiver archivedDataWithRootObject:[self stateArray]]
1107                                                                                  forKey:KEY_SAVED_STATUS
1108                                                                                   group:PREF_GROUP_SAVED_STATUS];
1111 //Machine Activity -----------------------------------------------------------------------------------------------------
1112 #pragma mark Machine Activity
1113 #define MACHINE_IDLE_THRESHOLD                  30      //30 seconds of inactivity is considered idle
1114 #define MACHINE_ACTIVE_POLL_INTERVAL    30      //Poll every 60 seconds when the user is active
1115 #define MACHINE_IDLE_POLL_INTERVAL              1       //Poll every second when the user is idle
1117 //Private idle function
1118 extern double CGSSecondsSinceLastInputEvent(unsigned long evType);
1121  * @brief Returns the current machine idle time
1123  * Returns the current number of seconds the machine has been idle.  The machine is idle when there are no input
1124  * events from the user (such as mouse movement or keyboard input).  In addition to this method, the status controller
1125  * sends out notifications when the machine becomes idle, stays idle, and returns to an active state.
1126  */
1127 - (double)currentMachineIdle
1129     double idleTime = CGSSecondsSinceLastInputEvent(-1);
1131         //On MDD Powermacs, the above function will return a large value when the machine is active (perhaps a -1?).
1132         //Here we check for that value and correctly return a 0 idle time.
1133         if(idleTime >= 18446744000.0) idleTime = 0.0; //18446744073.0 is the lowest I've seen on my MDD -ai
1135     return(idleTime);
1139  * @brief Timer that checkes for machine idle
1141  * This timer periodically checks the machine for inactivity.  When the machine has been inactive for atleast
1142  * MACHINE_IDLE_THRESHOLD seconds, a notification is broadcast.
1144  * When the machine is active, this timer is called infrequently.  It's not important to notice that the user went
1145  * idle immediately, so we relax our CPU usage while waiting for an idle state to begin.
1147  * When the machine is idle, the timer is called frequently.  It's important to notice immediately when the user
1148  * returns.
1149  */
1150 - (void)_idleCheckTimer:(NSTimer *)inTimer
1152         double  currentIdle = [self currentMachineIdle];
1154         if(machineIsIdle){
1155                 if(currentIdle < lastSeenIdle){
1156                         //If the machine is less idle than the last time we recorded, it means that activity has occured and the
1157                         //user is no longer idle.
1158                         [self _setMachineIsIdle:NO];
1159                 }else{
1160                         //Periodically broadcast a 'MachineIdleUpdate' notification
1161                         [[adium notificationCenter] postNotificationName:AIMachineIdleUpdateNotification
1162                                                                                                           object:nil
1163                                                                                                         userInfo:[NSDictionary dictionaryWithObjectsAndKeys:
1164                                                                                                                 [NSNumber numberWithDouble:currentIdle], @"Duration",
1165                                                                                                                 [NSDate dateWithTimeIntervalSinceNow:-currentIdle], @"IdleSince",
1166                                                                                                                 nil]];
1167                 }
1168         }else{
1169                 //If machine inactivity is over the threshold, the user has gone idle.
1170                 if(currentIdle > MACHINE_IDLE_THRESHOLD) [self _setMachineIsIdle:YES];
1171         }
1173         lastSeenIdle = currentIdle;
1177  * @brief Sets the machine as idle or not
1179  * This internal method updates the frequency of our idle timer depending on whether the machine is considered
1180  * idle or not.  It also posts the AIMachineIsIdleNotification and AIMachineIsActiveNotification notifications
1181  * based on the passed idle state
1182  */
1183 - (void)_setMachineIsIdle:(BOOL)inIdle
1185         machineIsIdle = inIdle;
1187         //Post the appropriate idle or active notification
1188         if(machineIsIdle){
1189                 [[adium notificationCenter] postNotificationName:AIMachineIsIdleNotification object:nil];
1190         }else{
1191                 [[adium notificationCenter] postNotificationName:AIMachineIsActiveNotification object:nil];
1192         }
1194         //Update our timer interval for either idle or active polling
1195         [idleTimer invalidate];
1196         [idleTimer release];
1197         idleTimer = [[NSTimer scheduledTimerWithTimeInterval:(machineIsIdle ? MACHINE_IDLE_POLL_INTERVAL : MACHINE_ACTIVE_POLL_INTERVAL)
1198                                                                                                   target:self
1199                                                                                                 selector:@selector(_idleCheckTimer:)
1200                                                                                                 userInfo:nil
1201                                                                                                  repeats:YES] retain];
1205 //Status state menu support ---------------------------------------------------------------------------------------------------
1206 #pragma mark Status state menu support
1208  * @brief Register a state menu plugin
1210  * A state menu plugin is the mitigator between our state menu items and a menu.  As states change the plugin
1211  * is told to add and remove items from the menu.  Everything else is handled by the status controller.
1212  * @param stateMenuPlugin The state menu plugin to register
1213  */
1214 - (void)registerStateMenuPlugin:(id <StateMenuPlugin>)stateMenuPlugin
1216         NSNumber        *identifier = [NSNumber numberWithInt:[stateMenuPlugin hash]];
1218         //Track this plugin
1219         [stateMenuItemArraysDict setObject:[NSMutableArray array] forKey:identifier];
1220         [stateMenuPluginsArray addObject:stateMenuPlugin];
1222         //Start it out with a fresh set of menu items
1223         [self _addStateMenuItemsForPlugin:stateMenuPlugin];
1227  * @brief Unregister a state menu plugin
1229  * All state menu items will be removed from the plugin when it unregisters
1230  * @param stateMenuPlugin The state menu plugin to unregister
1231  */
1232 - (void)unregisterStateMenuPlugin:(id <StateMenuPlugin>)stateMenuPlugin
1234         NSNumber        *identifier = [NSNumber numberWithInt:[stateMenuPlugin hash]];
1236         //Remove all the plugin's menu items
1237         [self _removeStateMenuItemsForPlugin:stateMenuPlugin];
1239         //Stop tracking the plugin
1240         [stateMenuItemArraysDict removeObjectForKey:identifier];
1241         [stateMenuPluginsArray removeObjectIdenticalTo:stateMenuPlugin];
1245  * @brief Remove the status controller's tracking for a plugin's menu items
1247  * This should be called in preparation for one or more plugin:didAddMenuItems: calls to clear out the current
1248  * tracking for the statusController generated menu items.
1249  */
1250 - (void)removeAllMenuItemsForPlugin:(id <StateMenuPlugin>)stateMenuPlugin
1252         NSNumber                *identifier = [NSNumber numberWithInt:[stateMenuPlugin hash]];
1253         NSMutableArray  *menuItemArray = [stateMenuItemArraysDict objectForKey:identifier];
1255         //Clear the array
1256         [menuItemArray removeAllObjects];
1260  * @brief A plugin created its own menu items it wants us to track and update
1261  */
1262 - (void)plugin:(id <StateMenuPlugin>)stateMenuPlugin didAddMenuItems:(NSArray *)addedMenuItems
1264         NSNumber                *identifier = [NSNumber numberWithInt:[stateMenuPlugin hash]];
1265         NSMutableArray  *menuItemArray = [stateMenuItemArraysDict objectForKey:identifier];
1267         [menuItemArray addObjectsFromArray:addedMenuItems];
1268         [stateMenuItemsNeedingUpdating  addObjectsFromArray:addedMenuItems];
1272  * @brief Add state menu items
1274  * Adds all the necessary state menu items to a plugin's state menu
1275  * @param stateMenuPlugin The state menu plugin we're updating
1276  */
1277 - (void)_addStateMenuItemsForPlugin:(id <StateMenuPlugin>)stateMenuPlugin
1279         NSNumber                *identifier = [NSNumber numberWithInt:[stateMenuPlugin hash]];
1280         NSMutableArray  *menuItemArray = [stateMenuItemArraysDict objectForKey:identifier];
1281         NSEnumerator    *enumerator;
1282         NSMenuItem              *menuItem;
1283         AIStatus                *statusState;
1284         AIStatusType    currentStatusType = AIAvailableStatusType;
1286         //Create a menu item for each state.  States must first be sorted such that states of the same AIStatusType
1287         //are grouped together.
1288         enumerator = [[self sortedFullStateArray] objectEnumerator];
1289         while(statusState = [enumerator nextObject]){
1290                 AIStatusType thisStatusType = [statusState statusType];
1292                 //We treat Invisible statuses as being the same as Away for purposes of the menu
1293                 if(thisStatusType == AIInvisibleStatusType) thisStatusType = AIAwayStatusType;
1295                 //Add the "Custom..." state option and a separatorItem before beginning to add items for a new statusType
1296                 if((currentStatusType != thisStatusType) &&
1297                    (currentStatusType != AIOfflineStatusType)){
1298                         menuItem = [[NSMenuItem alloc] initWithTitle:STATUS_TITLE_CUSTOM
1299                                                                                                   target:self
1300                                                                                                   action:@selector(selectCustomState:)
1301                                                                                    keyEquivalent:@""];
1303                         [menuItem setImage:[[[AIStatusIcons statusIconForStatusName:nil
1304                                                                                                                          statusType:currentStatusType
1305                                                                                                                            iconType:AIStatusIconList
1306                                                                                                                           direction:AIIconNormal] copy] autorelease]];
1307                         [menuItem setTag:currentStatusType];
1308                         [menuItemArray addObject:menuItem];
1309                         [menuItem release];
1311                         //Add a divider
1312                         [menuItemArray addObject:[NSMenuItem separatorItem]];
1314                         currentStatusType = thisStatusType;
1315                 }
1317                 menuItem = [[NSMenuItem alloc] initWithTitle:[self _titleForMenuDisplayOfState:statusState]
1318                                                                                           target:self
1319                                                                                           action:@selector(selectState:)
1320                                                                            keyEquivalent:@""];
1322                 //NSMenuItem will call setFlipped: on the image we pass it, causing flipped drawing elsewhere if we pass it the
1323                 //shared status icon.  So we pass it a copy of the shared icon that it's free to manipulate.
1324                 [menuItem setImage:[[[statusState icon] copy] autorelease]];
1325                 [menuItem setTag:currentStatusType];
1326                 if([NSApp isOnPantherOrBetter]){
1327                         [menuItem setToolTip:[statusState statusMessageString]];
1328                 }
1329                 [menuItem setRepresentedObject:[NSDictionary dictionaryWithObject:statusState
1330                                                                                                                                    forKey:@"AIStatus"]];
1331                 [menuItemArray addObject:menuItem];
1332                 [menuItem release];
1333         }
1335         if(currentStatusType != AIOfflineStatusType){
1336                 /* Add the last "Custom..." state optior for the last statusType we handled,
1337                  * which didn't get a "Custom..." item yet.  At present, our last status type should always be
1338                  * our AIOfflineStatusType, so this will never be executed and just exists for completeness. */
1339                 menuItem = [[NSMenuItem alloc] initWithTitle:STATUS_TITLE_CUSTOM
1340                                                                                           target:self
1341                                                                                           action:@selector(selectCustomState:)
1342                                                                            keyEquivalent:@""];
1343                 [menuItem setImage:[[[AIStatusIcons statusIconForStatusName:nil
1344                                                                                                                  statusType:currentStatusType
1345                                                                                                                    iconType:AIStatusIconList
1346                                                                                                                   direction:AIIconNormal] copy] autorelease]];
1347                 [menuItem setTag:currentStatusType];
1348                 [menuItemArray addObject:menuItem];
1349                 [menuItem release];
1350         }
1352         //Now that we are done creating the menu items, tell the plugin about them
1353         [stateMenuPlugin addStateMenuItems:menuItemArray];
1355         //Update the selected menu item after giving the plugin a chance to do with the menu items as it wants
1356         [self updateStateMenuSelectionForPlugin:stateMenuPlugin];
1360  * @brief Removes state menu items
1362  * Removes all the state menu items from a plugin's state menu
1363  * @param stateMenuPlugin The state menu plugin we're updating
1364  */
1365 - (void)_removeStateMenuItemsForPlugin:(id <StateMenuPlugin>)stateMenuPlugin
1367         NSNumber                *identifier = [NSNumber numberWithInt:[stateMenuPlugin hash]];
1368         NSMutableArray  *menuItemArray = [stateMenuItemArraysDict objectForKey:identifier];
1370         //Inform the plugin that we are removing the items in this array
1371         [stateMenuPlugin removeStateMenuItems:menuItemArray];
1373         //Now clear the array
1374         [menuItemArray removeAllObjects];
1378  * @brief Completely rebuild all state menus
1380  * Before doing so, clear the activeStatusState, which will be regenerated when next needed
1381  */
1382 - (void)rebuildAllStateMenus
1384         //Clear the sorted menu items array since our state array changed.
1385         [_sortedFullStateArray release]; _sortedFullStateArray = nil;
1387         [self _resetActiveStatusState];
1389         NSEnumerator                    *enumerator = [stateMenuPluginsArray objectEnumerator];
1390         id <StateMenuPlugin>    stateMenuPlugin;
1392         while(stateMenuPlugin = [enumerator nextObject]) {
1393                 [self _removeStateMenuItemsForPlugin:stateMenuPlugin];
1394                 [self _addStateMenuItemsForPlugin:stateMenuPlugin];
1395         }
1399  * @brief Completely rebuild all state menus for a single plugin
1400  */
1401 - (void)rebuildAllStateMenusForPlugin:(id <StateMenuPlugin>)stateMenuPlugin
1403         [self _removeStateMenuItemsForPlugin:stateMenuPlugin];
1404         [self _addStateMenuItemsForPlugin:stateMenuPlugin];
1408  * @brief Update the selected state in all state menus
1409  */
1410 - (void)updateAllStateMenuSelections
1412         if(stateMenuUpdateDelays == 0){
1413                 NSEnumerator                    *enumerator = [stateMenuPluginsArray objectEnumerator];
1414                 id <StateMenuPlugin>    stateMenuPlugin;
1416                 while(stateMenuPlugin = [enumerator nextObject]){
1417                         [self updateStateMenuSelectionForPlugin:stateMenuPlugin];
1418                 }
1419                 
1420                 [self _resetActiveStatusState];
1421                 
1422                 /* Let any relevant plugins respond to the to-be-changed state menu selection. Technically we
1423                  * haven't changed it yet, since we'll do that in validateMenuItem:, but the fact that we will now
1424                  * need to change it is useful if, for example, key equivalents change in a menu alongside selection
1425                  * changes, since we need key equivalents set immediately.
1426                  */
1427                 [[adium notificationCenter] postNotificationName:AIStatusStateMenuSelectionsChangedNotification
1428                                                                                                   object:nil];
1429         }
1433  * @brief Delay state menu updates
1435  * This should be called to prevent duplicative updates when multiple accounts are changing status simultaneously.
1436  */
1437 - (void)setDelayStateMenuUpdates:(BOOL)shouldDelay
1439         if(shouldDelay)
1440                 stateMenuUpdateDelays++;
1441         else
1442                 stateMenuUpdateDelays--;
1444         if(stateMenuUpdateDelays == 0){
1445                 [self updateAllStateMenuSelections];
1446         }
1450  * @brief Update the selected state in a plugin's state menu
1452  * Updates the selected state menu item to reflect the currently active state.
1453  * @param stateMenuPlugin The state menu plugin we're updating
1454  */
1455 - (void)updateStateMenuSelectionForPlugin:(id <StateMenuPlugin>)stateMenuPlugin
1457         NSNumber                *identifier = [NSNumber numberWithInt:[stateMenuPlugin hash]];
1458         NSArray                 *stateMenuItemArray = [stateMenuItemArraysDict objectForKey:identifier];
1459         [stateMenuItemsNeedingUpdating addObjectsFromArray:stateMenuItemArray];
1464  * @brief Account status changed.
1466  * Rebuild all our state menus
1467  */
1468 - (NSSet *)updateListObject:(AIListObject *)inObject keys:(NSSet *)inModifiedKeys silent:(BOOL)silent
1470         if([inObject isKindOfClass:[AIAccount class]]){
1471                 if([inModifiedKeys containsObject:@"Online"] ||
1472                    [inModifiedKeys containsObject:@"IdleSince"] ||
1473                    [inModifiedKeys containsObject:@"StatusState"]){
1475                         //Don't update the state menus if we are currently delaying
1476                         if(stateMenuUpdateDelays == 0) [self updateAllStateMenuSelections];
1478                         //We can get here without the preferencesChanged: notification if the account is automatically connected.
1479                         if([inModifiedKeys containsObject:@"Online"]){
1480                                 if([inObject online]) [accountsToConnect addObject:inObject];
1481                         }
1482                 }
1483         }
1485     return(nil);
1489  * @brief Preferences changed; update our accountsToConnect tracking set
1491  * We use the preferences changed notifications rather than the statusObject notifications because the statusObject
1492  * may not change immediately upon requesting a connect or disconnect, since the account may wait to receive confirmation
1493  * before reporting itself as online or offline.  With the preferences changed notification, we can distinguish a user
1494  * disconnect from selecting the global Offline menu item.
1495  */
1496 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key
1497                                                         object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
1499         if([key isEqualToString:@"Online"]){
1500                 /* Track the accounts we should connect when setting to an online state.  Our goal is to be able to reconnect
1501                 * the most recently connected account if accounts are disconnected one-by-one.  If accounts are disconnected
1502                 * all at once via the global Offline menu item, we want to restore all of the previously connected accounts when
1503                 * reconnecting, so we check to see if we are disconnecting via that menu item with the
1504                 * isProcessingGlobalChange BOOL. */
1505                 if(!isProcessingGlobalChange){
1506                         if([[object preferenceForKey:@"Online" group:GROUP_ACCOUNT_STATUS] boolValue]){
1507                                 [accountsToConnect addObject:object];
1508                         }else{
1509                                 if([accountsToConnect count] > 1){
1510                                         [accountsToConnect removeObject:object];
1511                                 }
1512                         }
1513                 }
1515                 //Clear these caches now. Observers which get called before we do when an account actually connects
1516                 //will want to get a fresh value.
1517                 [self _resetActiveStatusState];
1518         }
1522  * @brief Menu validation
1524  * Our state menu items should always be active, so always return YES for validation.
1526  * Here we lazily set the state of our menu items if our stateMenuItemsNeedingUpdating set indicates it is needed.
1527  */
1528 - (BOOL)validateMenuItem:(id <NSMenuItem>)menuItem
1530         if([stateMenuItemsNeedingUpdating containsObject:menuItem]){
1531                 BOOL                    noAccountsAreOnline = ![[adium accountController] oneOrMoreConnectedAccounts];
1532                 NSDictionary    *dict = [menuItem representedObject];
1533                 AIAccount               *account;
1534                 AIStatus                *menuItemStatusState;
1535                 BOOL                    shouldSelectOffline;
1537                 //Search for the account or global status state as appropriate for this menu item.
1538                 //Also, determine if we are looking to select the Offline menu item
1539                 if(account = [dict objectForKey:@"AIAccount"]){
1540                         shouldSelectOffline = ![account online];
1541                 }else{
1542                         shouldSelectOffline = noAccountsAreOnline;
1543                 }
1545                 menuItemStatusState = [dict objectForKey:@"AIStatus"];
1547                 if(shouldSelectOffline){
1548                         //If we should select offline, set all menu items which don't have the AIOfflineStatusType tag to be off.
1549                         if([menuItem tag] == AIOfflineStatusType){
1550                                 if([menuItem state] != NSOnState) [menuItem setState:NSOnState];
1551                         }else{
1552                                 if([menuItem state] != NSOffState) [menuItem setState:NSOffState];
1553                         }
1555                 }else{
1556                         if(account){
1557                                 /* Account-specific menu items */
1558                                 AIStatus                *appropiateActiveStatusState;
1559                                 appropiateActiveStatusState = [account statusState];
1561                                 /* Our "Custom..." menu choice has a nil represented object.  If the appropriate active search state is
1562                                         * in our array of states from which we made menu items, we'll be searching to match it.  If it isn't,
1563                                         * we have a custom state and will be searching for the custom item of the right type, switching all other
1564                                         * menu items to NSOffState. */
1565                                 if([[self sortedFullStateArray] containsObjectIdenticalTo:appropiateActiveStatusState]){
1566                                         //If the search state is in the array so is a saved state, search for the match
1567                                         if(menuItemStatusState == appropiateActiveStatusState){
1568                                                 if([menuItem state] != NSOnState) [menuItem setState:NSOnState];
1569                                         }else{
1570                                                 if([menuItem state] != NSOffState) [menuItem setState:NSOffState];
1571                                         }
1572                                 }else{
1573                                         //If there is not a status state, we are in a Custom state. Search for the correct Custom item.
1574                                         if(menuItemStatusState){
1575                                                 //If the menu item has an associated state, it's always off.
1576                                                 if([menuItem state] != NSOffState) [menuItem setState:NSOffState];
1577                                         }else{
1578                                                 //If it doesn't, check the tag to see if it should be on or off.
1579                                                 if([menuItem tag] == [appropiateActiveStatusState statusType]){
1580                                                         if([menuItem state] != NSOnState) [menuItem setState:NSOnState];
1581                                                 }else{
1582                                                         if([menuItem state] != NSOffState) [menuItem setState:NSOffState];
1583                                                 }
1584                                         }
1585                                 }
1586                         }else{
1587                                 /* General menu items */
1588                                 NSSet   *allActiveStatusStates = [self allActiveStatusStates];
1589                                 int             onState = (([allActiveStatusStates count] == 1) ? NSOnState : NSMixedState);
1591                                 if(menuItemStatusState){
1592                                         //If this menu item has a status state, set it to the right on state if that state is active
1593                                         if([allActiveStatusStates containsObject:menuItemStatusState]){
1594                                                 if([menuItem state] != onState) [menuItem setState:onState];
1595                                         }else{
1596                                                 if([menuItem state] != NSOffState) [menuItem setState:NSOffState];
1597                                         }
1598                                 }else{
1599                                         //If it doesn't, check the tag to see if it should be on or off by looking for a matching custom state
1600                                         NSEnumerator    *activeStatusStatesEnumerator = [allActiveStatusStates objectEnumerator];
1601                                         NSArray                 *sortedFullStateArray = [self sortedFullStateArray];
1602                                         AIStatus                *statusState;
1603                                         BOOL                    foundCorrectStatusState = NO;
1605                                         while(!foundCorrectStatusState && (statusState = [activeStatusStatesEnumerator nextObject])){
1606                                                 //We found a custom match if our array of menu item states doesn't contain this state and
1607                                                 //its statusType matches the menuItem's tag.
1608                                                 foundCorrectStatusState = (![sortedFullStateArray containsObjectIdenticalTo:statusState] &&
1609                                                                                                    ([menuItem tag] == [statusState statusType]));
1610                                         }
1612                                         if(foundCorrectStatusState){
1613                                                 if([menuItem state] != NSOnState) [menuItem setState:onState];
1614                                         }else{
1615                                                 if([menuItem state] != NSOffState) [menuItem setState:NSOffState];
1616                                         }
1617                                 }
1618                         }
1619                 }
1621                 [stateMenuItemsNeedingUpdating removeObject:menuItem];
1622         }
1624         return(YES);
1628  * @brief Select a state menu item
1630  * Invoked by a state menu item, sets the state corresponding to the menu item as the active state.
1632  * If the representedObject NSDictionary has an @"AIAccount" object, set the state just for the appropriate AIAccount.
1633  * Otherwise, set the state globally.
1634  */
1635 - (void)selectState:(id)sender
1637         NSDictionary    *dict = [sender representedObject];
1638         AIStatus                *statusState = [dict objectForKey:@"AIStatus"];
1639         AIAccount               *account = [dict objectForKey:@"AIAccount"];
1641         /* Random undocumented feature of the moment... hold option and select a state to bring up the custom status window
1642          * for modifying and then setting it.
1643          */
1644         if([NSEvent optionKey]){
1645                 [AIEditStateWindowController editCustomState:statusState
1646                                                                                          forType:[statusState statusType]
1647                                                                                   andAccount:account
1648                                                                           withSaveOption:YES
1649                                                                                         onWindow:nil
1650                                                                          notifyingTarget:self];
1652         }else{
1653                 if(account){
1654                         BOOL shouldRebuild;
1655                         
1656                         shouldRebuild = [self removeIfNecessaryTemporaryStatusState:[account statusState]];
1657                         [account setStatusState:statusState];
1658                         
1659                         if(shouldRebuild){
1660                                 //Rebuild our menus if there was a change
1661                                 [self rebuildAllStateMenus];
1662                         }
1663                         
1664                 }else{
1665                         [self setActiveStatusState:statusState];
1666                 }
1667         }
1671  * @brief Select the custom state menu item
1673  * Invoked by the custom state menu item, opens a custom state window.
1674  * If the representedObject NSDictionary has an @"AIAccount" object, configure just for the appropriate AIAccount.
1675  * Otherwise, configure globally.
1676  */
1677 - (IBAction)selectCustomState:(id)sender
1679         NSDictionary    *dict = [sender representedObject];
1680         AIAccount               *account = [dict objectForKey:@"AIAccount"];
1681         AIStatusType    statusType = [sender tag];
1682         AIStatus                *baseStatusState;
1684         if(account){
1685                 baseStatusState = [account statusState];
1686         }else{
1687                 baseStatusState = [self activeStatusState];
1688         }
1690         /* If we are going to a custom state of a different type, we don't want to prefill with baseStatusState as it stands.
1691          * Instead, we load the last used status of that type. */
1692         if(([baseStatusState statusType] != statusType)){
1693                 NSDictionary *lastStatusStates = [[adium preferenceController] preferenceForKey:@"LastStatusStates"
1694                                                                                                                                                                   group:PREF_GROUP_STATUS_PREFERENCES];
1696                 NSData          *lastStatusStateData = [lastStatusStates objectForKey:[NSNumber numberWithInt:statusType]];
1697                 AIStatus        *lastStatusStateOfThisType = (lastStatusStateData ?
1698                                                                                                   [NSKeyedUnarchiver unarchiveObjectWithData:lastStatusStateData] :
1699                                                                                                   nil);
1701                 baseStatusState = [[lastStatusStateOfThisType retain] autorelease];
1702         }
1704         //don't use the current status state as a base.  Going from Away to Available, don't autofill the Available
1705         //status message with the old away message.
1706         if([baseStatusState statusType] != statusType){
1707                 baseStatusState = nil;
1708         }
1710         [AIEditStateWindowController editCustomState:baseStatusState
1711                                                                                  forType:statusType
1712                                                                           andAccount:account
1713                                                                   withSaveOption:YES
1714                                                                                 onWindow:nil
1715                                                                  notifyingTarget:self];
1719  * @brief Called when a state could potentially need to removed from the temporary (non-saved) list
1721  * If originalState is in the temporary status array, and it is being used on one or zero accounts, it 
1722  * is removed from the temporary status array. This method should be used when one or more accounts have stopped
1723  * using a single status state to determine if that status state is both non-saved and unused.
1725  * @result YES if the state was removed
1726  */
1727 - (BOOL)removeIfNecessaryTemporaryStatusState:(AIStatus *)originalState
1729         BOOL didRemove = NO;
1730         
1731         /* If the original (old) status state is in our temporary array and is not being used in more than 1 account
1732         * we should remove it */
1733         if([temporaryStateArray containsObject:originalState]){
1734                 NSEnumerator    *enumerator;
1735                 AIAccount               *account;
1736                 int                             count = 0;
1737                 
1738                 enumerator = [[[adium accountController] accountArray] objectEnumerator];
1739                 while(account = [enumerator nextObject]){
1740                         if([account actualStatusState] == originalState){
1741                                 if(++count > 1) break;
1742                         }
1743                 }
1745                 if(count <= 1){
1746                         [temporaryStateArray removeObject:originalState];
1747                         didRemove = YES;
1748                 }
1749         }
1750         
1751         return didRemove;
1754  * @brief Apply a custom state
1756  * Invoked when the custom state window is closed by the user clicking OK.  In response this method sets the custom
1757  * state as the active state.
1758  */
1759 - (void)customStatusState:(AIStatus *)originalState changedTo:(AIStatus *)newState forAccount:(AIAccount *)account
1761         BOOL shouldRebuild = NO;
1762         
1763         if(account){
1764                 shouldRebuild = [self removeIfNecessaryTemporaryStatusState:originalState];
1766                 //Now set the newState for the account
1767                 [account setStatusState:newState];
1768                 
1769         }else{
1770                 //Set the state for all accounts.  This will clear out the temporaryStatusArray as necessary.
1771                 [self setActiveStatusState:newState];
1772         }
1774         if([newState mutabilityType] != AITemporaryEditableStatusState){
1775                 [[adium statusController] addStatusState:newState];
1776         }
1778         NSMutableDictionary *lastStatusStates;
1780         lastStatusStates = [[[adium preferenceController] preferenceForKey:@"LastStatusStates"
1781                                                                                                                                  group:PREF_GROUP_STATUS_PREFERENCES] mutableCopy];
1782         if(!lastStatusStates) lastStatusStates = [NSMutableDictionary dictionary];
1784         [lastStatusStates setObject:[NSKeyedArchiver archivedDataWithRootObject:newState]
1785                                                  forKey:[NSNumber numberWithInt:[newState statusType]]];
1787         [[adium preferenceController] setPreference:lastStatusStates
1788                                                                                  forKey:@"LastStatusStates"
1789                                                                                   group:PREF_GROUP_STATUS_PREFERENCES];
1791         //Add to our temporary status array if it's not in our state array
1792         if(shouldRebuild || (![[self stateArray] containsObjectIdenticalTo:newState])){
1793                 [temporaryStateArray addObject:newState];
1795                 //Now rebuild our menus to include this temporary item
1796                 [self rebuildAllStateMenus];
1797         }
1801  * @brief Determine a string to use as a menu title
1803  * This method truncates a state title string for display as a menu item.
1804  * Wide menus aren't pretty and may cause crashing in certain versions of OS X, so all state
1805  * titles should be run through this method before being used as menu item titles.
1807  * @param statusState The state for which we want a title
1809  * @result An appropriate NSString title
1810  */
1811 - (NSString *)_titleForMenuDisplayOfState:(AIStatus *)statusState
1813         NSString        *title = [statusState title];
1815         if([title length] > STATE_TITLE_MENU_LENGTH){
1816                 title = [title stringWithEllipsisByTruncatingToLength:STATE_TITLE_MENU_LENGTH];
1817         }
1819         return(title);
1823  * @brief Create and add the built-in status types
1825  * The built-in status types are basic, generic "Available" and "Away" states.
1826  */
1827 - (void)buildBuiltInStatusTypes
1829         NSDictionary    *statusDict;
1831         builtInStatusTypes[AIAvailableStatusType] = [[NSMutableSet alloc] init];
1832         statusDict = [NSDictionary dictionaryWithObjectsAndKeys:
1833                         STATUS_NAME_AVAILABLE, KEY_STATUS_NAME,
1834                         STATUS_DESCRIPTION_AVAILABLE, KEY_STATUS_DESCRIPTION,
1835                         [NSNumber numberWithInt:AIAvailableStatusType], KEY_STATUS_TYPE,
1836                         nil];
1837         [builtInStatusTypes[AIAvailableStatusType] addObject:statusDict];
1839         builtInStatusTypes[AIAwayStatusType] = [[NSMutableSet alloc] init];
1840         statusDict = [NSDictionary dictionaryWithObjectsAndKeys:
1841                 STATUS_NAME_AWAY, KEY_STATUS_NAME,
1842                 STATUS_DESCRIPTION_AWAY, KEY_STATUS_DESCRIPTION,
1843                 [NSNumber numberWithInt:AIAwayStatusType], KEY_STATUS_TYPE,
1844                 nil];
1845         [builtInStatusTypes[AIAwayStatusType] addObject:statusDict];
1848 - (NSMenu *)statusStatesMenu
1850         NSMenu                  *statusStatesMenu = [[NSMenu allocWithZone:[NSMenu menuZone]] init];
1851         NSEnumerator    *enumerator;
1852         AIStatus                *statusState;
1853         AIStatusType    currentStatusType = AIAvailableStatusType;
1854         NSMenuItem              *menuItem;
1856         [statusStatesMenu setMenuChangedMessagesEnabled:NO];
1857         [statusStatesMenu setAutoenablesItems:NO];
1859         //Create a menu item for each state.  States must first be sorted such that states of the same AIStatusType
1860         //are grouped together.
1861         enumerator = [[self sortedFullStateArray] objectEnumerator];
1862         while(statusState = [enumerator nextObject]){
1863                 AIStatusType thisStatusType = [statusState statusType];
1865                 if(currentStatusType != thisStatusType){
1866                         //Add a divider between each type of status
1867                         [statusStatesMenu addItem:[NSMenuItem separatorItem]];
1868                         currentStatusType = thisStatusType;
1869                 }
1871                 menuItem = [[NSMenuItem alloc] initWithTitle:[self _titleForMenuDisplayOfState:statusState]
1872                                                                                           target:nil
1873                                                                                           action:nil
1874                                                                            keyEquivalent:@""];
1876                 //NSMenuItem will call setFlipped: on the image we pass it, causing flipped drawing elsewhere if we pass it the
1877                 //shared status icon.  So we pass it a copy of the shared icon that it's free to manipulate.
1878                 [menuItem setImage:[[[statusState icon] copy] autorelease]];
1879                 [menuItem setRepresentedObject:[NSDictionary dictionaryWithObject:statusState
1880                                                                                                                                    forKey:@"AIStatus"]];
1881                 [menuItem setTag:[statusState statusType]];
1882                 [statusStatesMenu addItem:menuItem];
1883                 [menuItem release];
1884         }
1886         [statusStatesMenu setMenuChangedMessagesEnabled:YES];
1888         return statusStatesMenu;
1891 #pragma mark Upgrade code
1893  * @brief Temporary upgrade code for 0.7x -> 0.8
1895  * Versions 0.7x and prior stored their away messages in a different format.  This code allows a seamless
1896  * transition from 0.7x to 0.8.  We can easily recognize the old format because the away messages are of
1897  * type "Away" instead of type "State", which is used for all 0.8 and later saved states.
1898  * Since we are changing the array as we scan it, an enumerator will not work here.
1899  */
1900 #define OLD_KEY_SAVED_AWAYS                     @"Saved Away Messages"
1901 #define OLD_GROUP_AWAY_MESSAGES         @"Away Messages"
1902 #define OLD_STATE_SAVED_AWAY            @"Away"
1903 #define OLD_STATE_AWAY                          @"Message"
1904 #define OLD_STATE_AUTO_REPLY            @"Autoresponse"
1905 #define OLD_STATE_TITLE                         @"Title"
1906 - (void)_upgradeSavedAwaysToSavedStates
1908         NSArray *savedAways = [[adium preferenceController] preferenceForKey:OLD_KEY_SAVED_AWAYS
1909                                                                                                                                    group:OLD_GROUP_AWAY_MESSAGES];
1911         if(savedAways){
1912                 NSEnumerator    *enumerator = [savedAways objectEnumerator];
1913                 NSDictionary    *state;
1915                 //Update all the away messages to states.
1916                 while(state = [enumerator nextObject]){
1917                         if([[state objectForKey:@"Type"] isEqualToString:OLD_STATE_SAVED_AWAY]){
1918                                 AIStatus        *statusState;
1920                                 //Extract the away message information from this old record
1921                                 NSData          *statusMessageData = [state objectForKey:OLD_STATE_AWAY];
1922                                 NSData          *autoReplyMessageData = [state objectForKey:OLD_STATE_AUTO_REPLY];
1923                                 NSString        *title = [state objectForKey:OLD_STATE_TITLE];
1925                                 //Create an AIStatus from this information
1926                                 statusState = [AIStatus status];
1928                                 //General category: It's an away type
1929                                 [statusState setStatusType:AIAwayStatusType];
1931                                 //Specific state: It's the generic away. Funny how that works out.
1932                                 [statusState setStatusName:STATUS_NAME_AWAY];
1934                                 //Set the status message (which is just the away message).
1935                                 [statusState setStatusMessage:[NSAttributedString stringWithData:statusMessageData]];
1937                                 //It has an auto reply.
1938                                 [statusState setHasAutoReply:YES];
1940                                 if(autoReplyMessageData){
1941                                         //Use the custom auto reply if it was set.
1942                                         [statusState setAutoReply:[NSAttributedString stringWithData:autoReplyMessageData]];
1943                                 }else{
1944                                         //If no autoReplyMesssage, use the status message.
1945                                         [statusState setAutoReplyIsStatusMessage:YES];
1946                                 }
1948                                 if(title) [statusState setTitle:title];
1950                                 //Add the updated state to our state array.
1951                                 [stateArray addObject:statusState];
1952                         }
1953                 }
1955                 //Save these changes and delete the old aways so we don't need to do this again.
1956                 [self _saveStateArrayAndNotifyOfChanges];
1957                 [[adium preferenceController] setPreference:nil
1958                                                                                          forKey:OLD_KEY_SAVED_AWAYS
1959                                                                                           group:OLD_GROUP_AWAY_MESSAGES];
1960         }
1963 @end