Merged [14244]: Small but vital blocking fix from Kiel Gillard:
[adiumx.git] / Source / AIStatusController.m
blob7d03bc46800f16b8808b0107a86d408400771c02
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
463 - (NSString *)localizedDescriptionForCoreStatusName:(NSString *)statusName
465         static NSDictionary     *coreLocalizedStatusDescriptions = nil;
466         if(!coreLocalizedStatusDescriptions){
467                 coreLocalizedStatusDescriptions = [[NSDictionary dictionaryWithObjectsAndKeys:
468                         STATUS_DESCRIPTION_AVAILABLE, STATUS_NAME_AVAILABLE,
469                         STATUS_DESCRIPTION_FREE_FOR_CHAT, STATUS_NAME_FREE_FOR_CHAT,
470                         STATUS_DESCRIPTION_AVAILABLE_FRIENDS_ONLY, STATUS_NAME_AVAILABLE_FRIENDS_ONLY,
471                         STATUS_DESCRIPTION_AWAY, STATUS_NAME_AWAY,
472                         STATUS_DESCRIPTION_EXTENDED_AWAY, STATUS_NAME_EXTENDED_AWAY,
473                         STATUS_DESCRIPTION_AWAY_FRIENDS_ONLY, STATUS_NAME_AWAY_FRIENDS_ONLY,
474                         STATUS_DESCRIPTION_DND, STATUS_NAME_DND,
475                         STATUS_DESCRIPTION_NOT_AVAILABLE, STATUS_NAME_NOT_AVAILABLE,
476                         STATUS_DESCRIPTION_OCCUPIED, STATUS_NAME_OCCUPIED,
477                         STATUS_DESCRIPTION_BRB, STATUS_NAME_BRB,
478                         STATUS_DESCRIPTION_BUSY, STATUS_NAME_BUSY,
479                         STATUS_DESCRIPTION_PHONE, STATUS_NAME_PHONE,
480                         STATUS_DESCRIPTION_LUNCH, STATUS_NAME_LUNCH,
481                         STATUS_DESCRIPTION_NOT_AT_HOME, STATUS_NAME_NOT_AT_HOME,
482                         STATUS_DESCRIPTION_NOT_AT_DESK, STATUS_NAME_NOT_AT_DESK,
483                         STATUS_DESCRIPTION_NOT_IN_OFFICE, STATUS_NAME_NOT_IN_OFFICE,
484                         STATUS_DESCRIPTION_VACATION, STATUS_NAME_VACATION,
485                         STATUS_DESCRIPTION_STEPPED_OUT, STATUS_NAME_STEPPED_OUT,
486                         STATUS_DESCRIPTION_INVISIBLE, STATUS_NAME_INVISIBLE,
487                         STATUS_DESCRIPTION_OFFLINE, STATUS_NAME_OFFLINE,
488                         nil] retain];
489         }
491         return (statusName ? [coreLocalizedStatusDescriptions objectForKey:statusName] : nil);
494 - (NSString *)localizedDescriptionForStatusName:(NSString *)statusName statusType:(AIStatusType)statusType
496         NSString *description = nil;
498         if (statusName &&
499                 !(description = [self localizedDescriptionForCoreStatusName:statusName])) {
500                 NSEnumerator    *enumerator = [statusDictsByServiceCodeUniqueID[statusType] objectEnumerator];
501                 NSSet                   *set;
502                 
503                 while (!description && (set = [enumerator nextObject])) {
504                         NSEnumerator    *statusDictsEnumerator = [set objectEnumerator];
505                         NSDictionary    *statusDict;
506                         while (!description && (statusDict = [statusDictsEnumerator nextObject])) {
507                                 if ([[statusDict objectForKey:KEY_STATUS_NAME] isEqualToString:statusName]){
508                                         description = [statusDict objectForKey:KEY_STATUS_DESCRIPTION];
509                                 }
510                         }
511                 }               
512         }
514         return description;
518 * @brief Return the localized description for the sate of the passed status
520  * This could be stored with the statusState, but that would break if the locale changed.  This way, the nonlocalized
521  * string is used to look up the appropriate localized one.
523  * @result A localized description such as @"Away" or @"Out to Lunch" of the state used by statusState
524  */
525 - (NSString *)descriptionForStateOfStatus:(AIStatus *)statusState
527         return [self localizedDescriptionForStatusName:[statusState statusName]
528                                                                                 statusType:[statusState statusType]];
532  * @brief The status name to use by default for a passed type
534  * This is the name which will be used for new AIStatus objects of this type.
535  */
536 - (NSString *)defaultStatusNameForType:(AIStatusType)statusType
538         //Set the default status name
539         switch(statusType){
540                 case AIAvailableStatusType:
541                         return STATUS_NAME_AVAILABLE;
542                         break;
543                 case AIAwayStatusType:
544                         return STATUS_NAME_AWAY;
545                         break;
546                 case AIInvisibleStatusType:
547                         return STATUS_NAME_INVISIBLE;
548                         break;
549                 case AIOfflineStatusType:
550                         return STATUS_NAME_OFFLINE;
551                         break;
552         }
554         return nil;
557 #pragma mark Setting Status States
559  * @brief Set the active status state
561  * Sets the currently active status state.  This applies throughout Adium and to all accounts.  The state will become
562  * effective immediately.
563  */
564 - (void)setActiveStatusState:(AIStatus *)statusState
566         //Apply the state to our accounts and notify (delay to the next run loop to improve perceived speed)
567         [self performSelector:@selector(applyState:toAccounts:)
568                            withObject:statusState
569                            withObject:[[adium accountController] accountArray]
570                            afterDelay:0];
574  * @brief Return the <tt>AIStatus</tt> to be used by accounts as they are created
575  */
576 - (AIStatus *)defaultInitialStatusState
578         return [[self builtInStateArray] objectAtIndex:0];
582  * @brief Reset the active status state
584  * All active status states cache will also reset.  Posts an active status changed notification.  The active state
585  * will be regenerated the next time it is requested.
586  */
587 - (void)_resetActiveStatusState
589         //Clear the active status state.  It will be rebuilt next time it is requested
590         [_activeStatusState release]; _activeStatusState = nil;
591         [_allActiveStatusStates release]; _allActiveStatusStates = nil;
593         //Let observers know the active state has changed
594         [[adium notificationCenter] postNotificationName:AIStatusActiveStateChangedNotification object:nil];
598  * @brief Apply a state to multiple accounts
599  */
600 - (void)applyState:(AIStatus *)statusState toAccounts:(NSArray *)accountArray
602         NSEnumerator    *enumerator;
603         AIAccount               *account;
604         AIStatus                *aStatusState;
605         BOOL                    shouldRebuild = NO;
606         BOOL                    noConnectedAccounts = ![[adium accountController] oneOrMoreConnectedAccounts];
608         //We should connect all accounts if our accounts to connect array is empty and there are no connected accounts
609         BOOL                    shouldConnectAllAccounts = (([accountsToConnect count] == 0) &&
610                                                                                                 noConnectedAccounts);
612         isProcessingGlobalChange = YES;
613         [self setDelayStateMenuUpdates:YES];
614         
615         enumerator = [accountArray objectEnumerator];
616         while(account = [enumerator nextObject]){
617                 if([account online] ||
618                    (noConnectedAccounts && [accountsToConnect containsObject:account]) || 
619                    (shouldConnectAllAccounts)){
620                         /* If this account is online, or no accounts are online and this is an account to connect,
621                          * or we should be connecting all accounts, set the status completely. */
622                         [account setStatusState:statusState];
623                 }else{
624                         //If this account should not have its state set now, perform internal bookkeeping so a future sign-on
625                         //will be to the most appropriate state
626                         [account setStatusStateAndRemainOffline:statusState];
627                 }
628         }
629         
630         //Any objects in the temporary state array which aren't the state we just set should now be removed.
631         enumerator = [[[temporaryStateArray copy] autorelease] objectEnumerator];
632         while(aStatusState = [enumerator nextObject]){
633                 if(aStatusState != statusState){
634                         [temporaryStateArray removeObject:aStatusState];
635                         shouldRebuild = YES;
636                 }
637         }
639         isProcessingGlobalChange = NO;
641         if(shouldRebuild){
642                 //Manually decrease the update delays counter as we don't want to call [self updateAllStateMenuSelections]
643                 stateMenuUpdateDelays--;
644                 [self rebuildAllStateMenus];
645         }else{
646                 /* Allow setDelayStateMenuUpdates to decreate the counter and call
647                  * [self updateAllStateMenuSelections] as appropriate.
648                  */
649                 [self setDelayStateMenuUpdates:NO];                             
650         }
653 #pragma mark Retrieving Status States
655  * @brief Access to Adium's user-defined states
657  * Returns an array of available user-defined states, which are AIStatus objects
658  */
659 - (NSArray *)stateArray
661         if(!stateArray){
662                 NSData  *savedStateArrayData = [[adium preferenceController] preferenceForKey:KEY_SAVED_STATUS
663                                                                                                                                                                 group:PREF_GROUP_SAVED_STATUS];
664                 if(savedStateArrayData){
665                         stateArray = [[NSKeyedUnarchiver unarchiveObjectWithData:savedStateArrayData] mutableCopy];
666                 }
668                 if(!stateArray) stateArray = [[NSMutableArray alloc] init];
670                 //Upgrade Adium 0.7x away messages
671                 [self _upgradeSavedAwaysToSavedStates];
672         }
674         return(stateArray);
678  * @brief Return the array of built-in states
680  * These are basic Available and Away states which should always be visible and are (by convention) immutable.
681  * The first state in BUILT_IN_STATE_ARRAY will be used as the default for accounts as they are created.
682  */
683 - (NSArray *)builtInStateArray
685         if(!builtInStateArray){
686                 NSArray                 *savedBuiltInStateArray = [NSArray arrayNamed:BUILT_IN_STATE_ARRAY forClass:[self class]];
687                 NSEnumerator    *enumerator;
688                 NSDictionary    *dict;
690                 builtInStateArray = [[NSMutableArray alloc] initWithCapacity:[savedBuiltInStateArray count]];
692                 enumerator = [savedBuiltInStateArray objectEnumerator];
693                 while(dict = [enumerator nextObject]){
694                         AIStatus        *status = [AIStatus statusWithDictionary:dict];
695                         [builtInStateArray addObject:status];
697                         //Store a reference to our offline state if we just loaded it
698                         if([status statusType] == AIOfflineStatusType){
699                                 [offlineStatusState release];
700                                 offlineStatusState = [status retain];
701                         }
702                 }
703         }
705         return(builtInStateArray);
708 - (AIStatus *)offlineStatusState
710         //Ensure the built in states have been loaded
711         [self builtInStateArray];
713         NSAssert(offlineStatusState != nil, @"Nil offline status state");
714         return offlineStatusState;
717 //Sort the status array
718 int _statusArraySort(id objectA, id objectB, void *context)
720         AIStatusType statusTypeA = [objectA statusType];
721         AIStatusType statusTypeB = [objectB statusType];
723         //We treat Invisible statuses as being the same as Away for purposes of the menu
724         if(statusTypeA == AIInvisibleStatusType) statusTypeA = AIAwayStatusType;
725         if(statusTypeB == AIInvisibleStatusType) statusTypeB = AIAwayStatusType;
727         if(statusTypeA > statusTypeB){
728                 return NSOrderedDescending;
729         }else if(statusTypeB > statusTypeA){
730                 return NSOrderedAscending;
731         }else{
732                 BOOL isLockedMutabilityTypeA = ([objectA mutabilityType] == AILockedStatusState);
733                 BOOL isLockedMutabilityTypeB = ([objectB mutabilityType] == AILockedStatusState);
735                 //Put locked (built in) statuses at the top
736                 if(isLockedMutabilityTypeA && !isLockedMutabilityTypeB){
737                         return NSOrderedAscending;
738                         
739                 }else if(!isLockedMutabilityTypeA && isLockedMutabilityTypeB){
740                         return NSOrderedDescending;
742                 }else{
743                         /* Check to see if either is temporary; temporary items go above saved ones and below
744                          * built-in ones.
745                          */
746                         BOOL    isTemporaryA = [temporaryStateArray containsObject:objectA];
747                         BOOL    isTemporaryB = [temporaryStateArray containsObject:objectB];
749                         if(isTemporaryA && !isTemporaryB){
750                                 return NSOrderedAscending;
751                                 
752                         }else if(isTemporaryB && !isTemporaryA){
753                                 return NSOrderedDescending;
754                                 
755                         }else{
756                                 NSArray *originalArray = (NSArray *)context;
757                                 
758                                 //Return them in the same relative order as the original array if they are of the same type
759                                 int indexA = [originalArray indexOfObjectIdenticalTo:objectA];
760                                 int indexB = [originalArray indexOfObjectIdenticalTo:objectB];
761                                 
762                                 if(indexA > indexB){
763                                         return NSOrderedDescending;
764                                 }else{
765                                         return NSOrderedAscending;
766                                 }
767                         }
768                 }
769         }
773  * @brief Return a sorted state array for use in menu item creation
775  * The array is created by adding the built in states to the user states, then sorting using _statusArraySort
777  * @result A cached NSArray which is sorted by status type (available, away), built-in vs. user-made, and then original ordering.
778  */
779 - (NSArray *)sortedFullStateArray
781         if(!_sortedFullStateArray){
782                 NSArray                 *originalStateArray = [self stateArray];
783                 NSMutableArray  *tempArray = [originalStateArray mutableCopy];
784                 [tempArray addObjectsFromArray:[self builtInStateArray]];
785                 [tempArray addObjectsFromArray:[temporaryStateArray allObjects]];
787                 //Pass the original array so its indexes can be used for comparison of saved state ordering
788                 [tempArray sortUsingFunction:_statusArraySort context:originalStateArray];
790                 _sortedFullStateArray = tempArray;
791         }
793         return _sortedFullStateArray;
797  * @brief Retrieve active status state
799  * @result The currently active status state.
801  * This is defined as the status state which the most accounts are currently using.  The behavior in case of a tie
802  * is currently undefined but will yield one of the tying states.
803  */
804 - (AIStatus *)activeStatusState
806         if(!_activeStatusState){
807                 NSEnumerator            *enumerator = [[[adium accountController] accountArray] objectEnumerator];
808                 NSCountedSet            *statusCounts = [NSCountedSet set];
809                 AIAccount                       *account;
810                 AIStatus                        *statusState;
811                 unsigned                         highestCount = 0;
812                 BOOL                             accountsAreOnline = [[adium accountController] oneOrMoreConnectedOrConnectingAccounts];
814                 if(accountsAreOnline){
815                         AIStatus        *bestStatusState = nil;
817                         while((account = [enumerator nextObject])) {
818                                 if([account online]){
819                                         AIStatus *accountStatusState = [account statusState];
820                                         [statusCounts addObject:(accountStatusState ?
821                                                                                          accountStatusState :
822                                                                                          [self defaultInitialStatusState])];
823                                 }
824                         }
826                         enumerator = [statusCounts objectEnumerator];
827                         while((statusState = [enumerator nextObject])) {
828                                 unsigned thisCount = [statusCounts countForObject:statusState];
829                                 if(thisCount > highestCount){
830                                         bestStatusState = statusState;
831                                         highestCount = thisCount;
832                                 }
833                         }
835                         _activeStatusState = [bestStatusState retain];
836                 } else {
837                         _activeStatusState = [offlineStatusState retain];
838                 }
839         }
841         return _activeStatusState;
845  * @brief Find the 'active' AIStatusType
847  * The active type is the one used by the largest number of accounts.  In case of a tie, the order of the AIStatusType
848  * enum is respected
850  * @param invisibleIsAway If YES, AIInvisibleStatusType is trated as AIAwayStatusType
851  * @result The active AIStatusType for online accounts, or AIOfflineStatusType if all accounts are  offline
852  */
853 - (AIStatusType)activeStatusTypeTreatingInvisibleAsAway:(BOOL)invisibleIsAway
855         NSEnumerator            *enumerator = [[[adium accountController] accountArray] objectEnumerator];
856         AIAccount                       *account;
857         int                                     statusTypeCount[STATUS_TYPES_COUNT];
858         AIStatusType            activeStatusType = AIOfflineStatusType;
859         unsigned                        highestCount = 0;
861         unsigned i;
862         for(i = 0 ; i < STATUS_TYPES_COUNT ; i++){
863                 statusTypeCount[i] = 0;
864         }
866         while(account = [enumerator nextObject]){
867                 if([account online] || [account integerStatusObjectForKey:@"Connecting"]){
868                         AIStatusType statusType = [[account statusState] statusType];
870                         //If invisibleIsAway, pretend that invisible is away
871                         if (invisibleIsAway && (statusType == AIInvisibleStatusType)) statusType = AIAwayStatusType;
873                         statusTypeCount[statusType]++;
874                 }
875         }
877         for(i = 0 ; i < STATUS_TYPES_COUNT ; i++){
878                 if(statusTypeCount[i] > highestCount){
879                         activeStatusType = i;
880                         highestCount = statusTypeCount[i];
881                 }
882         }
884         return activeStatusType;
888  * @brief All active status states
890  * A status state is active if any online account is currently in that state.
892  * The return value of this method is cached.
894  * @result An <tt>NSSet</tt> of <tt>AIStatus</tt> objects
895  */
896 - (NSSet *)allActiveStatusStates
898         if(!_allActiveStatusStates){
899                 _allActiveStatusStates = [[NSMutableSet alloc] init];
900                 NSEnumerator            *enumerator = [[[adium accountController] accountArray] objectEnumerator];
901                 AIAccount                       *account;
903                 while(account = [enumerator nextObject]){
904                         if([account online] || [account integerStatusObjectForKey:@"Connecting"]){
905                                 [_allActiveStatusStates addObject:[account statusState]];
906                         }
907                 }
908         }
910         return _allActiveStatusStates;
914  * @brief Return the set of all unavailable statuses in use by online or connection accounts
916  * @param activeUnvailableStatusType Pointer to an AIStatusType; returns by reference the most popular unavailable type
917  * @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
918  * @param allOnlineAccountsAreUnvailable Pointer to a BOOL; returns by reference YES is all online accounts are unavailable, NO if one or more is available
919  */
920 - (NSSet *)activeUnavailableStatusesAndType:(AIStatusType *)activeUnvailableStatusType withName:(NSString **)activeUnvailableStatusName allOnlineAccountsAreUnvailable:(BOOL *)allOnlineAccountsAreUnvailable
922         NSEnumerator            *enumerator = [[[adium accountController] accountArray] objectEnumerator];
923         AIAccount                       *account;
924         NSMutableSet            *activeUnvailableStatuses = [NSMutableSet set];
925         BOOL                            foundStatusName = NO;
926         int                                     statusTypeCount[STATUS_TYPES_COUNT];
928         statusTypeCount[AIAwayStatusType] = 0;
929         statusTypeCount[AIInvisibleStatusType] = 0;
930         
931         //Assume all accounts are unavailable until proven otherwise
932         if(allOnlineAccountsAreUnvailable != NULL){
933                 *allOnlineAccountsAreUnvailable = YES;
934         }
935         
936         while(account = [enumerator nextObject]){
937                 if([account online] || [account integerStatusObjectForKey:@"Connecting"]){
938                         AIStatus        *statusState = [account statusState];
939                         AIStatusType statusType = [statusState statusType];
940                         
941                         if((statusType == AIAwayStatusType) || (statusType == AIInvisibleStatusType)){
942                                 NSString        *statusName = [statusState statusName];
943                                 
944                                 [activeUnvailableStatuses addObject:statusState];
945                                 
946                                 statusTypeCount[statusType]++;
947                                 
948                                 if(foundStatusName){
949                                         //Once we find a status name, we only want to return it if all our status names are the same.
950                                         if((activeUnvailableStatusName != NULL) &&
951                                            (*activeUnvailableStatusName != nil) && 
952                                            ![*activeUnvailableStatusName isEqualToString:statusName]){
953                                                 *activeUnvailableStatusName = nil;
954                                         }
955                                 }else{
956                                         //We haven't found a status name yet, so store this one as the active status name
957                                         if(activeUnvailableStatusName != NULL){
958                                                 *activeUnvailableStatusName = [statusState statusName];
959                                         }
960                                         foundStatusName = YES;
961                                 }
962                         }else{
963                                 //An online account isn't unavailable
964                                 if(allOnlineAccountsAreUnvailable != NULL){
965                                         *allOnlineAccountsAreUnvailable = NO;
966                                 }
967                         }
968                 }
969         }
970         
971         if(activeUnvailableStatusType != NULL){
972                 if(statusTypeCount[AIAwayStatusType] > statusTypeCount[AIInvisibleStatusType]){
973                         *activeUnvailableStatusType = AIAwayStatusType;
974                 }else{
975                         *activeUnvailableStatusType = AIInvisibleStatusType;            
976                 }
977         }
978         
979         return activeUnvailableStatuses;
984  * @brief Next available unique status ID
985  */
986 - (NSNumber *)nextUniqueStatusID
988         NSNumber        *nextUniqueStatusID;
990         //Retain and autorelease since we'll be replacing this value (and therefore releasing it) via the preferenceController.
991         nextUniqueStatusID = [[[[adium preferenceController] preferenceForKey:TOP_STATUS_STATE_ID
992                                                                                                                                   group:PREF_GROUP_SAVED_STATUS] retain] autorelease];
993         if(!nextUniqueStatusID) nextUniqueStatusID = [NSNumber numberWithInt:1];
995         [[adium preferenceController] setPreference:[NSNumber numberWithInt:([nextUniqueStatusID intValue] + 1)]
996                                                                                  forKey:TOP_STATUS_STATE_ID
997                                                                                   group:PREF_GROUP_SAVED_STATUS];
999         return nextUniqueStatusID;
1003  * @brief Find the status state with the requested uniqueStatusID
1004  */
1005 - (AIStatus *)statusStateWithUniqueStatusID:(NSNumber *)uniqueStatusID
1007         AIStatus                *statusState = nil;
1009         if(uniqueStatusID){
1010                 NSEnumerator    *enumerator = [[self sortedFullStateArray] objectEnumerator];
1012                 while(statusState = [enumerator nextObject]){
1013                         if([[statusState uniqueStatusID] compare:uniqueStatusID] == NSOrderedSame)
1014                                 break;
1015                 }
1016         }
1018         return statusState;
1021 //State Editing --------------------------------------------------------------------------------------------------------
1022 #pragma mark State Editing
1024  * @brief Add a state
1026  * Add a new state to Adium's state array.
1027  * @param state AIState to add
1028  */
1029 - (void)addStatusState:(AIStatus *)statusState
1031         [stateArray addObject:statusState];
1032         [self _saveStateArrayAndNotifyOfChanges];
1036  * @brief Remove a state
1038  * Remove a new state from Adium's state array.
1039  * @param state AIStatus to remove
1040  */
1041 - (void)removeStatusState:(AIStatus *)statusState
1043         [stateArray removeObject:statusState];
1044         [self _saveStateArrayAndNotifyOfChanges];
1048  * @brief Move a state
1050  * Move a state that already exists in Adium's state array to another index
1051  * @param state AIStatus to move
1052  * @param destIndex Destination index
1053  */
1054 - (int)moveStatusState:(AIStatus *)statusState toIndex:(int)destIndex
1056     int sourceIndex = [stateArray indexOfObjectIdenticalTo:statusState];
1058     //Remove the state
1059     [statusState retain];
1060     [stateArray removeObject:statusState];
1062     //Re-insert the state
1063     if(destIndex > sourceIndex) destIndex -= 1;
1064     [stateArray insertObject:statusState atIndex:destIndex];
1065     [statusState release];
1067         [self _saveStateArrayAndNotifyOfChanges];
1069         return(destIndex);
1073  * @brief Replace a state
1075  * Replace a state in Adium's state array with another state.
1076  * @param oldState AIStatus state that is in Adium's state array
1077  * @param newState AIStatus state with which to replace oldState
1078  */
1079 - (void)replaceExistingStatusState:(AIStatus *)oldStatusState withStatusState:(AIStatus *)newStatusState
1081         if(oldStatusState != newStatusState){
1082                 int index = [stateArray indexOfObject:oldStatusState];
1084                 if(index >= 0 && index < [stateArray count]){
1085                         [stateArray replaceObjectAtIndex:index withObject:newStatusState];
1086                 }
1087         }
1089         [self _saveStateArrayAndNotifyOfChanges];
1093  * @brief Save changes to the state array and notify observers
1095  * Saves any outstanding changes to the state array.  There should be no need to call this manually, since all the
1096  * state array modifying methods in this class call it automatically after making changes.
1098  * After the state array is saved, observers are notified that is has changed.  Call after making any changes to the
1099  * state array from within the controller.
1100  */
1101 - (void)_saveStateArrayAndNotifyOfChanges
1103         //Clear the sorted menu items array since our state array changed.
1104         [_sortedFullStateArray release]; _sortedFullStateArray = nil;
1106         [[adium preferenceController] setPreference:[NSKeyedArchiver archivedDataWithRootObject:[self stateArray]]
1107                                                                                  forKey:KEY_SAVED_STATUS
1108                                                                                   group:PREF_GROUP_SAVED_STATUS];
1109         [[adium notificationCenter] postNotificationName:AIStatusStateArrayChangedNotification object:nil];
1112 - (void)statusStateDidSetUniqueStatusID
1114         [[adium preferenceController] setPreference:[NSKeyedArchiver archivedDataWithRootObject:[self stateArray]]
1115                                                                                  forKey:KEY_SAVED_STATUS
1116                                                                                   group:PREF_GROUP_SAVED_STATUS];
1119 //Machine Activity -----------------------------------------------------------------------------------------------------
1120 #pragma mark Machine Activity
1121 #define MACHINE_IDLE_THRESHOLD                  30      //30 seconds of inactivity is considered idle
1122 #define MACHINE_ACTIVE_POLL_INTERVAL    30      //Poll every 60 seconds when the user is active
1123 #define MACHINE_IDLE_POLL_INTERVAL              1       //Poll every second when the user is idle
1125 //Private idle function
1126 extern double CGSSecondsSinceLastInputEvent(unsigned long evType);
1129  * @brief Returns the current machine idle time
1131  * Returns the current number of seconds the machine has been idle.  The machine is idle when there are no input
1132  * events from the user (such as mouse movement or keyboard input).  In addition to this method, the status controller
1133  * sends out notifications when the machine becomes idle, stays idle, and returns to an active state.
1134  */
1135 - (double)currentMachineIdle
1137     double idleTime = CGSSecondsSinceLastInputEvent(-1);
1139         //On MDD Powermacs, the above function will return a large value when the machine is active (perhaps a -1?).
1140         //Here we check for that value and correctly return a 0 idle time.
1141         if(idleTime >= 18446744000.0) idleTime = 0.0; //18446744073.0 is the lowest I've seen on my MDD -ai
1143     return(idleTime);
1147  * @brief Timer that checkes for machine idle
1149  * This timer periodically checks the machine for inactivity.  When the machine has been inactive for atleast
1150  * MACHINE_IDLE_THRESHOLD seconds, a notification is broadcast.
1152  * When the machine is active, this timer is called infrequently.  It's not important to notice that the user went
1153  * idle immediately, so we relax our CPU usage while waiting for an idle state to begin.
1155  * When the machine is idle, the timer is called frequently.  It's important to notice immediately when the user
1156  * returns.
1157  */
1158 - (void)_idleCheckTimer:(NSTimer *)inTimer
1160         double  currentIdle = [self currentMachineIdle];
1162         if(machineIsIdle){
1163                 if(currentIdle < lastSeenIdle){
1164                         //If the machine is less idle than the last time we recorded, it means that activity has occured and the
1165                         //user is no longer idle.
1166                         [self _setMachineIsIdle:NO];
1167                 }else{
1168                         //Periodically broadcast a 'MachineIdleUpdate' notification
1169                         [[adium notificationCenter] postNotificationName:AIMachineIdleUpdateNotification
1170                                                                                                           object:nil
1171                                                                                                         userInfo:[NSDictionary dictionaryWithObjectsAndKeys:
1172                                                                                                                 [NSNumber numberWithDouble:currentIdle], @"Duration",
1173                                                                                                                 [NSDate dateWithTimeIntervalSinceNow:-currentIdle], @"IdleSince",
1174                                                                                                                 nil]];
1175                 }
1176         }else{
1177                 //If machine inactivity is over the threshold, the user has gone idle.
1178                 if(currentIdle > MACHINE_IDLE_THRESHOLD) [self _setMachineIsIdle:YES];
1179         }
1181         lastSeenIdle = currentIdle;
1185  * @brief Sets the machine as idle or not
1187  * This internal method updates the frequency of our idle timer depending on whether the machine is considered
1188  * idle or not.  It also posts the AIMachineIsIdleNotification and AIMachineIsActiveNotification notifications
1189  * based on the passed idle state
1190  */
1191 - (void)_setMachineIsIdle:(BOOL)inIdle
1193         machineIsIdle = inIdle;
1195         //Post the appropriate idle or active notification
1196         if(machineIsIdle){
1197                 [[adium notificationCenter] postNotificationName:AIMachineIsIdleNotification object:nil];
1198         }else{
1199                 [[adium notificationCenter] postNotificationName:AIMachineIsActiveNotification object:nil];
1200         }
1202         //Update our timer interval for either idle or active polling
1203         [idleTimer invalidate];
1204         [idleTimer release];
1205         idleTimer = [[NSTimer scheduledTimerWithTimeInterval:(machineIsIdle ? MACHINE_IDLE_POLL_INTERVAL : MACHINE_ACTIVE_POLL_INTERVAL)
1206                                                                                                   target:self
1207                                                                                                 selector:@selector(_idleCheckTimer:)
1208                                                                                                 userInfo:nil
1209                                                                                                  repeats:YES] retain];
1213 //Status state menu support ---------------------------------------------------------------------------------------------------
1214 #pragma mark Status state menu support
1216  * @brief Register a state menu plugin
1218  * A state menu plugin is the mitigator between our state menu items and a menu.  As states change the plugin
1219  * is told to add and remove items from the menu.  Everything else is handled by the status controller.
1220  * @param stateMenuPlugin The state menu plugin to register
1221  */
1222 - (void)registerStateMenuPlugin:(id <StateMenuPlugin>)stateMenuPlugin
1224         NSNumber        *identifier = [NSNumber numberWithInt:[stateMenuPlugin hash]];
1226         //Track this plugin
1227         [stateMenuItemArraysDict setObject:[NSMutableArray array] forKey:identifier];
1228         [stateMenuPluginsArray addObject:stateMenuPlugin];
1230         //Start it out with a fresh set of menu items
1231         [self _addStateMenuItemsForPlugin:stateMenuPlugin];
1235  * @brief Unregister a state menu plugin
1237  * All state menu items will be removed from the plugin when it unregisters
1238  * @param stateMenuPlugin The state menu plugin to unregister
1239  */
1240 - (void)unregisterStateMenuPlugin:(id <StateMenuPlugin>)stateMenuPlugin
1242         NSNumber        *identifier = [NSNumber numberWithInt:[stateMenuPlugin hash]];
1244         //Remove all the plugin's menu items
1245         [self _removeStateMenuItemsForPlugin:stateMenuPlugin];
1247         //Stop tracking the plugin
1248         [stateMenuItemArraysDict removeObjectForKey:identifier];
1249         [stateMenuPluginsArray removeObjectIdenticalTo:stateMenuPlugin];
1253  * @brief Remove the status controller's tracking for a plugin's menu items
1255  * This should be called in preparation for one or more plugin:didAddMenuItems: calls to clear out the current
1256  * tracking for the statusController generated menu items.
1257  */
1258 - (void)removeAllMenuItemsForPlugin:(id <StateMenuPlugin>)stateMenuPlugin
1260         NSNumber                *identifier = [NSNumber numberWithInt:[stateMenuPlugin hash]];
1261         NSMutableArray  *menuItemArray = [stateMenuItemArraysDict objectForKey:identifier];
1263         //Clear the array
1264         [menuItemArray removeAllObjects];
1268  * @brief A plugin created its own menu items it wants us to track and update
1269  */
1270 - (void)plugin:(id <StateMenuPlugin>)stateMenuPlugin didAddMenuItems:(NSArray *)addedMenuItems
1272         NSNumber                *identifier = [NSNumber numberWithInt:[stateMenuPlugin hash]];
1273         NSMutableArray  *menuItemArray = [stateMenuItemArraysDict objectForKey:identifier];
1275         [menuItemArray addObjectsFromArray:addedMenuItems];
1276         [stateMenuItemsNeedingUpdating  addObjectsFromArray:addedMenuItems];
1280  * @brief Add state menu items
1282  * Adds all the necessary state menu items to a plugin's state menu
1283  * @param stateMenuPlugin The state menu plugin we're updating
1284  */
1285 - (void)_addStateMenuItemsForPlugin:(id <StateMenuPlugin>)stateMenuPlugin
1287         NSNumber                *identifier = [NSNumber numberWithInt:[stateMenuPlugin hash]];
1288         NSMutableArray  *menuItemArray = [stateMenuItemArraysDict objectForKey:identifier];
1289         NSEnumerator    *enumerator;
1290         NSMenuItem              *menuItem;
1291         AIStatus                *statusState;
1292         AIStatusType    currentStatusType = AIAvailableStatusType;
1294         //Create a menu item for each state.  States must first be sorted such that states of the same AIStatusType
1295         //are grouped together.
1296         enumerator = [[self sortedFullStateArray] objectEnumerator];
1297         while(statusState = [enumerator nextObject]){
1298                 AIStatusType thisStatusType = [statusState statusType];
1300                 //We treat Invisible statuses as being the same as Away for purposes of the menu
1301                 if(thisStatusType == AIInvisibleStatusType) thisStatusType = AIAwayStatusType;
1303                 //Add the "Custom..." state option and a separatorItem before beginning to add items for a new statusType
1304                 if((currentStatusType != thisStatusType) &&
1305                    (currentStatusType != AIOfflineStatusType)){
1306                         menuItem = [[NSMenuItem alloc] initWithTitle:STATUS_TITLE_CUSTOM
1307                                                                                                   target:self
1308                                                                                                   action:@selector(selectCustomState:)
1309                                                                                    keyEquivalent:@""];
1311                         [menuItem setImage:[[[AIStatusIcons statusIconForStatusName:nil
1312                                                                                                                          statusType:currentStatusType
1313                                                                                                                            iconType:AIStatusIconList
1314                                                                                                                           direction:AIIconNormal] copy] autorelease]];
1315                         [menuItem setTag:currentStatusType];
1316                         [menuItemArray addObject:menuItem];
1317                         [menuItem release];
1319                         //Add a divider
1320                         [menuItemArray addObject:[NSMenuItem separatorItem]];
1322                         currentStatusType = thisStatusType;
1323                 }
1325                 menuItem = [[NSMenuItem alloc] initWithTitle:[self _titleForMenuDisplayOfState:statusState]
1326                                                                                           target:self
1327                                                                                           action:@selector(selectState:)
1328                                                                            keyEquivalent:@""];
1330                 //NSMenuItem will call setFlipped: on the image we pass it, causing flipped drawing elsewhere if we pass it the
1331                 //shared status icon.  So we pass it a copy of the shared icon that it's free to manipulate.
1332                 [menuItem setImage:[[[statusState icon] copy] autorelease]];
1333                 [menuItem setTag:currentStatusType];
1334                 if([NSApp isOnPantherOrBetter]){
1335                         [menuItem setToolTip:[statusState statusMessageString]];
1336                 }
1337                 [menuItem setRepresentedObject:[NSDictionary dictionaryWithObject:statusState
1338                                                                                                                                    forKey:@"AIStatus"]];
1339                 [menuItemArray addObject:menuItem];
1340                 [menuItem release];
1341         }
1343         if(currentStatusType != AIOfflineStatusType){
1344                 /* Add the last "Custom..." state optior for the last statusType we handled,
1345                  * which didn't get a "Custom..." item yet.  At present, our last status type should always be
1346                  * our AIOfflineStatusType, so this will never be executed and just exists for completeness. */
1347                 menuItem = [[NSMenuItem alloc] initWithTitle:STATUS_TITLE_CUSTOM
1348                                                                                           target:self
1349                                                                                           action:@selector(selectCustomState:)
1350                                                                            keyEquivalent:@""];
1351                 [menuItem setImage:[[[AIStatusIcons statusIconForStatusName:nil
1352                                                                                                                  statusType:currentStatusType
1353                                                                                                                    iconType:AIStatusIconList
1354                                                                                                                   direction:AIIconNormal] copy] autorelease]];
1355                 [menuItem setTag:currentStatusType];
1356                 [menuItemArray addObject:menuItem];
1357                 [menuItem release];
1358         }
1360         //Now that we are done creating the menu items, tell the plugin about them
1361         [stateMenuPlugin addStateMenuItems:menuItemArray];
1363         //Update the selected menu item after giving the plugin a chance to do with the menu items as it wants
1364         [self updateStateMenuSelectionForPlugin:stateMenuPlugin];
1368  * @brief Removes state menu items
1370  * Removes all the state menu items from a plugin's state menu
1371  * @param stateMenuPlugin The state menu plugin we're updating
1372  */
1373 - (void)_removeStateMenuItemsForPlugin:(id <StateMenuPlugin>)stateMenuPlugin
1375         NSNumber                *identifier = [NSNumber numberWithInt:[stateMenuPlugin hash]];
1376         NSMutableArray  *menuItemArray = [stateMenuItemArraysDict objectForKey:identifier];
1378         //Inform the plugin that we are removing the items in this array
1379         [stateMenuPlugin removeStateMenuItems:menuItemArray];
1381         //Now clear the array
1382         [menuItemArray removeAllObjects];
1386  * @brief Completely rebuild all state menus
1388  * Before doing so, clear the activeStatusState, which will be regenerated when next needed
1389  */
1390 - (void)rebuildAllStateMenus
1392         //Clear the sorted menu items array since our state array changed.
1393         [_sortedFullStateArray release]; _sortedFullStateArray = nil;
1395         [self _resetActiveStatusState];
1397         NSEnumerator                    *enumerator = [stateMenuPluginsArray objectEnumerator];
1398         id <StateMenuPlugin>    stateMenuPlugin;
1400         while(stateMenuPlugin = [enumerator nextObject]) {
1401                 [self _removeStateMenuItemsForPlugin:stateMenuPlugin];
1402                 [self _addStateMenuItemsForPlugin:stateMenuPlugin];
1403         }
1407  * @brief Completely rebuild all state menus for a single plugin
1408  */
1409 - (void)rebuildAllStateMenusForPlugin:(id <StateMenuPlugin>)stateMenuPlugin
1411         [self _removeStateMenuItemsForPlugin:stateMenuPlugin];
1412         [self _addStateMenuItemsForPlugin:stateMenuPlugin];
1416  * @brief Update the selected state in all state menus
1417  */
1418 - (void)updateAllStateMenuSelections
1420         if(stateMenuUpdateDelays == 0){
1421                 NSEnumerator                    *enumerator = [stateMenuPluginsArray objectEnumerator];
1422                 id <StateMenuPlugin>    stateMenuPlugin;
1424                 while(stateMenuPlugin = [enumerator nextObject]){
1425                         [self updateStateMenuSelectionForPlugin:stateMenuPlugin];
1426                 }
1427                 
1428                 [self _resetActiveStatusState];
1429                 
1430                 /* Let any relevant plugins respond to the to-be-changed state menu selection. Technically we
1431                  * haven't changed it yet, since we'll do that in validateMenuItem:, but the fact that we will now
1432                  * need to change it is useful if, for example, key equivalents change in a menu alongside selection
1433                  * changes, since we need key equivalents set immediately.
1434                  */
1435                 [[adium notificationCenter] postNotificationName:AIStatusStateMenuSelectionsChangedNotification
1436                                                                                                   object:nil];
1437         }
1441  * @brief Delay state menu updates
1443  * This should be called to prevent duplicative updates when multiple accounts are changing status simultaneously.
1444  */
1445 - (void)setDelayStateMenuUpdates:(BOOL)shouldDelay
1447         if(shouldDelay)
1448                 stateMenuUpdateDelays++;
1449         else
1450                 stateMenuUpdateDelays--;
1452         if(stateMenuUpdateDelays == 0){
1453                 [self updateAllStateMenuSelections];
1454         }
1458  * @brief Update the selected state in a plugin's state menu
1460  * Updates the selected state menu item to reflect the currently active state.
1461  * @param stateMenuPlugin The state menu plugin we're updating
1462  */
1463 - (void)updateStateMenuSelectionForPlugin:(id <StateMenuPlugin>)stateMenuPlugin
1465         NSNumber                *identifier = [NSNumber numberWithInt:[stateMenuPlugin hash]];
1466         NSArray                 *stateMenuItemArray = [stateMenuItemArraysDict objectForKey:identifier];
1467         [stateMenuItemsNeedingUpdating addObjectsFromArray:stateMenuItemArray];
1472  * @brief Account status changed.
1474  * Rebuild all our state menus
1475  */
1476 - (NSSet *)updateListObject:(AIListObject *)inObject keys:(NSSet *)inModifiedKeys silent:(BOOL)silent
1478         if([inObject isKindOfClass:[AIAccount class]]){
1479                 if([inModifiedKeys containsObject:@"Online"] ||
1480                    [inModifiedKeys containsObject:@"IdleSince"] ||
1481                    [inModifiedKeys containsObject:@"StatusState"]){
1483                         //Don't update the state menus if we are currently delaying
1484                         if(stateMenuUpdateDelays == 0) [self updateAllStateMenuSelections];
1486                         //We can get here without the preferencesChanged: notification if the account is automatically connected.
1487                         if([inModifiedKeys containsObject:@"Online"]){
1488                                 if([inObject online]) [accountsToConnect addObject:inObject];
1489                         }
1490                 }
1491         }
1493     return(nil);
1497  * @brief Preferences changed; update our accountsToConnect tracking set
1499  * We use the preferences changed notifications rather than the statusObject notifications because the statusObject
1500  * may not change immediately upon requesting a connect or disconnect, since the account may wait to receive confirmation
1501  * before reporting itself as online or offline.  With the preferences changed notification, we can distinguish a user
1502  * disconnect from selecting the global Offline menu item.
1503  */
1504 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key
1505                                                         object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
1507         if([key isEqualToString:@"Online"]){
1508                 /* Track the accounts we should connect when setting to an online state.  Our goal is to be able to reconnect
1509                 * the most recently connected account if accounts are disconnected one-by-one.  If accounts are disconnected
1510                 * all at once via the global Offline menu item, we want to restore all of the previously connected accounts when
1511                 * reconnecting, so we check to see if we are disconnecting via that menu item with the
1512                 * isProcessingGlobalChange BOOL. */
1513                 if(!isProcessingGlobalChange){
1514                         if([[object preferenceForKey:@"Online" group:GROUP_ACCOUNT_STATUS] boolValue]){
1515                                 [accountsToConnect addObject:object];
1516                         }else{
1517                                 if([accountsToConnect count] > 1){
1518                                         [accountsToConnect removeObject:object];
1519                                 }
1520                         }
1521                 }
1523                 //Clear these caches now. Observers which get called before we do when an account actually connects
1524                 //will want to get a fresh value.
1525                 [self _resetActiveStatusState];
1526         }
1530  * @brief Menu validation
1532  * Our state menu items should always be active, so always return YES for validation.
1534  * Here we lazily set the state of our menu items if our stateMenuItemsNeedingUpdating set indicates it is needed.
1535  */
1536 - (BOOL)validateMenuItem:(id <NSMenuItem>)menuItem
1538         if([stateMenuItemsNeedingUpdating containsObject:menuItem]){
1539                 BOOL                    noAccountsAreOnline = ![[adium accountController] oneOrMoreConnectedAccounts];
1540                 NSDictionary    *dict = [menuItem representedObject];
1541                 AIAccount               *account;
1542                 AIStatus                *menuItemStatusState;
1543                 BOOL                    shouldSelectOffline;
1545                 //Search for the account or global status state as appropriate for this menu item.
1546                 //Also, determine if we are looking to select the Offline menu item
1547                 if(account = [dict objectForKey:@"AIAccount"]){
1548                         shouldSelectOffline = ![account online];
1549                 }else{
1550                         shouldSelectOffline = noAccountsAreOnline;
1551                 }
1553                 menuItemStatusState = [dict objectForKey:@"AIStatus"];
1555                 if(shouldSelectOffline){
1556                         //If we should select offline, set all menu items which don't have the AIOfflineStatusType tag to be off.
1557                         if([menuItem tag] == AIOfflineStatusType){
1558                                 if([menuItem state] != NSOnState) [menuItem setState:NSOnState];
1559                         }else{
1560                                 if([menuItem state] != NSOffState) [menuItem setState:NSOffState];
1561                         }
1563                 }else{
1564                         if(account){
1565                                 /* Account-specific menu items */
1566                                 AIStatus                *appropiateActiveStatusState;
1567                                 appropiateActiveStatusState = [account statusState];
1569                                 /* Our "Custom..." menu choice has a nil represented object.  If the appropriate active search state is
1570                                         * in our array of states from which we made menu items, we'll be searching to match it.  If it isn't,
1571                                         * we have a custom state and will be searching for the custom item of the right type, switching all other
1572                                         * menu items to NSOffState. */
1573                                 if([[self sortedFullStateArray] containsObjectIdenticalTo:appropiateActiveStatusState]){
1574                                         //If the search state is in the array so is a saved state, search for the match
1575                                         if(menuItemStatusState == appropiateActiveStatusState){
1576                                                 if([menuItem state] != NSOnState) [menuItem setState:NSOnState];
1577                                         }else{
1578                                                 if([menuItem state] != NSOffState) [menuItem setState:NSOffState];
1579                                         }
1580                                 }else{
1581                                         //If there is not a status state, we are in a Custom state. Search for the correct Custom item.
1582                                         if(menuItemStatusState){
1583                                                 //If the menu item has an associated state, it's always off.
1584                                                 if([menuItem state] != NSOffState) [menuItem setState:NSOffState];
1585                                         }else{
1586                                                 //If it doesn't, check the tag to see if it should be on or off.
1587                                                 if([menuItem tag] == [appropiateActiveStatusState statusType]){
1588                                                         if([menuItem state] != NSOnState) [menuItem setState:NSOnState];
1589                                                 }else{
1590                                                         if([menuItem state] != NSOffState) [menuItem setState:NSOffState];
1591                                                 }
1592                                         }
1593                                 }
1594                         }else{
1595                                 /* General menu items */
1596                                 NSSet   *allActiveStatusStates = [self allActiveStatusStates];
1597                                 int             onState = (([allActiveStatusStates count] == 1) ? NSOnState : NSMixedState);
1599                                 if(menuItemStatusState){
1600                                         //If this menu item has a status state, set it to the right on state if that state is active
1601                                         if([allActiveStatusStates containsObject:menuItemStatusState]){
1602                                                 if([menuItem state] != onState) [menuItem setState:onState];
1603                                         }else{
1604                                                 if([menuItem state] != NSOffState) [menuItem setState:NSOffState];
1605                                         }
1606                                 }else{
1607                                         //If it doesn't, check the tag to see if it should be on or off by looking for a matching custom state
1608                                         NSEnumerator    *activeStatusStatesEnumerator = [allActiveStatusStates objectEnumerator];
1609                                         NSArray                 *sortedFullStateArray = [self sortedFullStateArray];
1610                                         AIStatus                *statusState;
1611                                         BOOL                    foundCorrectStatusState = NO;
1613                                         while(!foundCorrectStatusState && (statusState = [activeStatusStatesEnumerator nextObject])){
1614                                                 //We found a custom match if our array of menu item states doesn't contain this state and
1615                                                 //its statusType matches the menuItem's tag.
1616                                                 foundCorrectStatusState = (![sortedFullStateArray containsObjectIdenticalTo:statusState] &&
1617                                                                                                    ([menuItem tag] == [statusState statusType]));
1618                                         }
1620                                         if(foundCorrectStatusState){
1621                                                 if([menuItem state] != NSOnState) [menuItem setState:onState];
1622                                         }else{
1623                                                 if([menuItem state] != NSOffState) [menuItem setState:NSOffState];
1624                                         }
1625                                 }
1626                         }
1627                 }
1629                 [stateMenuItemsNeedingUpdating removeObject:menuItem];
1630         }
1632         return(YES);
1636  * @brief Select a state menu item
1638  * Invoked by a state menu item, sets the state corresponding to the menu item as the active state.
1640  * If the representedObject NSDictionary has an @"AIAccount" object, set the state just for the appropriate AIAccount.
1641  * Otherwise, set the state globally.
1642  */
1643 - (void)selectState:(id)sender
1645         NSDictionary    *dict = [sender representedObject];
1646         AIStatus                *statusState = [dict objectForKey:@"AIStatus"];
1647         AIAccount               *account = [dict objectForKey:@"AIAccount"];
1649         /* Random undocumented feature of the moment... hold option and select a state to bring up the custom status window
1650          * for modifying and then setting it.
1651          */
1652         if([NSEvent optionKey]){
1653                 [AIEditStateWindowController editCustomState:statusState
1654                                                                                          forType:[statusState statusType]
1655                                                                                   andAccount:account
1656                                                                           withSaveOption:YES
1657                                                                                         onWindow:nil
1658                                                                          notifyingTarget:self];
1660         }else{
1661                 if(account){
1662                         BOOL shouldRebuild;
1663                         
1664                         shouldRebuild = [self removeIfNecessaryTemporaryStatusState:[account statusState]];
1665                         [account setStatusState:statusState];
1666                         
1667                         if(shouldRebuild){
1668                                 //Rebuild our menus if there was a change
1669                                 [self rebuildAllStateMenus];
1670                         }
1671                         
1672                 }else{
1673                         [self setActiveStatusState:statusState];
1674                 }
1675         }
1679  * @brief Select the custom state menu item
1681  * Invoked by the custom state menu item, opens a custom state window.
1682  * If the representedObject NSDictionary has an @"AIAccount" object, configure just for the appropriate AIAccount.
1683  * Otherwise, configure globally.
1684  */
1685 - (IBAction)selectCustomState:(id)sender
1687         NSDictionary    *dict = [sender representedObject];
1688         AIAccount               *account = [dict objectForKey:@"AIAccount"];
1689         AIStatusType    statusType = [sender tag];
1690         AIStatus                *baseStatusState;
1692         if(account){
1693                 baseStatusState = [account statusState];
1694         }else{
1695                 baseStatusState = [self activeStatusState];
1696         }
1698         /* If we are going to a custom state of a different type, we don't want to prefill with baseStatusState as it stands.
1699          * Instead, we load the last used status of that type. */
1700         if(([baseStatusState statusType] != statusType)){
1701                 NSDictionary *lastStatusStates = [[adium preferenceController] preferenceForKey:@"LastStatusStates"
1702                                                                                                                                                                   group:PREF_GROUP_STATUS_PREFERENCES];
1704                 NSData          *lastStatusStateData = [lastStatusStates objectForKey:[NSNumber numberWithInt:statusType]];
1705                 AIStatus        *lastStatusStateOfThisType = (lastStatusStateData ?
1706                                                                                                   [NSKeyedUnarchiver unarchiveObjectWithData:lastStatusStateData] :
1707                                                                                                   nil);
1709                 baseStatusState = [[lastStatusStateOfThisType retain] autorelease];
1710         }
1712         //don't use the current status state as a base.  Going from Away to Available, don't autofill the Available
1713         //status message with the old away message.
1714         if([baseStatusState statusType] != statusType){
1715                 baseStatusState = nil;
1716         }
1718         [AIEditStateWindowController editCustomState:baseStatusState
1719                                                                                  forType:statusType
1720                                                                           andAccount:account
1721                                                                   withSaveOption:YES
1722                                                                                 onWindow:nil
1723                                                                  notifyingTarget:self];
1727  * @brief Called when a state could potentially need to removed from the temporary (non-saved) list
1729  * If originalState is in the temporary status array, and it is being used on one or zero accounts, it 
1730  * is removed from the temporary status array. This method should be used when one or more accounts have stopped
1731  * using a single status state to determine if that status state is both non-saved and unused.
1733  * @result YES if the state was removed
1734  */
1735 - (BOOL)removeIfNecessaryTemporaryStatusState:(AIStatus *)originalState
1737         BOOL didRemove = NO;
1738         
1739         /* If the original (old) status state is in our temporary array and is not being used in more than 1 account
1740         * we should remove it */
1741         if([temporaryStateArray containsObject:originalState]){
1742                 NSEnumerator    *enumerator;
1743                 AIAccount               *account;
1744                 int                             count = 0;
1745                 
1746                 enumerator = [[[adium accountController] accountArray] objectEnumerator];
1747                 while(account = [enumerator nextObject]){
1748                         if([account actualStatusState] == originalState){
1749                                 if(++count > 1) break;
1750                         }
1751                 }
1753                 if(count <= 1){
1754                         [temporaryStateArray removeObject:originalState];
1755                         didRemove = YES;
1756                 }
1757         }
1758         
1759         return didRemove;
1762  * @brief Apply a custom state
1764  * Invoked when the custom state window is closed by the user clicking OK.  In response this method sets the custom
1765  * state as the active state.
1766  */
1767 - (void)customStatusState:(AIStatus *)originalState changedTo:(AIStatus *)newState forAccount:(AIAccount *)account
1769         BOOL shouldRebuild = NO;
1770         
1771         if(account){
1772                 shouldRebuild = [self removeIfNecessaryTemporaryStatusState:originalState];
1774                 //Now set the newState for the account
1775                 [account setStatusState:newState];
1776                 
1777         }else{
1778                 //Set the state for all accounts.  This will clear out the temporaryStatusArray as necessary.
1779                 [self setActiveStatusState:newState];
1780         }
1782         if([newState mutabilityType] != AITemporaryEditableStatusState){
1783                 [[adium statusController] addStatusState:newState];
1784         }
1786         NSMutableDictionary *lastStatusStates;
1788         lastStatusStates = [[[adium preferenceController] preferenceForKey:@"LastStatusStates"
1789                                                                                                                                  group:PREF_GROUP_STATUS_PREFERENCES] mutableCopy];
1790         if(!lastStatusStates) lastStatusStates = [NSMutableDictionary dictionary];
1792         [lastStatusStates setObject:[NSKeyedArchiver archivedDataWithRootObject:newState]
1793                                                  forKey:[NSNumber numberWithInt:[newState statusType]]];
1795         [[adium preferenceController] setPreference:lastStatusStates
1796                                                                                  forKey:@"LastStatusStates"
1797                                                                                   group:PREF_GROUP_STATUS_PREFERENCES];
1799         //Add to our temporary status array if it's not in our state array
1800         if(shouldRebuild || (![[self stateArray] containsObjectIdenticalTo:newState])){
1801                 [temporaryStateArray addObject:newState];
1803                 //Now rebuild our menus to include this temporary item
1804                 [self rebuildAllStateMenus];
1805         }
1809  * @brief Determine a string to use as a menu title
1811  * This method truncates a state title string for display as a menu item.
1812  * Wide menus aren't pretty and may cause crashing in certain versions of OS X, so all state
1813  * titles should be run through this method before being used as menu item titles.
1815  * @param statusState The state for which we want a title
1817  * @result An appropriate NSString title
1818  */
1819 - (NSString *)_titleForMenuDisplayOfState:(AIStatus *)statusState
1821         NSString        *title = [statusState title];
1823         /* Why plus 3? Say STATE_TITLE_MENU_LENGTH was 7, and the title is @"ABCDEFGHIJ".
1824          * The shortened title will be @"ABCDEFG..." which looks to be just as long - even
1825          * if the ellipsis is an ellipsis character and therefore technically two characters
1826          * shorter. Better to just use the full string, which appears as being the same length.
1827          */
1828         if ([title length] > STATE_TITLE_MENU_LENGTH+3) {
1829                 title = [title stringWithEllipsisByTruncatingToLength:STATE_TITLE_MENU_LENGTH];
1830         }
1832         return(title);
1836  * @brief Create and add the built-in status types
1838  * The built-in status types are basic, generic "Available" and "Away" states.
1839  */
1840 - (void)buildBuiltInStatusTypes
1842         NSDictionary    *statusDict;
1844         builtInStatusTypes[AIAvailableStatusType] = [[NSMutableSet alloc] init];
1845         statusDict = [NSDictionary dictionaryWithObjectsAndKeys:
1846                         STATUS_NAME_AVAILABLE, KEY_STATUS_NAME,
1847                         STATUS_DESCRIPTION_AVAILABLE, KEY_STATUS_DESCRIPTION,
1848                         [NSNumber numberWithInt:AIAvailableStatusType], KEY_STATUS_TYPE,
1849                         nil];
1850         [builtInStatusTypes[AIAvailableStatusType] addObject:statusDict];
1852         builtInStatusTypes[AIAwayStatusType] = [[NSMutableSet alloc] init];
1853         statusDict = [NSDictionary dictionaryWithObjectsAndKeys:
1854                 STATUS_NAME_AWAY, KEY_STATUS_NAME,
1855                 STATUS_DESCRIPTION_AWAY, KEY_STATUS_DESCRIPTION,
1856                 [NSNumber numberWithInt:AIAwayStatusType], KEY_STATUS_TYPE,
1857                 nil];
1858         [builtInStatusTypes[AIAwayStatusType] addObject:statusDict];
1861 - (NSMenu *)statusStatesMenu
1863         NSMenu                  *statusStatesMenu = [[NSMenu allocWithZone:[NSMenu menuZone]] init];
1864         NSEnumerator    *enumerator;
1865         AIStatus                *statusState;
1866         AIStatusType    currentStatusType = AIAvailableStatusType;
1867         NSMenuItem              *menuItem;
1869         [statusStatesMenu setMenuChangedMessagesEnabled:NO];
1870         [statusStatesMenu setAutoenablesItems:NO];
1872         //Create a menu item for each state.  States must first be sorted such that states of the same AIStatusType
1873         //are grouped together.
1874         enumerator = [[self sortedFullStateArray] objectEnumerator];
1875         while(statusState = [enumerator nextObject]){
1876                 AIStatusType thisStatusType = [statusState statusType];
1878                 if(currentStatusType != thisStatusType){
1879                         //Add a divider between each type of status
1880                         [statusStatesMenu addItem:[NSMenuItem separatorItem]];
1881                         currentStatusType = thisStatusType;
1882                 }
1884                 menuItem = [[NSMenuItem alloc] initWithTitle:[self _titleForMenuDisplayOfState:statusState]
1885                                                                                           target:nil
1886                                                                                           action:nil
1887                                                                            keyEquivalent:@""];
1889                 //NSMenuItem will call setFlipped: on the image we pass it, causing flipped drawing elsewhere if we pass it the
1890                 //shared status icon.  So we pass it a copy of the shared icon that it's free to manipulate.
1891                 [menuItem setImage:[[[statusState icon] copy] autorelease]];
1892                 [menuItem setRepresentedObject:[NSDictionary dictionaryWithObject:statusState
1893                                                                                                                                    forKey:@"AIStatus"]];
1894                 [menuItem setTag:[statusState statusType]];
1895                 [statusStatesMenu addItem:menuItem];
1896                 [menuItem release];
1897         }
1899         [statusStatesMenu setMenuChangedMessagesEnabled:YES];
1901         return statusStatesMenu;
1904 #pragma mark Upgrade code
1906  * @brief Temporary upgrade code for 0.7x -> 0.8
1908  * Versions 0.7x and prior stored their away messages in a different format.  This code allows a seamless
1909  * transition from 0.7x to 0.8.  We can easily recognize the old format because the away messages are of
1910  * type "Away" instead of type "State", which is used for all 0.8 and later saved states.
1911  * Since we are changing the array as we scan it, an enumerator will not work here.
1912  */
1913 #define OLD_KEY_SAVED_AWAYS                     @"Saved Away Messages"
1914 #define OLD_GROUP_AWAY_MESSAGES         @"Away Messages"
1915 #define OLD_STATE_SAVED_AWAY            @"Away"
1916 #define OLD_STATE_AWAY                          @"Message"
1917 #define OLD_STATE_AUTO_REPLY            @"Autoresponse"
1918 #define OLD_STATE_TITLE                         @"Title"
1919 - (void)_upgradeSavedAwaysToSavedStates
1921         NSArray *savedAways = [[adium preferenceController] preferenceForKey:OLD_KEY_SAVED_AWAYS
1922                                                                                                                                    group:OLD_GROUP_AWAY_MESSAGES];
1924         if(savedAways){
1925                 NSEnumerator    *enumerator = [savedAways objectEnumerator];
1926                 NSDictionary    *state;
1928                 //Update all the away messages to states.
1929                 while(state = [enumerator nextObject]){
1930                         if([[state objectForKey:@"Type"] isEqualToString:OLD_STATE_SAVED_AWAY]){
1931                                 AIStatus        *statusState;
1933                                 //Extract the away message information from this old record
1934                                 NSData          *statusMessageData = [state objectForKey:OLD_STATE_AWAY];
1935                                 NSData          *autoReplyMessageData = [state objectForKey:OLD_STATE_AUTO_REPLY];
1936                                 NSString        *title = [state objectForKey:OLD_STATE_TITLE];
1938                                 //Create an AIStatus from this information
1939                                 statusState = [AIStatus status];
1941                                 //General category: It's an away type
1942                                 [statusState setStatusType:AIAwayStatusType];
1944                                 //Specific state: It's the generic away. Funny how that works out.
1945                                 [statusState setStatusName:STATUS_NAME_AWAY];
1947                                 //Set the status message (which is just the away message).
1948                                 [statusState setStatusMessage:[NSAttributedString stringWithData:statusMessageData]];
1950                                 //It has an auto reply.
1951                                 [statusState setHasAutoReply:YES];
1953                                 if(autoReplyMessageData){
1954                                         //Use the custom auto reply if it was set.
1955                                         [statusState setAutoReply:[NSAttributedString stringWithData:autoReplyMessageData]];
1956                                 }else{
1957                                         //If no autoReplyMesssage, use the status message.
1958                                         [statusState setAutoReplyIsStatusMessage:YES];
1959                                 }
1961                                 if(title) [statusState setTitle:title];
1963                                 //Add the updated state to our state array.
1964                                 [stateArray addObject:statusState];
1965                         }
1966                 }
1968                 //Save these changes and delete the old aways so we don't need to do this again.
1969                 [self _saveStateArrayAndNotifyOfChanges];
1970                 [[adium preferenceController] setPreference:nil
1971                                                                                          forKey:OLD_KEY_SAVED_AWAYS
1972                                                                                           group:OLD_GROUP_AWAY_MESSAGES];
1973         }
1976 @end