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 "AIStatusController.h"
19 #import <Adium/AIAccountControllerProtocol.h>
20 #import <Adium/AISoundControllerProtocol.h>
22 #import <Adium/AIContactControllerProtocol.h>
23 #import <Adium/AIPreferenceControllerProtocol.h>
24 #import "AdiumIdleManager.h"
26 #import <AIUtilities/AIMenuAdditions.h>
27 #import <AIUtilities/AIArrayAdditions.h>
28 #import <AIUtilities/AIAttributedStringAdditions.h>
29 #import <AIUtilities/AIEventAdditions.h>
30 #import <AIUtilities/AIStringAdditions.h>
31 #import <AIUtilities/AIObjectAdditions.h>
32 #import <Adium/AIAccount.h>
33 #import <Adium/AIService.h>
34 #import <Adium/AIStatusIcons.h>
35 #import "AIStatusGroup.h"
36 #import <Adium/AIStatus.h>
39 #define STATUS_TITLE_OFFLINE AILocalizedString(@"Offline",nil)
41 #define BUILT_IN_STATE_ARRAY @"BuiltInStatusStates"
43 #define TOP_STATUS_STATE_ID @"TopStatusID"
45 @interface AIStatusController (PRIVATE)
46 - (NSArray *)builtInStateArray;
48 - (void)_upgradeSavedAwaysToSavedStates;
50 - (NSArray *)_menuItemsForStatusesOfType:(AIStatusType)type forServiceCodeUniqueID:(NSString *)inServiceCodeUniqueID withTarget:(id)target;
51 - (void)_addMenuItemsForStatusOfType:(AIStatusType)type
53 fromSet:(NSSet *)sourceArray
54 toArray:(NSMutableArray *)menuItems
55 alreadyAddedTitles:(NSMutableSet *)alreadyAddedTitles;
56 - (void)buildBuiltInStatusTypes;
57 - (void)notifyOfChangedStatusArray;
61 * @class AIStatusController
62 * @brief Core status & state methods
64 * This class provides a foundation for Adium's status and status state systems.
66 @implementation AIStatusController
68 static NSMutableSet *temporaryStateArray = nil;
71 * Init the status controller
75 if ((self = [super init])) {
76 stateMenuItemArraysDict = [[NSMutableDictionary alloc] init];
77 stateMenuPluginsArray = [[NSMutableArray alloc] init];
78 stateMenuItemsNeedingUpdating = [[NSMutableSet alloc] init];
79 activeStatusUpdateDelays = 0;
80 _sortedFullStateArray = nil;
81 _activeStatusState = nil;
82 _allActiveStatusStates = nil;
83 temporaryStateArray = [[NSMutableSet alloc] init];
85 accountsToConnect = [[NSMutableSet alloc] init];
87 idleManager = [[AdiumIdleManager alloc] init];
94 * @brief Finish initing the status controller
96 * Set our initial status state, and restore our array of accounts to connect when a global state is selected.
98 - (void)controllerDidLoad
100 NSEnumerator *enumerator;
103 [[adium contactController] registerListObjectObserver:self];
105 [self buildBuiltInStatusTypes];
107 //Put each account into the status it was in last time we quit.
108 BOOL needToRebuildMenus = NO;
109 BOOL allStatusesInSameState = YES;
110 AIStatus *prevStatus = nil;
111 enumerator = [[[adium accountController] accounts] objectEnumerator];
112 while ((account = [enumerator nextObject])) {
113 NSData *lastStatusData = [account preferenceForKey:@"LastStatus"
114 group:GROUP_ACCOUNT_STATUS];
115 AIStatus *lastStatus = nil;
117 lastStatus = [NSKeyedUnarchiver unarchiveObjectWithData:lastStatusData];
119 if (lastStatus && [lastStatus isKindOfClass:[AIStatus class]]) {
120 AIStatus *existingStatus;
122 /* We want to use a loaded status instance if one exists. This will be the case if the account
123 * was last in a built-in or user defined and saved state. If the last state was unsaved, existingStatus
126 existingStatus = [self statusStateWithUniqueStatusID:[lastStatus uniqueStatusID]];
128 if (existingStatus) {
129 lastStatus = existingStatus;
131 //Add to our temporary status array
132 [temporaryStateArray addObject:lastStatus];
134 /* We could clear out _flatStatusSet for the next iteration, but we _know_ what changed,
135 * so modify it directly for efficiency.
137 [_flatStatusSet addObject:lastStatus];
139 needToRebuildMenus = YES;
142 prevStatus = lastStatus;
143 else if (prevStatus != lastStatus)
144 allStatusesInSameState = NO;
145 [account setStatusStateAndRemainOffline:lastStatus];
149 if (needToRebuildMenus) {
150 [self notifyOfChangedStatusArray];
155 * @brief Begin closing the status controller
157 * Save the online accounts; they will be the accounts connected by a global status change
159 * Also save the current status state of each account so it can be restored on next launch.
161 - (void)controllerWillClose
163 NSEnumerator *enumerator;
166 enumerator = [[[adium accountController] accounts] objectEnumerator];
167 while ((account = [enumerator nextObject])) {
168 /* Store the current status state for use on next launch.
170 * We use the statusObjectForKey:@"StatusState" accessor rather than [account statusState]
171 * because we don't want anything besides the account's actual status state. That is, we don't
172 * want the default available state if the account doesn't have a state yet, and we want the
173 * real last-state-which-was-set (not the offline one) if the account is offline.
175 AIStatus *currentStatus = [account statusObjectForKey:@"StatusState"];
176 [account setPreference:((currentStatus && (currentStatus != offlineStatusState)) ?
177 [NSKeyedArchiver archivedDataWithRootObject:currentStatus] :
180 group:GROUP_ACCOUNT_STATUS];
183 //XXX change this back sometime before 1.0 release
184 // [[adium preferenceController] setPreference:[NSKeyedArchiver archivedDataWithRootObject:[self rootStateGroup]]
185 [[adium preferenceController] setPreference:[NSKeyedArchiver archivedDataWithRootObject:[[self rootStateGroup] containedStatusItems]]
186 forKey:KEY_SAVED_STATUS
187 group:PREF_GROUP_SAVED_STATUS];
189 [[adium notificationCenter] removeObserver:self];
190 [[adium preferenceController] unregisterPreferenceObserver:self];
191 [[adium contactController] unregisterListObjectObserver:self];
199 [_rootStateGroup release]; _rootStateGroup = nil;
200 [_sortedFullStateArray release]; _sortedFullStateArray = nil;
204 #pragma mark Status registration
206 * @brief Register a status for a service
208 * Implementation note: Each AIStatusType has its own NSMutableDictionary, statusDictsByServiceCodeUniqueID.
209 * statusDictsByServiceCodeUniqueID is keyed by serviceCodeUniqueID; each object is an NSMutableSet of NSDictionaries.
210 * Each of these dictionaries has KEY_STATUS_NAME, KEY_STATUS_DESCRIPTION, and KEY_STATUS_TYPE.
212 * @param statusName A name which will be passed back to accounts of this service. Internal use only. Use the AIStatusController.h #defines where appropriate.
213 * @param description A human-readable localized description which will be shown to the user. Use the AIStatusController.h #defines where appropriate.
214 * @param type An AIStatusType, the general type of this status.
215 * @param service The AIService for which to register the status
217 - (void)registerStatus:(NSString *)statusName withDescription:(NSString *)description ofType:(AIStatusType)type forService:(AIService *)service
219 NSMutableSet *statusDicts;
220 NSString *serviceCodeUniqueID = [service serviceCodeUniqueID];
222 //Create the set if necessary
223 if (!statusDictsByServiceCodeUniqueID[type]) statusDictsByServiceCodeUniqueID[type] = [[NSMutableDictionary alloc] init];
224 if (!(statusDicts = [statusDictsByServiceCodeUniqueID[type] objectForKey:serviceCodeUniqueID])) {
225 statusDicts = [NSMutableSet set];
226 [statusDictsByServiceCodeUniqueID[type] setObject:statusDicts
227 forKey:serviceCodeUniqueID];
230 //Create a dictionary for this status entry
231 NSDictionary *statusDict = [NSDictionary dictionaryWithObjectsAndKeys:
232 statusName, KEY_STATUS_NAME,
233 description, KEY_STATUS_DESCRIPTION,
234 [NSNumber numberWithInt:type], KEY_STATUS_TYPE,
237 [statusDicts addObject:statusDict];
240 #pragma mark Status menus
242 * @brief Generate and return a menu of status types (Away, Be right back, etc.)
244 * @param service The service for which to return a specific list of types, or nil to return all available types
245 * @param target The target for the menu items, which will have an action of @selector(selectStatus:)
247 * @result The menu of statuses, separated by available and away status types
249 - (NSMenu *)menuOfStatusesForService:(AIService *)service withTarget:(id)target
251 NSMenu *menu = [[NSMenu allocWithZone:[NSMenu menuZone]] init];
252 NSEnumerator *enumerator;
253 NSMenuItem *menuItem;
254 NSString *serviceCodeUniqueID = [service serviceCodeUniqueID];
257 for (type = AIAvailableStatusType ; type < STATUS_TYPES_COUNT ; type++) {
258 NSArray *menuItemArray;
260 menuItemArray = [self _menuItemsForStatusesOfType:type
261 forServiceCodeUniqueID:serviceCodeUniqueID
264 //Add a separator between each type after available
265 if ((type > AIAvailableStatusType) && [menuItemArray count]) {
266 [menu addItem:[NSMenuItem separatorItem]];
269 //Add the items for this type
270 enumerator = [menuItemArray objectEnumerator];
271 while ((menuItem = [enumerator nextObject])) {
272 [menu addItem:menuItem];
276 return [menu autorelease];
280 * @brief Return an array of menu items for an AIStatusType and service
282 * @pram type The AIStatusType for which to return statuses
283 * @param inServiceCodeUniqueID The service for which to return active statuses. If nil, return all statuses for online services.
284 * @param target The target for the menu items
286 * @result An <tt>NSArray</tt> of <tt>NSMenuItem</tt> objects.
288 - (NSArray *)_menuItemsForStatusesOfType:(AIStatusType)type forServiceCodeUniqueID:(NSString *)inServiceCodeUniqueID withTarget:(id)target
290 NSMutableArray *menuItems = [[NSMutableArray alloc] init];
291 NSMutableSet *alreadyAddedTitles = [NSMutableSet set];
293 //First, add our built-in items (so they will be at the top of the array and service-specific 'copies' won't replace them)
294 [self _addMenuItemsForStatusOfType:type
296 fromSet:builtInStatusTypes[type]
298 alreadyAddedTitles:alreadyAddedTitles];
300 //Now, add items for this service, or from all available services, as appropriate
301 if (inServiceCodeUniqueID) {
304 //Obtain the status dicts for this type and service code unique ID
305 if ((statusDicts = [statusDictsByServiceCodeUniqueID[type] objectForKey:inServiceCodeUniqueID])) {
307 [self _addMenuItemsForStatusOfType:type
311 alreadyAddedTitles:alreadyAddedTitles];
315 NSEnumerator *enumerator;
318 enumerator = [[[adium accountController] activeServicesIncludingCompatibleServices:NO] objectEnumerator];
319 while ((service = [enumerator nextObject])) {
322 //Obtain the status dicts for this type and service code unique ID
323 if ((statusDicts = [statusDictsByServiceCodeUniqueID[type] objectForKey:[service serviceCodeUniqueID]])) {
325 [self _addMenuItemsForStatusOfType:type
329 alreadyAddedTitles:alreadyAddedTitles];
335 [menuItems sortUsingSelector:@selector(titleCompare:)];
337 return [menuItems autorelease];
341 * @brief Add menu items for a particular type of status
343 * @param type The AIStatusType, used for determining the icon of the menu items
344 * @param target The target of the created menu items
345 * @param statusDicts An NSSet of NSDictionary objects, which should each represent a status of the passed type
346 * @param menuItems The NSMutableArray to which to add the menuItems
347 * @param alreadyAddedTitles NSMutableSet of NSString titles which have already been added and should not be duplicated. Will be updated as items are added.
349 - (void)_addMenuItemsForStatusOfType:(AIStatusType)type
350 withTarget:(id)target
351 fromSet:(NSSet *)statusDicts
352 toArray:(NSMutableArray *)menuItems
353 alreadyAddedTitles:(NSMutableSet *)alreadyAddedTitles
355 NSEnumerator *statusDictEnumerator = [statusDicts objectEnumerator];
356 NSDictionary *statusDict;
358 //Enumerate the status dicts
359 while ((statusDict = [statusDictEnumerator nextObject])) {
360 NSString *title = [statusDict objectForKey:KEY_STATUS_DESCRIPTION];
363 * Only add if it has not already been added by another service.... Services need to use unique titles if they have
364 * unique state names, but are welcome to share common name/description combinations, which is why the #defines
367 if (![alreadyAddedTitles containsObject:title]) {
369 NSMenuItem *menuItem;
371 menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:title
373 action:@selector(selectStatus:)
376 image = [AIStatusIcons statusIconForStatusName:[statusDict objectForKey:KEY_STATUS_NAME]
378 iconType:AIStatusIconMenu
379 direction:AIIconNormal];
381 [menuItem setRepresentedObject:statusDict];
382 [menuItem setImage:image];
383 [menuItem setEnabled:YES];
384 [menuItems addObject:menuItem];
387 [alreadyAddedTitles addObject:title];
392 #pragma mark Status State Descriptions
393 - (NSString *)localizedDescriptionForCoreStatusName:(NSString *)statusName
395 static NSDictionary *coreLocalizedStatusDescriptions = nil;
396 if(!coreLocalizedStatusDescriptions){
397 coreLocalizedStatusDescriptions = [[NSDictionary dictionaryWithObjectsAndKeys:
398 AILocalizedString(@"Available", nil), STATUS_NAME_AVAILABLE,
399 AILocalizedString(@"Free for chat", nil), STATUS_NAME_FREE_FOR_CHAT,
400 AILocalizedString(@"Available for friends only",nil), STATUS_NAME_AVAILABLE_FRIENDS_ONLY,
401 AILocalizedString(@"Away", nil), STATUS_NAME_AWAY,
402 AILocalizedString(@"Extended away",nil), STATUS_NAME_EXTENDED_AWAY,
403 AILocalizedString(@"Away for friends only",nil), STATUS_NAME_AWAY_FRIENDS_ONLY,
404 AILocalizedString(@"Do not disturb", nil), STATUS_NAME_DND,
405 AILocalizedString(@"Not available", nil), STATUS_NAME_NOT_AVAILABLE,
406 AILocalizedString(@"Occupied", nil), STATUS_NAME_OCCUPIED,
407 AILocalizedString(@"Be right back",nil), STATUS_NAME_BRB,
408 AILocalizedString(@"Busy",nil), STATUS_NAME_BUSY,
409 AILocalizedString(@"On the phone",nil), STATUS_NAME_PHONE,
410 AILocalizedString(@"Out to lunch",nil), STATUS_NAME_LUNCH,
411 AILocalizedString(@"Not at home",nil), STATUS_NAME_NOT_AT_HOME,
412 AILocalizedString(@"Not at my desk",nil), STATUS_NAME_NOT_AT_DESK,
413 AILocalizedString(@"Not in the office",nil), STATUS_NAME_NOT_IN_OFFICE,
414 AILocalizedString(@"On vacation",nil), STATUS_NAME_VACATION,
415 AILocalizedString(@"Stepped out",nil), STATUS_NAME_STEPPED_OUT,
416 AILocalizedString(@"Invisible",nil), STATUS_NAME_INVISIBLE,
417 AILocalizedString(@"Offline",nil), STATUS_NAME_OFFLINE,
421 return (statusName ? [coreLocalizedStatusDescriptions objectForKey:statusName] : nil);
424 - (NSString *)localizedDescriptionForStatusName:(NSString *)statusName statusType:(AIStatusType)statusType
426 NSString *description = nil;
429 !(description = [self localizedDescriptionForCoreStatusName:statusName])) {
430 NSEnumerator *enumerator = [statusDictsByServiceCodeUniqueID[statusType] objectEnumerator];
433 while (!description && (set = [enumerator nextObject])) {
434 NSEnumerator *statusDictsEnumerator = [set objectEnumerator];
435 NSDictionary *statusDict;
436 while (!description && (statusDict = [statusDictsEnumerator nextObject])) {
437 if ([[statusDict objectForKey:KEY_STATUS_NAME] isEqualToString:statusName]){
438 description = [statusDict objectForKey:KEY_STATUS_DESCRIPTION];
448 * @brief Return the localized description for the sate of the passed status
450 * This could be stored with the statusState, but that would break if the locale changed. This way, the nonlocalized
451 * string is used to look up the appropriate localized one.
453 * @result A localized description such as @"Away" or @"Out to Lunch" of the state used by statusState
455 - (NSString *)descriptionForStateOfStatus:(AIStatus *)statusState
457 return [self localizedDescriptionForStatusName:[statusState statusName]
458 statusType:[statusState statusType]];
462 * @brief The status name to use by default for a passed type
464 * This is the name which will be used for new AIStatus objects of this type.
466 - (NSString *)defaultStatusNameForType:(AIStatusType)statusType
468 //Set the default status name
469 switch (statusType) {
470 case AIAvailableStatusType:
471 return STATUS_NAME_AVAILABLE;
473 case AIAwayStatusType:
474 return STATUS_NAME_AWAY;
476 case AIInvisibleStatusType:
477 return STATUS_NAME_INVISIBLE;
479 case AIOfflineStatusType:
480 return STATUS_NAME_OFFLINE;
487 #pragma mark Setting Status States
489 * @brief Set the active status state
491 * Sets the currently active status state. This applies throughout Adium and to all accounts. The state will become
492 * effective immediately.
494 - (void)setActiveStatusState:(AIStatus *)statusState
496 //Apply the state to our accounts and notify (delay to the next run loop to improve perceived speed)
497 [self performSelector:@selector(applyState:toAccounts:)
498 withObject:statusState
499 withObject:[[adium accountController] accounts]
504 * @brief Return the <tt>AIStatus</tt> to be used by accounts as they are created
506 - (AIStatus *)defaultInitialStatusState
508 return [[self builtInStateArray] objectAtIndex:0];
512 * @brief Reset the active status state
514 * All active status states cache will also reset. Posts an active status changed notification. The active state
515 * will be regenerated the next time it is requested.
517 - (void)_resetActiveStatusState
519 //Clear the active status state. It will be rebuilt next time it is requested
520 [_activeStatusState release]; _activeStatusState = nil;
521 [_allActiveStatusStates release]; _allActiveStatusStates = nil;
523 //Let observers know the active state has changed
524 if (!activeStatusUpdateDelays) {
525 [[adium notificationCenter] postNotificationName:AIStatusActiveStateChangedNotification object:nil];
530 * @brief Account status changed.
532 * Rebuild all our state menus
534 - (NSSet *)updateListObject:(AIListObject *)inObject keys:(NSSet *)inModifiedKeys silent:(BOOL)silent
536 if ([inObject isKindOfClass:[AIAccount class]]) {
537 if ([inModifiedKeys containsObject:@"Online"] ||
538 [inModifiedKeys containsObject:@"IdleSince"] ||
539 [inModifiedKeys containsObject:@"StatusState"] ||
540 [inModifiedKeys containsObject:KEY_ENABLED]) {
542 [self _resetActiveStatusState];
551 * @brief Delay activee status menu updates
553 * This should be called to prevent duplicative updates when multiple accounts are changing status simultaneously.
555 - (void)setDelayActiveStatusUpdates:(BOOL)shouldDelay
558 activeStatusUpdateDelays++;
560 activeStatusUpdateDelays--;
562 if (!activeStatusUpdateDelays) {
563 [[adium notificationCenter] postNotificationName:AIStatusActiveStateChangedNotification object:nil];
568 * @brief Delay activee status menu updates
570 * This should be called to prevent duplicative rebuilds when the status menu will change multple times.
572 - (void)setDelayStatusMenuRebuilding:(BOOL)shouldDelay
575 statusMenuRebuildDelays++;
577 statusMenuRebuildDelays--;
579 if (!statusMenuRebuildDelays) {
580 [[adium notificationCenter] postNotificationName:AIStatusStateArrayChangedNotification object:nil];
585 * @brief Apply a state to multiple accounts
587 - (void)applyState:(AIStatus *)statusState toAccounts:(NSArray *)accountArray
589 NSEnumerator *enumerator;
591 AIStatus *aStatusState;
592 BOOL shouldRebuild = NO;
593 BOOL noConnectedAccounts = ![[adium accountController] oneOrMoreConnectedOrConnectingAccounts];
594 BOOL isOfflineStatus = ([statusState statusType] == AIOfflineStatusType);
595 [self setDelayActiveStatusUpdates:YES];
597 /* If we're going offline, determine what accounts are currently online, first, so that we can restore that when an online state
600 if (isOfflineStatus && !noConnectedAccounts) {
601 [accountsToConnect removeAllObjects];
603 enumerator = [accountArray objectEnumerator];
604 while ((account = [enumerator nextObject])) {
605 if ([account online]) [accountsToConnect addObject:account];
609 if (noConnectedAccounts) {
610 /* No connected accounts: Connect all enabled accounts which were set offline previously.
611 * If we have no such list of accounts, connect 'em all.
613 BOOL noAccountsToConnectCount = ([accountsToConnect count] == 0);
614 enumerator = [accountArray objectEnumerator];
615 while ((account = [enumerator nextObject])) {
616 if ([account enabled] &&
617 ([accountsToConnect containsObject:account] || noAccountsToConnectCount)) {
618 [account setStatusState:statusState];
621 [account setStatusStateAndRemainOffline:statusState];
626 //At least one account is online. Just change its status without taking any other accounts online.
627 enumerator = [accountArray objectEnumerator];
628 while ((account = [enumerator nextObject])) {
629 if ([account online] || isOfflineStatus) {
630 [account setStatusState:statusState];
633 [account setStatusStateAndRemainOffline:statusState];
638 //If this is not an offline status, we've now made use of accountsToConnect and should clear it so it isn't used again.
639 if (!isOfflineStatus) {
640 [accountsToConnect removeAllObjects];
643 //Any objects in the temporary state array which aren't the state we just set should now be removed.
644 enumerator = [[[temporaryStateArray copy] autorelease] objectEnumerator];
645 while ((aStatusState = [enumerator nextObject])) {
646 if (aStatusState != statusState) {
647 [temporaryStateArray removeObject:aStatusState];
653 [self notifyOfChangedStatusArray];
656 [self setDelayActiveStatusUpdates:NO];
659 #pragma mark Retrieving Status States
661 * @brief Access to Adium's user-defined states
663 * Returns the root AIStatusGroup of user-defined states
665 - (AIStatusGroup *)rootStateGroup
667 if (!_rootStateGroup) {
668 NSData *savedStateData = [[adium preferenceController] preferenceForKey:KEY_SAVED_STATUS
669 group:PREF_GROUP_SAVED_STATUS];
670 if (savedStateData) {
671 id archivedObject = [NSKeyedUnarchiver unarchiveObjectWithData:savedStateData];
673 if ([archivedObject isKindOfClass:[AIStatusGroup class]]) {
674 //Adium 1.0 archives an AIStatusGroup
675 _rootStateGroup = [archivedObject retain];
677 } else if ([archivedObject isKindOfClass:[NSArray class]]) {
678 //Adium 0.8x archived an NSArray
679 _rootStateGroup = [[AIStatusGroup statusGroupWithContainedStatusItems:archivedObject] retain];
683 if (!_rootStateGroup) _rootStateGroup = [[AIStatusGroup statusGroup] retain];
685 //Upgrade Adium 0.7x away messages
686 [self _upgradeSavedAwaysToSavedStates];
689 return _rootStateGroup;
693 * @brief Return the array of built-in states
695 * These are basic Available and Away states which should always be visible and are (by convention) immutable.
696 * The first state in BUILT_IN_STATE_ARRAY will be used as the default for accounts as they are created.
698 - (NSArray *)builtInStateArray
700 if (!builtInStateArray) {
701 NSArray *savedBuiltInStateArray = [NSArray arrayNamed:BUILT_IN_STATE_ARRAY forClass:[self class]];
702 NSEnumerator *enumerator;
705 builtInStateArray = [[NSMutableArray alloc] initWithCapacity:[savedBuiltInStateArray count]];
707 enumerator = [savedBuiltInStateArray objectEnumerator];
708 while ((dict = [enumerator nextObject])) {
709 AIStatus *status = [AIStatus statusWithDictionary:dict];
710 [builtInStateArray addObject:status];
712 //Store a reference to our offline state if we just loaded it
713 if ([status statusType] == AIOfflineStatusType) {
714 [offlineStatusState release];
715 offlineStatusState = [status retain];
720 return builtInStateArray;
724 * @brief Create and add the built-in status types; even if no service explicitly registers these, they are available.
726 * The built-in status types are basic, generic "Available" and "Away" states.
728 - (void)buildBuiltInStatusTypes
730 NSDictionary *statusDict;
732 builtInStatusTypes[AIAvailableStatusType] = [[NSMutableSet alloc] init];
733 statusDict = [NSDictionary dictionaryWithObjectsAndKeys:
734 STATUS_NAME_AVAILABLE, KEY_STATUS_NAME,
735 [self localizedDescriptionForCoreStatusName:STATUS_NAME_AVAILABLE], KEY_STATUS_DESCRIPTION,
736 [NSNumber numberWithInt:AIAvailableStatusType], KEY_STATUS_TYPE,
738 [builtInStatusTypes[AIAvailableStatusType] addObject:statusDict];
740 builtInStatusTypes[AIAwayStatusType] = [[NSMutableSet alloc] init];
741 statusDict = [NSDictionary dictionaryWithObjectsAndKeys:
742 STATUS_NAME_AWAY, KEY_STATUS_NAME,
743 [self localizedDescriptionForCoreStatusName:STATUS_NAME_AWAY], KEY_STATUS_DESCRIPTION,
744 [NSNumber numberWithInt:AIAwayStatusType], KEY_STATUS_TYPE,
746 [builtInStatusTypes[AIAwayStatusType] addObject:statusDict];
750 - (AIStatus *)offlineStatusState
752 //Ensure the built in states have been loaded
753 [self builtInStateArray];
755 NSAssert(offlineStatusState != nil, @"Nil offline status state");
756 return offlineStatusState;
760 * @brief Return a sorted state array for use in menu item creation
762 * The array is created by adding the built in states to the user states, then sorting using _statusArraySort
763 * The resulting array may contain AIStatus and AIStatusGroup objects.
765 * @result A cached NSArray which is sorted by status type (available, away), built-in vs. user-made, and then original ordering.
767 - (NSArray *)sortedFullStateArray
769 if (!_sortedFullStateArray) {
770 NSArray *originalStateArray;
771 NSMutableArray *tempArray;
773 //Start with everything contained 1) in our built-in array and then 2) in our root group
774 originalStateArray = [[self builtInStateArray] arrayByAddingObjectsFromArray:[[self rootStateGroup] containedStatusItems]];
776 tempArray = [originalStateArray mutableCopy];
778 //Now add the temporary statues
779 [tempArray addObjectsFromArray:[temporaryStateArray allObjects]];
781 //Pass the original array so its indexes can be used for comparison of saved state ordering
782 [AIStatusGroup sortArrayOfStatusItems:tempArray context:originalStateArray];
784 _sortedFullStateArray = tempArray;
787 return _sortedFullStateArray;
791 * @brief Generate and return an array of AIStatus objects which are all known saved, temporary, and built-in statuses
793 - (NSArray *)flatStatusSet
795 if (!_flatStatusSet) {
796 NSMutableArray *tempArray = [[[self rootStateGroup] flatStatusSet] mutableCopy];
798 //Add built in states
799 [tempArray addObjectsFromArray:[self builtInStateArray]];
802 [tempArray addObjectsFromArray:[temporaryStateArray allObjects]];
804 _flatStatusSet = tempArray;
807 return _flatStatusSet;
811 * @brief Retrieve active status state
813 * @result The currently active status state.
815 * This is defined as the status state which the most accounts are currently using. The behavior in case of a tie
816 * is currently undefined but will yield one of the tying states.
818 - (AIStatus *)activeStatusState
820 if (!_activeStatusState) {
821 NSEnumerator *enumerator = [[[adium accountController] accounts] objectEnumerator];
822 NSCountedSet *statusCounts = [NSCountedSet set];
824 AIStatus *statusState;
825 unsigned highestCount = 0;
826 //This was "oneOrMoreConnectedOrConnectingAccounts" before... was there a good reason?
827 BOOL accountsAreOnline = [[adium accountController] oneOrMoreConnectedAccounts];
829 if (accountsAreOnline) {
830 AIStatus *bestStatusState = nil;
832 while ((account = [enumerator nextObject])) {
833 if ([account online]) {
834 AIStatus *accountStatusState = [account statusState];
835 [statusCounts addObject:(accountStatusState ?
837 [self defaultInitialStatusState])];
841 enumerator = [statusCounts objectEnumerator];
842 while ((statusState = [enumerator nextObject])) {
843 unsigned thisCount = [statusCounts countForObject:statusState];
844 if (thisCount > highestCount) {
845 bestStatusState = statusState;
846 highestCount = thisCount;
850 _activeStatusState = (bestStatusState ? [bestStatusState retain]: [offlineStatusState retain]);
853 _activeStatusState = [offlineStatusState retain];
857 return _activeStatusState;
861 * @brief Find the 'active' AIStatusType
863 * The active type is the one used by the largest number of accounts. In case of a tie, the order of the AIStatusType
866 * @param invisibleIsAway If YES, AIInvisibleStatusType is trated as AIAwayStatusType
867 * @result The active AIStatusType for online accounts, or AIOfflineStatusType if all accounts are offline
869 - (AIStatusType)activeStatusTypeTreatingInvisibleAsAway:(BOOL)invisibleIsAway
871 NSEnumerator *enumerator = [[[adium accountController] accounts] objectEnumerator];
873 int statusTypeCount[STATUS_TYPES_COUNT];
874 AIStatusType activeStatusType = AIOfflineStatusType;
875 unsigned highestCount = 0;
878 for (i = 0 ; i < STATUS_TYPES_COUNT ; i++) {
879 statusTypeCount[i] = 0;
882 while ((account = [enumerator nextObject])) {
883 if ([account online] || [account integerStatusObjectForKey:@"Connecting"]) {
884 AIStatusType statusType = [[account statusState] statusType];
886 //If invisibleIsAway, pretend that invisible is away
887 if (invisibleIsAway && (statusType == AIInvisibleStatusType)) statusType = AIAwayStatusType;
889 statusTypeCount[statusType]++;
893 for (i = 0 ; i < STATUS_TYPES_COUNT ; i++) {
894 if (statusTypeCount[i] > highestCount) {
895 activeStatusType = i;
896 highestCount = statusTypeCount[i];
900 return activeStatusType;
904 * @brief All active status states
906 * A status state is active if any enabled account is currently in that state.
908 * The return value of this method is cached.
910 * @result An <tt>NSSet</tt> of <tt>AIStatus</tt> objects
912 - (NSSet *)allActiveStatusStates
914 if (!_allActiveStatusStates) {
915 _allActiveStatusStates = [[NSMutableSet alloc] init];
916 NSEnumerator *enumerator = [[[adium accountController] accounts] objectEnumerator];
919 while ((account = [enumerator nextObject])) {
920 if ([account enabled]) {
921 [_allActiveStatusStates addObject:[account statusState]];
926 return _allActiveStatusStates;
930 * @brief Return the set of all unavailable statuses in use by online or connection accounts
932 * @param activeUnvailableStatusType Pointer to an AIStatusType; returns by reference the most popular unavailable type
933 * @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
934 * @param allOnlineAccountsAreUnvailable Pointer to a BOOL; returns by reference YES is all online accounts are unavailable, NO if one or more is available
936 - (NSSet *)activeUnavailableStatusesAndType:(AIStatusType *)activeUnvailableStatusType withName:(NSString **)activeUnvailableStatusName allOnlineAccountsAreUnvailable:(BOOL *)allOnlineAccountsAreUnvailable
938 NSEnumerator *enumerator = [[[adium accountController] accounts] objectEnumerator];
940 NSMutableSet *activeUnvailableStatuses = [NSMutableSet set];
941 BOOL foundStatusName = NO;
942 int statusTypeCount[STATUS_TYPES_COUNT];
944 statusTypeCount[AIAwayStatusType] = 0;
945 statusTypeCount[AIInvisibleStatusType] = 0;
947 //Assume all accounts are unavailable until proven otherwise
948 if (allOnlineAccountsAreUnvailable != NULL) {
949 *allOnlineAccountsAreUnvailable = YES;
952 while ((account = [enumerator nextObject])) {
953 if ([account online] || [account integerStatusObjectForKey:@"Connecting"]) {
954 AIStatus *statusState = [account statusState];
955 AIStatusType statusType = [statusState statusType];
957 if ((statusType == AIAwayStatusType) || (statusType == AIInvisibleStatusType)) {
958 NSString *statusName = [statusState statusName];
960 [activeUnvailableStatuses addObject:statusState];
962 statusTypeCount[statusType]++;
964 if (foundStatusName) {
965 //Once we find a status name, we only want to return it if all our status names are the same.
966 if ((activeUnvailableStatusName != NULL) &&
967 (*activeUnvailableStatusName != nil) &&
968 ![*activeUnvailableStatusName isEqualToString:statusName]) {
969 *activeUnvailableStatusName = nil;
972 //We haven't found a status name yet, so store this one as the active status name
973 if (activeUnvailableStatusName != NULL) {
974 *activeUnvailableStatusName = [statusState statusName];
976 foundStatusName = YES;
979 //An online account isn't unavailable
980 if (allOnlineAccountsAreUnvailable != NULL) {
981 *allOnlineAccountsAreUnvailable = NO;
987 if (activeUnvailableStatusType != NULL) {
988 if (statusTypeCount[AIAwayStatusType] > statusTypeCount[AIInvisibleStatusType]) {
989 *activeUnvailableStatusType = AIAwayStatusType;
991 *activeUnvailableStatusType = AIInvisibleStatusType;
995 return activeUnvailableStatuses;
1000 * @brief Next available unique status ID
1002 - (NSNumber *)nextUniqueStatusID
1004 NSNumber *nextUniqueStatusID;
1006 //Retain and autorelease since we'll be replacing this value (and therefore releasing it) via the preferenceController.
1007 nextUniqueStatusID = [[[[adium preferenceController] preferenceForKey:TOP_STATUS_STATE_ID
1008 group:PREF_GROUP_SAVED_STATUS] retain] autorelease];
1009 if (!nextUniqueStatusID) nextUniqueStatusID = [NSNumber numberWithInt:1];
1011 [[adium preferenceController] setPreference:[NSNumber numberWithInt:([nextUniqueStatusID intValue] + 1)]
1012 forKey:TOP_STATUS_STATE_ID
1013 group:PREF_GROUP_SAVED_STATUS];
1015 return nextUniqueStatusID;
1019 * @brief Find the status state with the requested uniqueStatusID
1021 - (AIStatus *)statusStateWithUniqueStatusID:(NSNumber *)uniqueStatusID
1023 AIStatus *statusState = nil;
1025 if (uniqueStatusID) {
1026 NSEnumerator *enumerator = [[self flatStatusSet] objectEnumerator];
1028 while ((statusState = [enumerator nextObject])) {
1029 if ([[statusState uniqueStatusID] compare:uniqueStatusID] == NSOrderedSame)
1037 //State Editing --------------------------------------------------------------------------------------------------------
1038 #pragma mark State Editing
1040 * @brief Add a state
1042 * Add a new state to Adium's state array.
1043 * @param state AIState to add
1045 - (void)addStatusState:(AIStatus *)statusState
1047 AIStatusMutabilityType mutabilityType = [statusState mutabilityType];
1049 if ((mutabilityType == AILockedStatusState) ||
1050 (mutabilityType == AISecondaryLockedStatusState)) {
1051 //If we are adding a locked status, add it to the built-in statuses
1052 [(NSMutableArray *)[self builtInStateArray] addObject:statusState];
1054 [self notifyOfChangedStatusArray];
1057 //Otherwise, add it to the user-created statuses
1058 [[self rootStateGroup] addStatusItem:statusState atIndex:-1];
1063 * @brief Remove a state
1065 * Remove a new state from Adium's state array.
1066 * @param state AIStatus to remove
1068 - (void)removeStatusState:(AIStatus *)statusState
1070 NSLog(@"shouldn't be calling this.");
1071 // [stateArray removeObject:statusState];
1072 [self savedStatusesChanged];
1075 - (void)notifyOfChangedStatusArray
1077 //Clear the sorted menu items array since our state array changed.
1078 [_sortedFullStateArray release]; _sortedFullStateArray = nil;
1079 [_flatStatusSet release]; _flatStatusSet = nil;
1081 if (!statusMenuRebuildDelays) {
1082 [[adium notificationCenter] postNotificationName:AIStatusStateArrayChangedNotification object:nil];
1087 * @brief Save changes to the state array and notify observers
1089 * Saves any outstanding changes to the state array. There should be no need to call this manually, since all the
1090 * state array modifying methods in this class call it automatically after making changes.
1092 * After the state array is saved, observers are notified that is has changed. Call after making any changes to the
1093 * state array from within the controller.
1095 - (void)savedStatusesChanged
1097 //XXX change this back sometime before 1.0 release
1098 // [[adium preferenceController] setPreference:[NSKeyedArchiver archivedDataWithRootObject:[self rootStateGroup]]
1099 [[adium preferenceController] setPreference:[NSKeyedArchiver archivedDataWithRootObject:[[self rootStateGroup] containedStatusItems]]
1100 forKey:KEY_SAVED_STATUS
1101 group:PREF_GROUP_SAVED_STATUS];
1102 [self notifyOfChangedStatusArray];
1105 - (void)statusStateDidSetUniqueStatusID
1107 //XXX change this back sometime before 1.0 release
1108 // [[adium preferenceController] setPreference:[NSKeyedArchiver archivedDataWithRootObject:[self rootStateGroup]]
1109 [[adium preferenceController] setPreference:[NSKeyedArchiver archivedDataWithRootObject:[[self rootStateGroup] containedStatusItems]]
1110 forKey:KEY_SAVED_STATUS
1111 group:PREF_GROUP_SAVED_STATUS];
1115 * @brief Called when a state could potentially need to removed from the temporary (non-saved) list
1117 * If originalState is in the temporary status array, and it is being used on one or zero accounts, it
1118 * is removed from the temporary status array. This method should be used when one or more accounts have stopped
1119 * using a single status state to determine if that status state is both non-saved and unused.
1121 * Note that while it would seem logical to post AIStatusStateArrayChangedNotification when this method would
1122 * return YES, we don't want to force observers of the notification to update immediately since there may be further
1123 * processing. We therefore let the calling method take action if it chooses to.
1125 * @result YES if the state was removed
1127 - (BOOL)removeIfNecessaryTemporaryStatusState:(AIStatus *)originalState
1129 BOOL didRemove = NO;
1131 /* If the original (old) status state is in our temporary array and is not being used in more than 1 account,
1132 * then we should remove it.
1134 if ([temporaryStateArray containsObject:originalState]) {
1135 NSEnumerator *enumerator;
1139 enumerator = [[[adium accountController] accounts] objectEnumerator];
1140 while ((account = [enumerator nextObject])) {
1141 if ([account actualStatusState] == originalState) {
1142 if (++count > 1) break;
1147 [temporaryStateArray removeObject:originalState];
1155 //Status state menu support ---------------------------------------------------------------------------------------------------
1156 #pragma mark Status state menu support
1158 * @brief Apply a custom state
1160 * Invoked when the custom state window is closed by the user clicking OK. In response this method sets the custom
1161 * state as the active state.
1163 - (void)customStatusState:(AIStatus *)originalState changedTo:(AIStatus *)newState forAccount:(AIAccount *)account
1165 BOOL shouldRebuild = NO;
1168 shouldRebuild = [self removeIfNecessaryTemporaryStatusState:originalState];
1170 //Now set the newState for the account
1171 [account setStatusState:newState];
1173 //Enable the account if it isn't currently enabled
1174 if (![account enabled]) {
1175 [account setEnabled:YES];
1179 //Set the state for all accounts. This will clear out the temporaryStatusArray as necessary.
1180 [self setActiveStatusState:newState];
1183 if ([newState mutabilityType] != AITemporaryEditableStatusState) {
1184 [[adium statusController] addStatusState:newState];
1187 NSMutableDictionary *lastStatusStates;
1189 lastStatusStates = [[[adium preferenceController] preferenceForKey:@"LastStatusStates"
1190 group:PREF_GROUP_STATUS_PREFERENCES] mutableCopy];
1191 if (!lastStatusStates) lastStatusStates = [NSMutableDictionary dictionary];
1193 [lastStatusStates setObject:[NSKeyedArchiver archivedDataWithRootObject:newState]
1194 forKey:[NSNumber numberWithInt:[newState statusType]]];
1196 [[adium preferenceController] setPreference:lastStatusStates
1197 forKey:@"LastStatusStates"
1198 group:PREF_GROUP_STATUS_PREFERENCES];
1200 //Add to our temporary status array if it's not in our state array
1201 if (shouldRebuild || (![[self flatStatusSet] containsObject:newState])) {
1202 [temporaryStateArray addObject:newState];
1204 [self notifyOfChangedStatusArray];
1209 #pragma mark Upgrade code
1211 * @brief Temporary upgrade code for 0.7x -> 0.8
1213 * Versions 0.7x and prior stored their away messages in a different format. This code allows a seamless
1214 * transition from 0.7x to 0.8. We can easily recognize the old format because the away messages are of
1215 * type "Away" instead of type "State", which is used for all 0.8 and later saved states.
1216 * Since we are changing the array as we scan it, an enumerator will not work here.
1218 #define OLD_KEY_SAVED_AWAYS @"Saved Away Messages"
1219 #define OLD_GROUP_AWAY_MESSAGES @"Away Messages"
1220 #define OLD_STATE_SAVED_AWAY @"Away"
1221 #define OLD_STATE_AWAY @"Message"
1222 #define OLD_STATE_AUTO_REPLY @"Autoresponse"
1223 #define OLD_STATE_TITLE @"Title"
1224 - (void)_upgradeSavedAwaysToSavedStates
1226 NSArray *savedAways = [[adium preferenceController] preferenceForKey:OLD_KEY_SAVED_AWAYS
1227 group:OLD_GROUP_AWAY_MESSAGES];
1230 NSEnumerator *enumerator = [savedAways objectEnumerator];
1231 NSDictionary *state;
1233 AILog(@"*** Upgrading Adium 0.7x saved aways: %@", savedAways);
1235 [self setDelayStatusMenuRebuilding:YES];
1237 //Update all the away messages to states.
1238 while ((state = [enumerator nextObject])) {
1239 if ([[state objectForKey:@"Type"] isEqualToString:OLD_STATE_SAVED_AWAY]) {
1240 AIStatus *statusState;
1242 //Extract the away message information from this old record
1243 NSData *statusMessageData = [state objectForKey:OLD_STATE_AWAY];
1244 NSData *autoReplyMessageData = [state objectForKey:OLD_STATE_AUTO_REPLY];
1245 NSString *title = [state objectForKey:OLD_STATE_TITLE];
1247 //Create an AIStatus from this information
1248 statusState = [AIStatus status];
1250 //General category: It's an away type
1251 [statusState setStatusType:AIAwayStatusType];
1253 //Specific state: It's the generic away. Funny how that works out.
1254 [statusState setStatusName:STATUS_NAME_AWAY];
1256 //Set the status message (which is just the away message).
1257 [statusState setStatusMessage:[NSAttributedString stringWithData:statusMessageData]];
1259 //It has an auto reply.
1260 [statusState setHasAutoReply:YES];
1262 if (autoReplyMessageData) {
1263 //Use the custom auto reply if it was set.
1264 [statusState setAutoReply:[NSAttributedString stringWithData:autoReplyMessageData]];
1266 //If no autoReplyMesssage, use the status message.
1267 [statusState setAutoReplyIsStatusMessage:YES];
1270 if (title) [statusState setTitle:title];
1272 //Add the updated state to our state array.
1273 [self addStatusState:statusState];
1277 AILog(@"*** Finished upgrading old saved statuses");
1279 //Save these changes and delete the old aways so we don't need to do this again.
1280 [self setDelayStatusMenuRebuilding:NO];
1282 [[adium preferenceController] setPreference:nil
1283 forKey:OLD_KEY_SAVED_AWAYS
1284 group:OLD_GROUP_AWAY_MESSAGES];