2 * Adium is the legal property of its developers, whose names are listed in the copyright file included
3 * with this source distribution.
5 * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
6 * General Public License as published by the Free Software Foundation; either version 2 of the License,
7 * or (at your option) any later version.
9 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
10 * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
11 * Public License for more details.
13 * You should have received a copy of the GNU General Public License along with this program; if not,
14 * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
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>
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
56 fromSet:(NSSet *)sourceArray
57 toArray:(NSMutableArray *)menuItems
58 alreadyAddedTitles:(NSMutableSet *)alreadyAddedTitles;
59 - (void)buildBuiltInStatusTypes;
64 * @class AIStatusController
65 * @brief Core status & state methods
67 * This class provides a foundation for Adium's status and status state systems.
69 @implementation AIStatusController
71 static NSMutableSet *temporaryStateArray = nil;
74 * Init the status controller
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];
87 accountsToConnect = [[NSMutableSet alloc] init];
88 isProcessingGlobalChange = NO;
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
100 [adiumNotificationCenter addObserver:self
101 selector:@selector(rebuildAllStateMenus)
102 name:AIStatusIconSetDidChangeNotification
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.
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;
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];
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];
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;
152 lastStatus = [NSKeyedUnarchiver unarchiveObjectWithData:lastStatusData];
156 AIStatus *existingStatus;
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
162 existingStatus = [self statusStateWithUniqueStatusID:[lastStatus uniqueStatusID]];
165 lastStatus = existingStatus;
167 //Add to our temporary status array
168 [temporaryStateArray addObject:lastStatus];
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;
175 [account setStatusStateAndRemainOffline:lastStatus];
179 if(needToRebuildMenus){
180 //Clear the sorted menu items array since our state array changed.
181 [_sortedFullStateArray release]; _sortedFullStateArray = nil;
183 //Now rebuild our menus to include this temporary item
184 [self rebuildAllStateMenus];
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).
200 NSMutableArray *savedAccountsToConnect = [NSMutableArray array];
201 NSEnumerator *enumerator;
204 enumerator = [[[adium accountController] accountArray] objectEnumerator];
205 while(account = [enumerator nextObject]){
207 //If this account is online, we'll want to save its internalObjectID.
208 if([account online]){
209 [savedAccountsToConnect addObject:[account internalObjectID]];
212 /* Store the current status state for use on next launch.
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] :
223 group:GROUP_ACCOUNT_STATUS];
226 [[adium preferenceController] setPreference:savedAccountsToConnect
227 forKey:@"SavedAccountsToConnect"
228 group:GROUP_ACCOUNT_STATUS];
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
238 - (void)closeController
240 [[adium notificationCenter] removeObserver:self];
241 [[adium preferenceController] unregisterPreferenceObserver:self];
242 [[adium contactController] unregisterListObjectObserver:self];
250 [stateArray release]; stateArray = nil;
251 [_sortedFullStateArray release]; _sortedFullStateArray = nil;
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
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];
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,
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
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];
308 for(type = AIAvailableStatusType ; type < STATUS_TYPES_COUNT ; type++){
309 NSArray *menuItemArray;
311 menuItemArray = [self _menuItemsForStatusesOfType:type
312 forServiceCodeUniqueID:serviceCodeUniqueID
315 //Add a separator between each type after available
316 if ((type > AIAvailableStatusType) && [menuItemArray count]){
317 [menu addItem:[NSMenuItem separatorItem]];
320 //Add the items for this type
321 enumerator = [menuItemArray objectEnumerator];
322 while(menuItem = [enumerator nextObject]){
323 [menu addItem:menuItem];
327 return [menu autorelease];
331 * @brief Sort status menu items
333 * Sort alphabetically by title.
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.
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
360 fromSet:builtInStatusTypes[type]
362 alreadyAddedTitles:alreadyAddedTitles];
364 //Now, add items for this service, or from all available services, as appropriate
365 if(inServiceCodeUniqueID){
368 //Obtain the status dicts for this type and service code unique ID
369 if(statusDicts = [statusDictsByServiceCodeUniqueID[type] objectForKey:inServiceCodeUniqueID]){
371 [self _addMenuItemsForStatusOfType:type
375 alreadyAddedTitles:alreadyAddedTitles];
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]])){
392 //Obtain the status dicts for this type and service code unique ID
393 if(statusDicts = [statusDictsByServiceCodeUniqueID[type] objectForKey:serviceCodeUniqueID]){
395 [self _addMenuItemsForStatusOfType:type
399 alreadyAddedTitles:alreadyAddedTitles];
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.
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];
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
437 if(![alreadyAddedTitles containsObject:title]){
439 NSMenuItem *menuItem;
441 menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:title
443 action:@selector(selectStatus:)
446 image = [[[AIStatusIcons statusIconForStatusName:[statusDict objectForKey:KEY_STATUS_NAME]
448 iconType:AIStatusIconList
449 direction:AIIconNormal] copy] autorelease];
451 [menuItem setRepresentedObject:statusDict];
452 [menuItem setImage:image];
453 [menuItem setEnabled:YES];
454 [menuItems addObject:menuItem];
457 [alreadyAddedTitles addObject:title];
462 #pragma mark Status State Descriptions
464 * @brief Return the localized description for the sate of the passed status
466 * This could be stored with the statusState, but that would break if the locale changed. This way, the nonlocalized
467 * string is used to look up the appropriate localized one.
469 * @result A localized description such as @"Away" or @"Out to Lunch" of the state used by statusState
471 - (NSString *)descriptionForStateOfStatus:(AIStatus *)statusState
473 NSString *statusName = [statusState statusName];
474 AIStatusType statusType = [statusState statusType];
475 NSEnumerator *enumerator = [statusDictsByServiceCodeUniqueID[statusType] objectEnumerator];
477 while(set = [enumerator nextObject]){
478 NSEnumerator *statusDictsEnumerator = [set objectEnumerator];
479 NSDictionary *statusDict;
480 while(statusDict = [statusDictsEnumerator nextObject]){
481 if([[statusDict objectForKey:KEY_STATUS_NAME] isEqualToString:statusName]){
482 return [statusDict objectForKey:KEY_STATUS_DESCRIPTION];
490 - (NSString *)localizedDescriptionForCoreStatusName:(NSString *)statusName
492 static NSDictionary *coreLocalizedStatusDescriptions = nil;
493 if(!coreLocalizedStatusDescriptions){
494 coreLocalizedStatusDescriptions = [[NSDictionary dictionaryWithObjectsAndKeys:
495 STATUS_DESCRIPTION_AVAILABLE, STATUS_NAME_AVAILABLE,
496 STATUS_DESCRIPTION_FREE_FOR_CHAT, STATUS_NAME_FREE_FOR_CHAT,
497 STATUS_DESCRIPTION_AVAILABLE_FRIENDS_ONLY, STATUS_NAME_AVAILABLE_FRIENDS_ONLY,
498 STATUS_DESCRIPTION_AWAY, STATUS_NAME_AWAY,
499 STATUS_DESCRIPTION_EXTENDED_AWAY, STATUS_NAME_EXTENDED_AWAY,
500 STATUS_DESCRIPTION_AWAY_FRIENDS_ONLY, STATUS_NAME_AWAY_FRIENDS_ONLY,
501 STATUS_DESCRIPTION_DND, STATUS_NAME_DND,
502 STATUS_DESCRIPTION_NOT_AVAILABLE, STATUS_NAME_NOT_AVAILABLE,
503 STATUS_DESCRIPTION_OCCUPIED, STATUS_NAME_OCCUPIED,
504 STATUS_DESCRIPTION_BRB, STATUS_NAME_BRB,
505 STATUS_DESCRIPTION_BUSY, STATUS_NAME_BUSY,
506 STATUS_DESCRIPTION_PHONE, STATUS_NAME_PHONE,
507 STATUS_DESCRIPTION_LUNCH, STATUS_NAME_LUNCH,
508 STATUS_DESCRIPTION_NOT_AT_HOME, STATUS_NAME_NOT_AT_HOME,
509 STATUS_DESCRIPTION_NOT_AT_DESK, STATUS_NAME_NOT_AT_DESK,
510 STATUS_DESCRIPTION_NOT_IN_OFFICE, STATUS_NAME_NOT_IN_OFFICE,
511 STATUS_DESCRIPTION_VACATION, STATUS_NAME_VACATION,
512 STATUS_DESCRIPTION_STEPPED_OUT, STATUS_NAME_STEPPED_OUT,
513 STATUS_DESCRIPTION_INVISIBLE, STATUS_NAME_INVISIBLE,
514 STATUS_DESCRIPTION_OFFLINE, STATUS_NAME_OFFLINE,
518 return([coreLocalizedStatusDescriptions objectForKey:statusName]);
524 * @brief The status name to use by default for a passed type
526 * This is the name which will be used for new AIStatus objects of this type.
528 - (NSString *)defaultStatusNameForType:(AIStatusType)statusType
530 //Set the default status name
532 case AIAvailableStatusType:
533 return STATUS_NAME_AVAILABLE;
535 case AIAwayStatusType:
536 return STATUS_NAME_AWAY;
538 case AIInvisibleStatusType:
539 return STATUS_NAME_INVISIBLE;
541 case AIOfflineStatusType:
542 return STATUS_NAME_OFFLINE;
549 #pragma mark Setting Status States
551 * @brief Set the active status state
553 * Sets the currently active status state. This applies throughout Adium and to all accounts. The state will become
554 * effective immediately.
556 - (void)setActiveStatusState:(AIStatus *)statusState
558 //Apply the state to our accounts and notify (delay to the next run loop to improve perceived speed)
559 [self performSelector:@selector(applyState:toAccounts:)
560 withObject:statusState
561 withObject:[[adium accountController] accountArray]
566 * @brief Return the <tt>AIStatus</tt> to be used by accounts as they are created
568 - (AIStatus *)defaultInitialStatusState
570 return [[self builtInStateArray] objectAtIndex:0];
574 * @brief Reset the active status state
576 * All active status states cache will also reset. Posts an active status changed notification. The active state
577 * will be regenerated the next time it is requested.
579 - (void)_resetActiveStatusState
581 //Clear the active status state. It will be rebuilt next time it is requested
582 [_activeStatusState release]; _activeStatusState = nil;
583 [_allActiveStatusStates release]; _allActiveStatusStates = nil;
585 //Let observers know the active state has changed
586 [[adium notificationCenter] postNotificationName:AIStatusActiveStateChangedNotification object:nil];
590 * @brief Apply a state to multiple accounts
592 - (void)applyState:(AIStatus *)statusState toAccounts:(NSArray *)accountArray
594 NSEnumerator *enumerator;
596 AIStatus *aStatusState;
597 BOOL shouldRebuild = NO;
598 BOOL noConnectedAccounts = ![[adium accountController] oneOrMoreConnectedAccounts];
600 //We should connect all accounts if our accounts to connect array is empty and there are no connected accounts
601 BOOL shouldConnectAllAccounts = (([accountsToConnect count] == 0) &&
602 noConnectedAccounts);
604 isProcessingGlobalChange = YES;
605 [self setDelayStateMenuUpdates:YES];
607 enumerator = [accountArray objectEnumerator];
608 while(account = [enumerator nextObject]){
609 if([account online] ||
610 (noConnectedAccounts && [accountsToConnect containsObject:account]) ||
611 (shouldConnectAllAccounts)){
612 /* If this account is online, or no accounts are online and this is an account to connect,
613 * or we should be connecting all accounts, set the status completely. */
614 [account setStatusState:statusState];
616 //If this account should not have its state set now, perform internal bookkeeping so a future sign-on
617 //will be to the most appropriate state
618 [account setStatusStateAndRemainOffline:statusState];
622 //Any objects in the temporary state array which aren't the state we just set should now be removed.
623 enumerator = [[[temporaryStateArray copy] autorelease] objectEnumerator];
624 while(aStatusState = [enumerator nextObject]){
625 if(aStatusState != statusState){
626 [temporaryStateArray removeObject:aStatusState];
631 isProcessingGlobalChange = NO;
634 //Manually decrease the update delays counter as we don't want to call [self updateAllStateMenuSelections]
635 stateMenuUpdateDelays--;
636 [self rebuildAllStateMenus];
638 /* Allow setDelayStateMenuUpdates to decreate the counter and call
639 * [self updateAllStateMenuSelections] as appropriate.
641 [self setDelayStateMenuUpdates:NO];
645 #pragma mark Retrieving Status States
647 * @brief Access to Adium's user-defined states
649 * Returns an array of available user-defined states, which are AIStatus objects
651 - (NSArray *)stateArray
654 NSData *savedStateArrayData = [[adium preferenceController] preferenceForKey:KEY_SAVED_STATUS
655 group:PREF_GROUP_SAVED_STATUS];
656 if(savedStateArrayData){
657 stateArray = [[NSKeyedUnarchiver unarchiveObjectWithData:savedStateArrayData] mutableCopy];
660 if(!stateArray) stateArray = [[NSMutableArray alloc] init];
662 //Upgrade Adium 0.7x away messages
663 [self _upgradeSavedAwaysToSavedStates];
670 * @brief Return the array of built-in states
672 * These are basic Available and Away states which should always be visible and are (by convention) immutable.
673 * The first state in BUILT_IN_STATE_ARRAY will be used as the default for accounts as they are created.
675 - (NSArray *)builtInStateArray
677 if(!builtInStateArray){
678 NSArray *savedBuiltInStateArray = [NSArray arrayNamed:BUILT_IN_STATE_ARRAY forClass:[self class]];
679 NSEnumerator *enumerator;
682 builtInStateArray = [[NSMutableArray alloc] initWithCapacity:[savedBuiltInStateArray count]];
684 enumerator = [savedBuiltInStateArray objectEnumerator];
685 while(dict = [enumerator nextObject]){
686 AIStatus *status = [AIStatus statusWithDictionary:dict];
687 [builtInStateArray addObject:status];
689 //Store a reference to our offline state if we just loaded it
690 if([status statusType] == AIOfflineStatusType){
691 [offlineStatusState release];
692 offlineStatusState = [status retain];
697 return(builtInStateArray);
700 - (AIStatus *)offlineStatusState
702 //Ensure the built in states have been loaded
703 [self builtInStateArray];
705 NSAssert(offlineStatusState != nil, @"Nil offline status state");
706 return offlineStatusState;
709 //Sort the status array
710 int _statusArraySort(id objectA, id objectB, void *context)
712 AIStatusType statusTypeA = [objectA statusType];
713 AIStatusType statusTypeB = [objectB statusType];
715 //We treat Invisible statuses as being the same as Away for purposes of the menu
716 if(statusTypeA == AIInvisibleStatusType) statusTypeA = AIAwayStatusType;
717 if(statusTypeB == AIInvisibleStatusType) statusTypeB = AIAwayStatusType;
719 if(statusTypeA > statusTypeB){
720 return NSOrderedDescending;
721 }else if(statusTypeB > statusTypeA){
722 return NSOrderedAscending;
724 BOOL isLockedMutabilityTypeA = ([objectA mutabilityType] == AILockedStatusState);
725 BOOL isLockedMutabilityTypeB = ([objectB mutabilityType] == AILockedStatusState);
727 //Put locked (built in) statuses at the top
728 if(isLockedMutabilityTypeA && !isLockedMutabilityTypeB){
729 return NSOrderedAscending;
731 }else if(!isLockedMutabilityTypeA && isLockedMutabilityTypeB){
732 return NSOrderedDescending;
735 /* Check to see if either is temporary; temporary items go above saved ones and below
738 BOOL isTemporaryA = [temporaryStateArray containsObject:objectA];
739 BOOL isTemporaryB = [temporaryStateArray containsObject:objectB];
741 if(isTemporaryA && !isTemporaryB){
742 return NSOrderedAscending;
744 }else if(isTemporaryB && !isTemporaryA){
745 return NSOrderedDescending;
748 NSArray *originalArray = (NSArray *)context;
750 //Return them in the same relative order as the original array if they are of the same type
751 int indexA = [originalArray indexOfObjectIdenticalTo:objectA];
752 int indexB = [originalArray indexOfObjectIdenticalTo:objectB];
755 return NSOrderedDescending;
757 return NSOrderedAscending;
765 * @brief Return a sorted state array for use in menu item creation
767 * The array is created by adding the built in states to the user states, then sorting using _statusArraySort
769 * @result A cached NSArray which is sorted by status type (available, away), built-in vs. user-made, and then original ordering.
771 - (NSArray *)sortedFullStateArray
773 if(!_sortedFullStateArray){
774 NSArray *originalStateArray = [self stateArray];
775 NSMutableArray *tempArray = [originalStateArray mutableCopy];
776 [tempArray addObjectsFromArray:[self builtInStateArray]];
777 [tempArray addObjectsFromArray:[temporaryStateArray allObjects]];
779 //Pass the original array so its indexes can be used for comparison of saved state ordering
780 [tempArray sortUsingFunction:_statusArraySort context:originalStateArray];
782 _sortedFullStateArray = tempArray;
785 return _sortedFullStateArray;
789 * @brief Retrieve active status state
791 * @result The currently active status state.
793 * This is defined as the status state which the most accounts are currently using. The behavior in case of a tie
794 * is currently undefined but will yield one of the tying states.
796 - (AIStatus *)activeStatusState
798 if(!_activeStatusState){
799 NSEnumerator *enumerator = [[[adium accountController] accountArray] objectEnumerator];
800 NSCountedSet *statusCounts = [NSCountedSet set];
802 AIStatus *statusState;
803 unsigned highestCount = 0;
804 BOOL accountsAreOnline = [[adium accountController] oneOrMoreConnectedOrConnectingAccounts];
806 if(accountsAreOnline){
807 AIStatus *bestStatusState = nil;
809 while((account = [enumerator nextObject])) {
810 if([account online]){
811 AIStatus *accountStatusState = [account statusState];
812 [statusCounts addObject:(accountStatusState ?
814 [self defaultInitialStatusState])];
818 enumerator = [statusCounts objectEnumerator];
819 while((statusState = [enumerator nextObject])) {
820 unsigned thisCount = [statusCounts countForObject:statusState];
821 if(thisCount > highestCount){
822 bestStatusState = statusState;
823 highestCount = thisCount;
827 _activeStatusState = [bestStatusState retain];
829 _activeStatusState = [offlineStatusState retain];
833 return _activeStatusState;
837 * @brief Find the 'active' AIStatusType
839 * The active type is the one used by the largest number of accounts. In case of a tie, the order of the AIStatusType
842 * @param invisibleIsAway If YES, AIInvisibleStatusType is trated as AIAwayStatusType
843 * @result The active AIStatusType for online accounts, or AIOfflineStatusType if all accounts are offline
845 - (AIStatusType)activeStatusTypeTreatingInvisibleAsAway:(BOOL)invisibleIsAway
847 NSEnumerator *enumerator = [[[adium accountController] accountArray] objectEnumerator];
849 int statusTypeCount[STATUS_TYPES_COUNT];
850 AIStatusType activeStatusType = AIOfflineStatusType;
851 unsigned highestCount = 0;
854 for(i = 0 ; i < STATUS_TYPES_COUNT ; i++){
855 statusTypeCount[i] = 0;
858 while(account = [enumerator nextObject]){
859 if([account online] || [account integerStatusObjectForKey:@"Connecting"]){
860 AIStatusType statusType = [[account statusState] statusType];
862 //If invisibleIsAway, pretend that invisible is away
863 if (invisibleIsAway && (statusType == AIInvisibleStatusType)) statusType = AIAwayStatusType;
865 statusTypeCount[statusType]++;
869 for(i = 0 ; i < STATUS_TYPES_COUNT ; i++){
870 if(statusTypeCount[i] > highestCount){
871 activeStatusType = i;
872 highestCount = statusTypeCount[i];
876 return activeStatusType;
880 * @brief All active status states
882 * A status state is active if any online account is currently in that state.
884 * The return value of this method is cached.
886 * @result An <tt>NSSet</tt> of <tt>AIStatus</tt> objects
888 - (NSSet *)allActiveStatusStates
890 if(!_allActiveStatusStates){
891 _allActiveStatusStates = [[NSMutableSet alloc] init];
892 NSEnumerator *enumerator = [[[adium accountController] accountArray] objectEnumerator];
895 while(account = [enumerator nextObject]){
896 if([account online] || [account integerStatusObjectForKey:@"Connecting"]){
897 [_allActiveStatusStates addObject:[account statusState]];
902 return _allActiveStatusStates;
906 * @brief Return the set of all unavailable statuses in use by online or connection accounts
908 * @param activeUnvailableStatusType Pointer to an AIStatusType; returns by reference the most popular unavailable type
909 * @param activeUnvailableStatusName Pointer to an NSString*; returns by reference a status name if all states are in the same name, or nil if they differ
910 * @param allOnlineAccountsAreUnvailable Pointer to a BOOL; returns by reference YES is all online accounts are unavailable, NO if one or more is available
912 - (NSSet *)activeUnavailableStatusesAndType:(AIStatusType *)activeUnvailableStatusType withName:(NSString **)activeUnvailableStatusName allOnlineAccountsAreUnvailable:(BOOL *)allOnlineAccountsAreUnvailable
914 NSEnumerator *enumerator = [[[adium accountController] accountArray] objectEnumerator];
916 NSMutableSet *activeUnvailableStatuses = [NSMutableSet set];
917 BOOL foundStatusName = NO;
918 int statusTypeCount[STATUS_TYPES_COUNT];
920 statusTypeCount[AIAwayStatusType] = 0;
921 statusTypeCount[AIInvisibleStatusType] = 0;
923 //Assume all accounts are unavailable until proven otherwise
924 if(allOnlineAccountsAreUnvailable != NULL){
925 *allOnlineAccountsAreUnvailable = YES;
928 while(account = [enumerator nextObject]){
929 if([account online] || [account integerStatusObjectForKey:@"Connecting"]){
930 AIStatus *statusState = [account statusState];
931 AIStatusType statusType = [statusState statusType];
933 if((statusType == AIAwayStatusType) || (statusType == AIInvisibleStatusType)){
934 NSString *statusName = [statusState statusName];
936 [activeUnvailableStatuses addObject:statusState];
938 statusTypeCount[statusType]++;
941 //Once we find a status name, we only want to return it if all our status names are the same.
942 if((activeUnvailableStatusName != NULL) &&
943 (*activeUnvailableStatusName != nil) &&
944 ![*activeUnvailableStatusName isEqualToString:statusName]){
945 *activeUnvailableStatusName = nil;
948 //We haven't found a status name yet, so store this one as the active status name
949 if(activeUnvailableStatusName != NULL){
950 *activeUnvailableStatusName = [statusState statusName];
952 foundStatusName = YES;
955 //An online account isn't unavailable
956 if(allOnlineAccountsAreUnvailable != NULL){
957 *allOnlineAccountsAreUnvailable = NO;
963 if(activeUnvailableStatusType != NULL){
964 if(statusTypeCount[AIAwayStatusType] > statusTypeCount[AIInvisibleStatusType]){
965 *activeUnvailableStatusType = AIAwayStatusType;
967 *activeUnvailableStatusType = AIInvisibleStatusType;
971 return activeUnvailableStatuses;
976 * @brief Next available unique status ID
978 - (NSNumber *)nextUniqueStatusID
980 NSNumber *nextUniqueStatusID;
982 //Retain and autorelease since we'll be replacing this value (and therefore releasing it) via the preferenceController.
983 nextUniqueStatusID = [[[[adium preferenceController] preferenceForKey:TOP_STATUS_STATE_ID
984 group:PREF_GROUP_SAVED_STATUS] retain] autorelease];
985 if(!nextUniqueStatusID) nextUniqueStatusID = [NSNumber numberWithInt:1];
987 [[adium preferenceController] setPreference:[NSNumber numberWithInt:([nextUniqueStatusID intValue] + 1)]
988 forKey:TOP_STATUS_STATE_ID
989 group:PREF_GROUP_SAVED_STATUS];
991 return nextUniqueStatusID;
995 * @brief Find the status state with the requested uniqueStatusID
997 - (AIStatus *)statusStateWithUniqueStatusID:(NSNumber *)uniqueStatusID
999 AIStatus *statusState = nil;
1002 NSEnumerator *enumerator = [[self sortedFullStateArray] objectEnumerator];
1004 while(statusState = [enumerator nextObject]){
1005 if([[statusState uniqueStatusID] compare:uniqueStatusID] == NSOrderedSame)
1013 //State Editing --------------------------------------------------------------------------------------------------------
1014 #pragma mark State Editing
1016 * @brief Add a state
1018 * Add a new state to Adium's state array.
1019 * @param state AIState to add
1021 - (void)addStatusState:(AIStatus *)statusState
1023 [stateArray addObject:statusState];
1024 [self _saveStateArrayAndNotifyOfChanges];
1028 * @brief Remove a state
1030 * Remove a new state from Adium's state array.
1031 * @param state AIStatus to remove
1033 - (void)removeStatusState:(AIStatus *)statusState
1035 [stateArray removeObject:statusState];
1036 [self _saveStateArrayAndNotifyOfChanges];
1040 * @brief Move a state
1042 * Move a state that already exists in Adium's state array to another index
1043 * @param state AIStatus to move
1044 * @param destIndex Destination index
1046 - (int)moveStatusState:(AIStatus *)statusState toIndex:(int)destIndex
1048 int sourceIndex = [stateArray indexOfObjectIdenticalTo:statusState];
1051 [statusState retain];
1052 [stateArray removeObject:statusState];
1054 //Re-insert the state
1055 if(destIndex > sourceIndex) destIndex -= 1;
1056 [stateArray insertObject:statusState atIndex:destIndex];
1057 [statusState release];
1059 [self _saveStateArrayAndNotifyOfChanges];
1065 * @brief Replace a state
1067 * Replace a state in Adium's state array with another state.
1068 * @param oldState AIStatus state that is in Adium's state array
1069 * @param newState AIStatus state with which to replace oldState
1071 - (void)replaceExistingStatusState:(AIStatus *)oldStatusState withStatusState:(AIStatus *)newStatusState
1073 if(oldStatusState != newStatusState){
1074 int index = [stateArray indexOfObject:oldStatusState];
1076 if(index >= 0 && index < [stateArray count]){
1077 [stateArray replaceObjectAtIndex:index withObject:newStatusState];
1081 [self _saveStateArrayAndNotifyOfChanges];
1085 * @brief Save changes to the state array and notify observers
1087 * Saves any outstanding changes to the state array. There should be no need to call this manually, since all the
1088 * state array modifying methods in this class call it automatically after making changes.
1090 * After the state array is saved, observers are notified that is has changed. Call after making any changes to the
1091 * state array from within the controller.
1093 - (void)_saveStateArrayAndNotifyOfChanges
1095 //Clear the sorted menu items array since our state array changed.
1096 [_sortedFullStateArray release]; _sortedFullStateArray = nil;
1098 [[adium preferenceController] setPreference:[NSKeyedArchiver archivedDataWithRootObject:[self stateArray]]
1099 forKey:KEY_SAVED_STATUS
1100 group:PREF_GROUP_SAVED_STATUS];
1101 [[adium notificationCenter] postNotificationName:AIStatusStateArrayChangedNotification object:nil];
1104 - (void)statusStateDidSetUniqueStatusID
1106 [[adium preferenceController] setPreference:[NSKeyedArchiver archivedDataWithRootObject:[self stateArray]]
1107 forKey:KEY_SAVED_STATUS
1108 group:PREF_GROUP_SAVED_STATUS];
1111 //Machine Activity -----------------------------------------------------------------------------------------------------
1112 #pragma mark Machine Activity
1113 #define MACHINE_IDLE_THRESHOLD 30 //30 seconds of inactivity is considered idle
1114 #define MACHINE_ACTIVE_POLL_INTERVAL 30 //Poll every 60 seconds when the user is active
1115 #define MACHINE_IDLE_POLL_INTERVAL 1 //Poll every second when the user is idle
1117 //Private idle function
1118 extern double CGSSecondsSinceLastInputEvent(unsigned long evType);
1121 * @brief Returns the current machine idle time
1123 * Returns the current number of seconds the machine has been idle. The machine is idle when there are no input
1124 * events from the user (such as mouse movement or keyboard input). In addition to this method, the status controller
1125 * sends out notifications when the machine becomes idle, stays idle, and returns to an active state.
1127 - (double)currentMachineIdle
1129 double idleTime = CGSSecondsSinceLastInputEvent(-1);
1131 //On MDD Powermacs, the above function will return a large value when the machine is active (perhaps a -1?).
1132 //Here we check for that value and correctly return a 0 idle time.
1133 if(idleTime >= 18446744000.0) idleTime = 0.0; //18446744073.0 is the lowest I've seen on my MDD -ai
1139 * @brief Timer that checkes for machine idle
1141 * This timer periodically checks the machine for inactivity. When the machine has been inactive for atleast
1142 * MACHINE_IDLE_THRESHOLD seconds, a notification is broadcast.
1144 * When the machine is active, this timer is called infrequently. It's not important to notice that the user went
1145 * idle immediately, so we relax our CPU usage while waiting for an idle state to begin.
1147 * When the machine is idle, the timer is called frequently. It's important to notice immediately when the user
1150 - (void)_idleCheckTimer:(NSTimer *)inTimer
1152 double currentIdle = [self currentMachineIdle];
1155 if(currentIdle < lastSeenIdle){
1156 //If the machine is less idle than the last time we recorded, it means that activity has occured and the
1157 //user is no longer idle.
1158 [self _setMachineIsIdle:NO];
1160 //Periodically broadcast a 'MachineIdleUpdate' notification
1161 [[adium notificationCenter] postNotificationName:AIMachineIdleUpdateNotification
1163 userInfo:[NSDictionary dictionaryWithObjectsAndKeys:
1164 [NSNumber numberWithDouble:currentIdle], @"Duration",
1165 [NSDate dateWithTimeIntervalSinceNow:-currentIdle], @"IdleSince",
1169 //If machine inactivity is over the threshold, the user has gone idle.
1170 if(currentIdle > MACHINE_IDLE_THRESHOLD) [self _setMachineIsIdle:YES];
1173 lastSeenIdle = currentIdle;
1177 * @brief Sets the machine as idle or not
1179 * This internal method updates the frequency of our idle timer depending on whether the machine is considered
1180 * idle or not. It also posts the AIMachineIsIdleNotification and AIMachineIsActiveNotification notifications
1181 * based on the passed idle state
1183 - (void)_setMachineIsIdle:(BOOL)inIdle
1185 machineIsIdle = inIdle;
1187 //Post the appropriate idle or active notification
1189 [[adium notificationCenter] postNotificationName:AIMachineIsIdleNotification object:nil];
1191 [[adium notificationCenter] postNotificationName:AIMachineIsActiveNotification object:nil];
1194 //Update our timer interval for either idle or active polling
1195 [idleTimer invalidate];
1196 [idleTimer release];
1197 idleTimer = [[NSTimer scheduledTimerWithTimeInterval:(machineIsIdle ? MACHINE_IDLE_POLL_INTERVAL : MACHINE_ACTIVE_POLL_INTERVAL)
1199 selector:@selector(_idleCheckTimer:)
1201 repeats:YES] retain];
1205 //Status state menu support ---------------------------------------------------------------------------------------------------
1206 #pragma mark Status state menu support
1208 * @brief Register a state menu plugin
1210 * A state menu plugin is the mitigator between our state menu items and a menu. As states change the plugin
1211 * is told to add and remove items from the menu. Everything else is handled by the status controller.
1212 * @param stateMenuPlugin The state menu plugin to register
1214 - (void)registerStateMenuPlugin:(id <StateMenuPlugin>)stateMenuPlugin
1216 NSNumber *identifier = [NSNumber numberWithInt:[stateMenuPlugin hash]];
1219 [stateMenuItemArraysDict setObject:[NSMutableArray array] forKey:identifier];
1220 [stateMenuPluginsArray addObject:stateMenuPlugin];
1222 //Start it out with a fresh set of menu items
1223 [self _addStateMenuItemsForPlugin:stateMenuPlugin];
1227 * @brief Unregister a state menu plugin
1229 * All state menu items will be removed from the plugin when it unregisters
1230 * @param stateMenuPlugin The state menu plugin to unregister
1232 - (void)unregisterStateMenuPlugin:(id <StateMenuPlugin>)stateMenuPlugin
1234 NSNumber *identifier = [NSNumber numberWithInt:[stateMenuPlugin hash]];
1236 //Remove all the plugin's menu items
1237 [self _removeStateMenuItemsForPlugin:stateMenuPlugin];
1239 //Stop tracking the plugin
1240 [stateMenuItemArraysDict removeObjectForKey:identifier];
1241 [stateMenuPluginsArray removeObjectIdenticalTo:stateMenuPlugin];
1245 * @brief Remove the status controller's tracking for a plugin's menu items
1247 * This should be called in preparation for one or more plugin:didAddMenuItems: calls to clear out the current
1248 * tracking for the statusController generated menu items.
1250 - (void)removeAllMenuItemsForPlugin:(id <StateMenuPlugin>)stateMenuPlugin
1252 NSNumber *identifier = [NSNumber numberWithInt:[stateMenuPlugin hash]];
1253 NSMutableArray *menuItemArray = [stateMenuItemArraysDict objectForKey:identifier];
1256 [menuItemArray removeAllObjects];
1260 * @brief A plugin created its own menu items it wants us to track and update
1262 - (void)plugin:(id <StateMenuPlugin>)stateMenuPlugin didAddMenuItems:(NSArray *)addedMenuItems
1264 NSNumber *identifier = [NSNumber numberWithInt:[stateMenuPlugin hash]];
1265 NSMutableArray *menuItemArray = [stateMenuItemArraysDict objectForKey:identifier];
1267 [menuItemArray addObjectsFromArray:addedMenuItems];
1268 [stateMenuItemsNeedingUpdating addObjectsFromArray:addedMenuItems];
1272 * @brief Add state menu items
1274 * Adds all the necessary state menu items to a plugin's state menu
1275 * @param stateMenuPlugin The state menu plugin we're updating
1277 - (void)_addStateMenuItemsForPlugin:(id <StateMenuPlugin>)stateMenuPlugin
1279 NSNumber *identifier = [NSNumber numberWithInt:[stateMenuPlugin hash]];
1280 NSMutableArray *menuItemArray = [stateMenuItemArraysDict objectForKey:identifier];
1281 NSEnumerator *enumerator;
1282 NSMenuItem *menuItem;
1283 AIStatus *statusState;
1284 AIStatusType currentStatusType = AIAvailableStatusType;
1286 //Create a menu item for each state. States must first be sorted such that states of the same AIStatusType
1287 //are grouped together.
1288 enumerator = [[self sortedFullStateArray] objectEnumerator];
1289 while(statusState = [enumerator nextObject]){
1290 AIStatusType thisStatusType = [statusState statusType];
1292 //We treat Invisible statuses as being the same as Away for purposes of the menu
1293 if(thisStatusType == AIInvisibleStatusType) thisStatusType = AIAwayStatusType;
1295 //Add the "Custom..." state option and a separatorItem before beginning to add items for a new statusType
1296 if((currentStatusType != thisStatusType) &&
1297 (currentStatusType != AIOfflineStatusType)){
1298 menuItem = [[NSMenuItem alloc] initWithTitle:STATUS_TITLE_CUSTOM
1300 action:@selector(selectCustomState:)
1303 [menuItem setImage:[[[AIStatusIcons statusIconForStatusName:nil
1304 statusType:currentStatusType
1305 iconType:AIStatusIconList
1306 direction:AIIconNormal] copy] autorelease]];
1307 [menuItem setTag:currentStatusType];
1308 [menuItemArray addObject:menuItem];
1312 [menuItemArray addObject:[NSMenuItem separatorItem]];
1314 currentStatusType = thisStatusType;
1317 menuItem = [[NSMenuItem alloc] initWithTitle:[self _titleForMenuDisplayOfState:statusState]
1319 action:@selector(selectState:)
1322 //NSMenuItem will call setFlipped: on the image we pass it, causing flipped drawing elsewhere if we pass it the
1323 //shared status icon. So we pass it a copy of the shared icon that it's free to manipulate.
1324 [menuItem setImage:[[[statusState icon] copy] autorelease]];
1325 [menuItem setTag:currentStatusType];
1326 if([NSApp isOnPantherOrBetter]){
1327 [menuItem setToolTip:[statusState statusMessageString]];
1329 [menuItem setRepresentedObject:[NSDictionary dictionaryWithObject:statusState
1330 forKey:@"AIStatus"]];
1331 [menuItemArray addObject:menuItem];
1335 if(currentStatusType != AIOfflineStatusType){
1336 /* Add the last "Custom..." state optior for the last statusType we handled,
1337 * which didn't get a "Custom..." item yet. At present, our last status type should always be
1338 * our AIOfflineStatusType, so this will never be executed and just exists for completeness. */
1339 menuItem = [[NSMenuItem alloc] initWithTitle:STATUS_TITLE_CUSTOM
1341 action:@selector(selectCustomState:)
1343 [menuItem setImage:[[[AIStatusIcons statusIconForStatusName:nil
1344 statusType:currentStatusType
1345 iconType:AIStatusIconList
1346 direction:AIIconNormal] copy] autorelease]];
1347 [menuItem setTag:currentStatusType];
1348 [menuItemArray addObject:menuItem];
1352 //Now that we are done creating the menu items, tell the plugin about them
1353 [stateMenuPlugin addStateMenuItems:menuItemArray];
1355 //Update the selected menu item after giving the plugin a chance to do with the menu items as it wants
1356 [self updateStateMenuSelectionForPlugin:stateMenuPlugin];
1360 * @brief Removes state menu items
1362 * Removes all the state menu items from a plugin's state menu
1363 * @param stateMenuPlugin The state menu plugin we're updating
1365 - (void)_removeStateMenuItemsForPlugin:(id <StateMenuPlugin>)stateMenuPlugin
1367 NSNumber *identifier = [NSNumber numberWithInt:[stateMenuPlugin hash]];
1368 NSMutableArray *menuItemArray = [stateMenuItemArraysDict objectForKey:identifier];
1370 //Inform the plugin that we are removing the items in this array
1371 [stateMenuPlugin removeStateMenuItems:menuItemArray];
1373 //Now clear the array
1374 [menuItemArray removeAllObjects];
1378 * @brief Completely rebuild all state menus
1380 * Before doing so, clear the activeStatusState, which will be regenerated when next needed
1382 - (void)rebuildAllStateMenus
1384 //Clear the sorted menu items array since our state array changed.
1385 [_sortedFullStateArray release]; _sortedFullStateArray = nil;
1387 [self _resetActiveStatusState];
1389 NSEnumerator *enumerator = [stateMenuPluginsArray objectEnumerator];
1390 id <StateMenuPlugin> stateMenuPlugin;
1392 while(stateMenuPlugin = [enumerator nextObject]) {
1393 [self _removeStateMenuItemsForPlugin:stateMenuPlugin];
1394 [self _addStateMenuItemsForPlugin:stateMenuPlugin];
1399 * @brief Completely rebuild all state menus for a single plugin
1401 - (void)rebuildAllStateMenusForPlugin:(id <StateMenuPlugin>)stateMenuPlugin
1403 [self _removeStateMenuItemsForPlugin:stateMenuPlugin];
1404 [self _addStateMenuItemsForPlugin:stateMenuPlugin];
1408 * @brief Update the selected state in all state menus
1410 - (void)updateAllStateMenuSelections
1412 if(stateMenuUpdateDelays == 0){
1413 NSEnumerator *enumerator = [stateMenuPluginsArray objectEnumerator];
1414 id <StateMenuPlugin> stateMenuPlugin;
1416 while(stateMenuPlugin = [enumerator nextObject]){
1417 [self updateStateMenuSelectionForPlugin:stateMenuPlugin];
1420 [self _resetActiveStatusState];
1422 /* Let any relevant plugins respond to the to-be-changed state menu selection. Technically we
1423 * haven't changed it yet, since we'll do that in validateMenuItem:, but the fact that we will now
1424 * need to change it is useful if, for example, key equivalents change in a menu alongside selection
1425 * changes, since we need key equivalents set immediately.
1427 [[adium notificationCenter] postNotificationName:AIStatusStateMenuSelectionsChangedNotification
1433 * @brief Delay state menu updates
1435 * This should be called to prevent duplicative updates when multiple accounts are changing status simultaneously.
1437 - (void)setDelayStateMenuUpdates:(BOOL)shouldDelay
1440 stateMenuUpdateDelays++;
1442 stateMenuUpdateDelays--;
1444 if(stateMenuUpdateDelays == 0){
1445 [self updateAllStateMenuSelections];
1450 * @brief Update the selected state in a plugin's state menu
1452 * Updates the selected state menu item to reflect the currently active state.
1453 * @param stateMenuPlugin The state menu plugin we're updating
1455 - (void)updateStateMenuSelectionForPlugin:(id <StateMenuPlugin>)stateMenuPlugin
1457 NSNumber *identifier = [NSNumber numberWithInt:[stateMenuPlugin hash]];
1458 NSArray *stateMenuItemArray = [stateMenuItemArraysDict objectForKey:identifier];
1459 [stateMenuItemsNeedingUpdating addObjectsFromArray:stateMenuItemArray];
1464 * @brief Account status changed.
1466 * Rebuild all our state menus
1468 - (NSSet *)updateListObject:(AIListObject *)inObject keys:(NSSet *)inModifiedKeys silent:(BOOL)silent
1470 if([inObject isKindOfClass:[AIAccount class]]){
1471 if([inModifiedKeys containsObject:@"Online"] ||
1472 [inModifiedKeys containsObject:@"IdleSince"] ||
1473 [inModifiedKeys containsObject:@"StatusState"]){
1475 //Don't update the state menus if we are currently delaying
1476 if(stateMenuUpdateDelays == 0) [self updateAllStateMenuSelections];
1478 //We can get here without the preferencesChanged: notification if the account is automatically connected.
1479 if([inModifiedKeys containsObject:@"Online"]){
1480 if([inObject online]) [accountsToConnect addObject:inObject];
1489 * @brief Preferences changed; update our accountsToConnect tracking set
1491 * We use the preferences changed notifications rather than the statusObject notifications because the statusObject
1492 * may not change immediately upon requesting a connect or disconnect, since the account may wait to receive confirmation
1493 * before reporting itself as online or offline. With the preferences changed notification, we can distinguish a user
1494 * disconnect from selecting the global Offline menu item.
1496 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key
1497 object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
1499 if([key isEqualToString:@"Online"]){
1500 /* Track the accounts we should connect when setting to an online state. Our goal is to be able to reconnect
1501 * the most recently connected account if accounts are disconnected one-by-one. If accounts are disconnected
1502 * all at once via the global Offline menu item, we want to restore all of the previously connected accounts when
1503 * reconnecting, so we check to see if we are disconnecting via that menu item with the
1504 * isProcessingGlobalChange BOOL. */
1505 if(!isProcessingGlobalChange){
1506 if([[object preferenceForKey:@"Online" group:GROUP_ACCOUNT_STATUS] boolValue]){
1507 [accountsToConnect addObject:object];
1509 if([accountsToConnect count] > 1){
1510 [accountsToConnect removeObject:object];
1515 //Clear these caches now. Observers which get called before we do when an account actually connects
1516 //will want to get a fresh value.
1517 [self _resetActiveStatusState];
1522 * @brief Menu validation
1524 * Our state menu items should always be active, so always return YES for validation.
1526 * Here we lazily set the state of our menu items if our stateMenuItemsNeedingUpdating set indicates it is needed.
1528 - (BOOL)validateMenuItem:(id <NSMenuItem>)menuItem
1530 if([stateMenuItemsNeedingUpdating containsObject:menuItem]){
1531 BOOL noAccountsAreOnline = ![[adium accountController] oneOrMoreConnectedAccounts];
1532 NSDictionary *dict = [menuItem representedObject];
1534 AIStatus *menuItemStatusState;
1535 BOOL shouldSelectOffline;
1537 //Search for the account or global status state as appropriate for this menu item.
1538 //Also, determine if we are looking to select the Offline menu item
1539 if(account = [dict objectForKey:@"AIAccount"]){
1540 shouldSelectOffline = ![account online];
1542 shouldSelectOffline = noAccountsAreOnline;
1545 menuItemStatusState = [dict objectForKey:@"AIStatus"];
1547 if(shouldSelectOffline){
1548 //If we should select offline, set all menu items which don't have the AIOfflineStatusType tag to be off.
1549 if([menuItem tag] == AIOfflineStatusType){
1550 if([menuItem state] != NSOnState) [menuItem setState:NSOnState];
1552 if([menuItem state] != NSOffState) [menuItem setState:NSOffState];
1557 /* Account-specific menu items */
1558 AIStatus *appropiateActiveStatusState;
1559 appropiateActiveStatusState = [account statusState];
1561 /* Our "Custom..." menu choice has a nil represented object. If the appropriate active search state is
1562 * in our array of states from which we made menu items, we'll be searching to match it. If it isn't,
1563 * we have a custom state and will be searching for the custom item of the right type, switching all other
1564 * menu items to NSOffState. */
1565 if([[self sortedFullStateArray] containsObjectIdenticalTo:appropiateActiveStatusState]){
1566 //If the search state is in the array so is a saved state, search for the match
1567 if(menuItemStatusState == appropiateActiveStatusState){
1568 if([menuItem state] != NSOnState) [menuItem setState:NSOnState];
1570 if([menuItem state] != NSOffState) [menuItem setState:NSOffState];
1573 //If there is not a status state, we are in a Custom state. Search for the correct Custom item.
1574 if(menuItemStatusState){
1575 //If the menu item has an associated state, it's always off.
1576 if([menuItem state] != NSOffState) [menuItem setState:NSOffState];
1578 //If it doesn't, check the tag to see if it should be on or off.
1579 if([menuItem tag] == [appropiateActiveStatusState statusType]){
1580 if([menuItem state] != NSOnState) [menuItem setState:NSOnState];
1582 if([menuItem state] != NSOffState) [menuItem setState:NSOffState];
1587 /* General menu items */
1588 NSSet *allActiveStatusStates = [self allActiveStatusStates];
1589 int onState = (([allActiveStatusStates count] == 1) ? NSOnState : NSMixedState);
1591 if(menuItemStatusState){
1592 //If this menu item has a status state, set it to the right on state if that state is active
1593 if([allActiveStatusStates containsObject:menuItemStatusState]){
1594 if([menuItem state] != onState) [menuItem setState:onState];
1596 if([menuItem state] != NSOffState) [menuItem setState:NSOffState];
1599 //If it doesn't, check the tag to see if it should be on or off by looking for a matching custom state
1600 NSEnumerator *activeStatusStatesEnumerator = [allActiveStatusStates objectEnumerator];
1601 NSArray *sortedFullStateArray = [self sortedFullStateArray];
1602 AIStatus *statusState;
1603 BOOL foundCorrectStatusState = NO;
1605 while(!foundCorrectStatusState && (statusState = [activeStatusStatesEnumerator nextObject])){
1606 //We found a custom match if our array of menu item states doesn't contain this state and
1607 //its statusType matches the menuItem's tag.
1608 foundCorrectStatusState = (![sortedFullStateArray containsObjectIdenticalTo:statusState] &&
1609 ([menuItem tag] == [statusState statusType]));
1612 if(foundCorrectStatusState){
1613 if([menuItem state] != NSOnState) [menuItem setState:onState];
1615 if([menuItem state] != NSOffState) [menuItem setState:NSOffState];
1621 [stateMenuItemsNeedingUpdating removeObject:menuItem];
1628 * @brief Select a state menu item
1630 * Invoked by a state menu item, sets the state corresponding to the menu item as the active state.
1632 * If the representedObject NSDictionary has an @"AIAccount" object, set the state just for the appropriate AIAccount.
1633 * Otherwise, set the state globally.
1635 - (void)selectState:(id)sender
1637 NSDictionary *dict = [sender representedObject];
1638 AIStatus *statusState = [dict objectForKey:@"AIStatus"];
1639 AIAccount *account = [dict objectForKey:@"AIAccount"];
1641 /* Random undocumented feature of the moment... hold option and select a state to bring up the custom status window
1642 * for modifying and then setting it.
1644 if([NSEvent optionKey]){
1645 [AIEditStateWindowController editCustomState:statusState
1646 forType:[statusState statusType]
1650 notifyingTarget:self];
1656 shouldRebuild = [self removeIfNecessaryTemporaryStatusState:[account statusState]];
1657 [account setStatusState:statusState];
1660 //Rebuild our menus if there was a change
1661 [self rebuildAllStateMenus];
1665 [self setActiveStatusState:statusState];
1671 * @brief Select the custom state menu item
1673 * Invoked by the custom state menu item, opens a custom state window.
1674 * If the representedObject NSDictionary has an @"AIAccount" object, configure just for the appropriate AIAccount.
1675 * Otherwise, configure globally.
1677 - (IBAction)selectCustomState:(id)sender
1679 NSDictionary *dict = [sender representedObject];
1680 AIAccount *account = [dict objectForKey:@"AIAccount"];
1681 AIStatusType statusType = [sender tag];
1682 AIStatus *baseStatusState;
1685 baseStatusState = [account statusState];
1687 baseStatusState = [self activeStatusState];
1690 /* If we are going to a custom state of a different type, we don't want to prefill with baseStatusState as it stands.
1691 * Instead, we load the last used status of that type. */
1692 if(([baseStatusState statusType] != statusType)){
1693 NSDictionary *lastStatusStates = [[adium preferenceController] preferenceForKey:@"LastStatusStates"
1694 group:PREF_GROUP_STATUS_PREFERENCES];
1696 NSData *lastStatusStateData = [lastStatusStates objectForKey:[NSNumber numberWithInt:statusType]];
1697 AIStatus *lastStatusStateOfThisType = (lastStatusStateData ?
1698 [NSKeyedUnarchiver unarchiveObjectWithData:lastStatusStateData] :
1701 baseStatusState = [[lastStatusStateOfThisType retain] autorelease];
1704 //don't use the current status state as a base. Going from Away to Available, don't autofill the Available
1705 //status message with the old away message.
1706 if([baseStatusState statusType] != statusType){
1707 baseStatusState = nil;
1710 [AIEditStateWindowController editCustomState:baseStatusState
1715 notifyingTarget:self];
1719 * @brief Called when a state could potentially need to removed from the temporary (non-saved) list
1721 * If originalState is in the temporary status array, and it is being used on one or zero accounts, it
1722 * is removed from the temporary status array. This method should be used when one or more accounts have stopped
1723 * using a single status state to determine if that status state is both non-saved and unused.
1725 * @result YES if the state was removed
1727 - (BOOL)removeIfNecessaryTemporaryStatusState:(AIStatus *)originalState
1729 BOOL didRemove = NO;
1731 /* If the original (old) status state is in our temporary array and is not being used in more than 1 account
1732 * we should remove it */
1733 if([temporaryStateArray containsObject:originalState]){
1734 NSEnumerator *enumerator;
1738 enumerator = [[[adium accountController] accountArray] objectEnumerator];
1739 while(account = [enumerator nextObject]){
1740 if([account actualStatusState] == originalState){
1741 if(++count > 1) break;
1746 [temporaryStateArray removeObject:originalState];
1754 * @brief Apply a custom state
1756 * Invoked when the custom state window is closed by the user clicking OK. In response this method sets the custom
1757 * state as the active state.
1759 - (void)customStatusState:(AIStatus *)originalState changedTo:(AIStatus *)newState forAccount:(AIAccount *)account
1761 BOOL shouldRebuild = NO;
1764 shouldRebuild = [self removeIfNecessaryTemporaryStatusState:originalState];
1766 //Now set the newState for the account
1767 [account setStatusState:newState];
1770 //Set the state for all accounts. This will clear out the temporaryStatusArray as necessary.
1771 [self setActiveStatusState:newState];
1774 if([newState mutabilityType] != AITemporaryEditableStatusState){
1775 [[adium statusController] addStatusState:newState];
1778 NSMutableDictionary *lastStatusStates;
1780 lastStatusStates = [[[adium preferenceController] preferenceForKey:@"LastStatusStates"
1781 group:PREF_GROUP_STATUS_PREFERENCES] mutableCopy];
1782 if(!lastStatusStates) lastStatusStates = [NSMutableDictionary dictionary];
1784 [lastStatusStates setObject:[NSKeyedArchiver archivedDataWithRootObject:newState]
1785 forKey:[NSNumber numberWithInt:[newState statusType]]];
1787 [[adium preferenceController] setPreference:lastStatusStates
1788 forKey:@"LastStatusStates"
1789 group:PREF_GROUP_STATUS_PREFERENCES];
1791 //Add to our temporary status array if it's not in our state array
1792 if(shouldRebuild || (![[self stateArray] containsObjectIdenticalTo:newState])){
1793 [temporaryStateArray addObject:newState];
1795 //Now rebuild our menus to include this temporary item
1796 [self rebuildAllStateMenus];
1801 * @brief Determine a string to use as a menu title
1803 * This method truncates a state title string for display as a menu item.
1804 * Wide menus aren't pretty and may cause crashing in certain versions of OS X, so all state
1805 * titles should be run through this method before being used as menu item titles.
1807 * @param statusState The state for which we want a title
1809 * @result An appropriate NSString title
1811 - (NSString *)_titleForMenuDisplayOfState:(AIStatus *)statusState
1813 NSString *title = [statusState title];
1815 if([title length] > STATE_TITLE_MENU_LENGTH){
1816 title = [title stringWithEllipsisByTruncatingToLength:STATE_TITLE_MENU_LENGTH];
1823 * @brief Create and add the built-in status types
1825 * The built-in status types are basic, generic "Available" and "Away" states.
1827 - (void)buildBuiltInStatusTypes
1829 NSDictionary *statusDict;
1831 builtInStatusTypes[AIAvailableStatusType] = [[NSMutableSet alloc] init];
1832 statusDict = [NSDictionary dictionaryWithObjectsAndKeys:
1833 STATUS_NAME_AVAILABLE, KEY_STATUS_NAME,
1834 STATUS_DESCRIPTION_AVAILABLE, KEY_STATUS_DESCRIPTION,
1835 [NSNumber numberWithInt:AIAvailableStatusType], KEY_STATUS_TYPE,
1837 [builtInStatusTypes[AIAvailableStatusType] addObject:statusDict];
1839 builtInStatusTypes[AIAwayStatusType] = [[NSMutableSet alloc] init];
1840 statusDict = [NSDictionary dictionaryWithObjectsAndKeys:
1841 STATUS_NAME_AWAY, KEY_STATUS_NAME,
1842 STATUS_DESCRIPTION_AWAY, KEY_STATUS_DESCRIPTION,
1843 [NSNumber numberWithInt:AIAwayStatusType], KEY_STATUS_TYPE,
1845 [builtInStatusTypes[AIAwayStatusType] addObject:statusDict];
1848 - (NSMenu *)statusStatesMenu
1850 NSMenu *statusStatesMenu = [[NSMenu allocWithZone:[NSMenu menuZone]] init];
1851 NSEnumerator *enumerator;
1852 AIStatus *statusState;
1853 AIStatusType currentStatusType = AIAvailableStatusType;
1854 NSMenuItem *menuItem;
1856 [statusStatesMenu setMenuChangedMessagesEnabled:NO];
1857 [statusStatesMenu setAutoenablesItems:NO];
1859 //Create a menu item for each state. States must first be sorted such that states of the same AIStatusType
1860 //are grouped together.
1861 enumerator = [[self sortedFullStateArray] objectEnumerator];
1862 while(statusState = [enumerator nextObject]){
1863 AIStatusType thisStatusType = [statusState statusType];
1865 if(currentStatusType != thisStatusType){
1866 //Add a divider between each type of status
1867 [statusStatesMenu addItem:[NSMenuItem separatorItem]];
1868 currentStatusType = thisStatusType;
1871 menuItem = [[NSMenuItem alloc] initWithTitle:[self _titleForMenuDisplayOfState:statusState]
1876 //NSMenuItem will call setFlipped: on the image we pass it, causing flipped drawing elsewhere if we pass it the
1877 //shared status icon. So we pass it a copy of the shared icon that it's free to manipulate.
1878 [menuItem setImage:[[[statusState icon] copy] autorelease]];
1879 [menuItem setRepresentedObject:[NSDictionary dictionaryWithObject:statusState
1880 forKey:@"AIStatus"]];
1881 [menuItem setTag:[statusState statusType]];
1882 [statusStatesMenu addItem:menuItem];
1886 [statusStatesMenu setMenuChangedMessagesEnabled:YES];
1888 return statusStatesMenu;
1891 #pragma mark Upgrade code
1893 * @brief Temporary upgrade code for 0.7x -> 0.8
1895 * Versions 0.7x and prior stored their away messages in a different format. This code allows a seamless
1896 * transition from 0.7x to 0.8. We can easily recognize the old format because the away messages are of
1897 * type "Away" instead of type "State", which is used for all 0.8 and later saved states.
1898 * Since we are changing the array as we scan it, an enumerator will not work here.
1900 #define OLD_KEY_SAVED_AWAYS @"Saved Away Messages"
1901 #define OLD_GROUP_AWAY_MESSAGES @"Away Messages"
1902 #define OLD_STATE_SAVED_AWAY @"Away"
1903 #define OLD_STATE_AWAY @"Message"
1904 #define OLD_STATE_AUTO_REPLY @"Autoresponse"
1905 #define OLD_STATE_TITLE @"Title"
1906 - (void)_upgradeSavedAwaysToSavedStates
1908 NSArray *savedAways = [[adium preferenceController] preferenceForKey:OLD_KEY_SAVED_AWAYS
1909 group:OLD_GROUP_AWAY_MESSAGES];
1912 NSEnumerator *enumerator = [savedAways objectEnumerator];
1913 NSDictionary *state;
1915 //Update all the away messages to states.
1916 while(state = [enumerator nextObject]){
1917 if([[state objectForKey:@"Type"] isEqualToString:OLD_STATE_SAVED_AWAY]){
1918 AIStatus *statusState;
1920 //Extract the away message information from this old record
1921 NSData *statusMessageData = [state objectForKey:OLD_STATE_AWAY];
1922 NSData *autoReplyMessageData = [state objectForKey:OLD_STATE_AUTO_REPLY];
1923 NSString *title = [state objectForKey:OLD_STATE_TITLE];
1925 //Create an AIStatus from this information
1926 statusState = [AIStatus status];
1928 //General category: It's an away type
1929 [statusState setStatusType:AIAwayStatusType];
1931 //Specific state: It's the generic away. Funny how that works out.
1932 [statusState setStatusName:STATUS_NAME_AWAY];
1934 //Set the status message (which is just the away message).
1935 [statusState setStatusMessage:[NSAttributedString stringWithData:statusMessageData]];
1937 //It has an auto reply.
1938 [statusState setHasAutoReply:YES];
1940 if(autoReplyMessageData){
1941 //Use the custom auto reply if it was set.
1942 [statusState setAutoReply:[NSAttributedString stringWithData:autoReplyMessageData]];
1944 //If no autoReplyMesssage, use the status message.
1945 [statusState setAutoReplyIsStatusMessage:YES];
1948 if(title) [statusState setTitle:title];
1950 //Add the updated state to our state array.
1951 [stateArray addObject:statusState];
1955 //Save these changes and delete the old aways so we don't need to do this again.
1956 [self _saveStateArrayAndNotifyOfChanges];
1957 [[adium preferenceController] setPreference:nil
1958 forKey:OLD_KEY_SAVED_AWAYS
1959 group:OLD_GROUP_AWAY_MESSAGES];