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 AILocalizedStringFromTable(@"Offline", @"Statuses", "Name of a status")
41 #define BUILT_IN_STATE_ARRAY @"BuiltInStatusStates"
43 @interface AIStatusController (PRIVATE)
44 - (NSArray *)builtInStateArray;
46 - (void)_upgradeSavedAwaysToSavedStates;
48 - (NSArray *)_menuItemsForStatusesOfType:(AIStatusType)type forServiceCodeUniqueID:(NSString *)inServiceCodeUniqueID withTarget:(id)target;
49 - (void)_addMenuItemsForStatusOfType:(AIStatusType)type
51 fromSet:(NSSet *)sourceArray
52 toArray:(NSMutableArray *)menuItems
53 alreadyAddedTitles:(NSMutableSet *)alreadyAddedTitles;
54 - (void)buildBuiltInStatusTypes;
55 - (void)notifyOfChangedStatusArray;
59 * @class AIStatusController
60 * @brief Core status & state methods
62 * This class provides a foundation for Adium's status and status state systems.
64 @implementation AIStatusController
66 static NSMutableSet *temporaryStateArray = nil;
69 * Init the status controller
73 if ((self = [super init])) {
74 stateMenuItemArraysDict = [[NSMutableDictionary alloc] init];
75 stateMenuPluginsArray = [[NSMutableArray alloc] init];
76 stateMenuItemsNeedingUpdating = [[NSMutableSet alloc] init];
77 activeStatusUpdateDelays = 0;
78 _sortedFullStateArray = nil;
79 _activeStatusState = nil;
80 _allActiveStatusStates = nil;
81 temporaryStateArray = [[NSMutableSet alloc] init];
83 accountsToConnect = [[NSMutableSet alloc] init];
85 idleManager = [[AdiumIdleManager alloc] init];
92 * @brief Finish initing the status controller
94 * Set our initial status state, and restore our array of accounts to connect when a global state is selected.
96 - (void)controllerDidLoad
98 NSEnumerator *enumerator;
101 [[adium contactController] registerListObjectObserver:self];
103 [self buildBuiltInStatusTypes];
105 //Put each account into the status it was in last time we quit.
106 BOOL needToRebuildMenus = NO;
107 BOOL allStatusesInSameState = YES;
108 AIStatus *prevStatus = nil;
109 enumerator = [[[adium accountController] accounts] objectEnumerator];
110 while ((account = [enumerator nextObject])) {
111 NSData *lastStatusData = [account preferenceForKey:@"LastStatus"
112 group:GROUP_ACCOUNT_STATUS];
113 AIStatus *lastStatus = nil;
115 lastStatus = [NSKeyedUnarchiver unarchiveObjectWithData:lastStatusData];
117 if (lastStatus && [lastStatus isKindOfClass:[AIStatus class]]) {
118 AIStatus *existingStatus;
120 /* We want to use a loaded status instance if one exists. This will be the case if the account
121 * was last in a built-in or user defined and saved state. If the last state was unsaved, existingStatus
124 existingStatus = [self statusStateWithUniqueStatusID:[lastStatus uniqueStatusID]];
126 if (existingStatus) {
127 lastStatus = existingStatus;
129 //Add to our temporary status array
130 [temporaryStateArray addObject:lastStatus];
132 /* We could clear out _flatStatusSet for the next iteration, but we _know_ what changed,
133 * so modify it directly for efficiency.
135 [_flatStatusSet addObject:lastStatus];
137 needToRebuildMenus = YES;
140 prevStatus = lastStatus;
141 else if (prevStatus != lastStatus)
142 allStatusesInSameState = NO;
143 [account setStatusStateAndRemainOffline:lastStatus];
147 if (needToRebuildMenus) {
148 [self notifyOfChangedStatusArray];
153 * @brief Begin closing the status controller
155 * Save the online accounts; they will be the accounts connected by a global status change
157 * Also save the current status state of each account so it can be restored on next launch.
159 - (void)controllerWillClose
161 NSEnumerator *enumerator;
164 enumerator = [[[adium accountController] accounts] objectEnumerator];
165 while ((account = [enumerator nextObject])) {
166 /* Store the current status state for use on next launch.
168 * We use the statusObjectForKey:@"StatusState" accessor rather than [account statusState]
169 * because we don't want anything besides the account's actual status state. That is, we don't
170 * want the default available state if the account doesn't have a state yet, and we want the
171 * real last-state-which-was-set (not the offline one) if the account is offline.
173 AIStatus *currentStatus = [account statusObjectForKey:@"StatusState"];
174 [account setPreference:((currentStatus && (currentStatus != offlineStatusState)) ?
175 [NSKeyedArchiver archivedDataWithRootObject:currentStatus] :
178 group:GROUP_ACCOUNT_STATUS];
181 //XXX change this back sometime before 1.0 release
182 // [[adium preferenceController] setPreference:[NSKeyedArchiver archivedDataWithRootObject:[self rootStateGroup]]
183 [[adium preferenceController] setPreference:[NSKeyedArchiver archivedDataWithRootObject:[[self rootStateGroup] containedStatusItems]]
184 forKey:KEY_SAVED_STATUS
185 group:PREF_GROUP_SAVED_STATUS];
187 [[adium notificationCenter] removeObserver:self];
188 [[adium preferenceController] unregisterPreferenceObserver:self];
189 [[adium contactController] unregisterListObjectObserver:self];
197 [_rootStateGroup release]; _rootStateGroup = nil;
198 [_sortedFullStateArray release]; _sortedFullStateArray = nil;
202 #pragma mark Status registration
204 * @brief Register a status for a service
206 * Implementation note: Each AIStatusType has its own NSMutableDictionary, statusDictsByServiceCodeUniqueID.
207 * statusDictsByServiceCodeUniqueID is keyed by serviceCodeUniqueID; each object is an NSMutableSet of NSDictionaries.
208 * Each of these dictionaries has KEY_STATUS_NAME, KEY_STATUS_DESCRIPTION, and KEY_STATUS_TYPE.
210 * @param statusName A name which will be passed back to accounts of this service. Internal use only. Use the AIStatusController.h #defines where appropriate.
211 * @param description A human-readable localized description which will be shown to the user. Use the AIStatusController.h #defines where appropriate.
212 * @param type An AIStatusType, the general type of this status.
213 * @param service The AIService for which to register the status
215 - (void)registerStatus:(NSString *)statusName withDescription:(NSString *)description ofType:(AIStatusType)type forService:(AIService *)service
217 NSMutableSet *statusDicts;
218 NSString *serviceCodeUniqueID = [service serviceCodeUniqueID];
220 //Create the set if necessary
221 if (!statusDictsByServiceCodeUniqueID[type]) statusDictsByServiceCodeUniqueID[type] = [[NSMutableDictionary alloc] init];
222 if (!(statusDicts = [statusDictsByServiceCodeUniqueID[type] objectForKey:serviceCodeUniqueID])) {
223 statusDicts = [NSMutableSet set];
224 [statusDictsByServiceCodeUniqueID[type] setObject:statusDicts
225 forKey:serviceCodeUniqueID];
228 //Create a dictionary for this status entry
229 NSDictionary *statusDict = [NSDictionary dictionaryWithObjectsAndKeys:
230 statusName, KEY_STATUS_NAME,
231 description, KEY_STATUS_DESCRIPTION,
232 [NSNumber numberWithInt:type], KEY_STATUS_TYPE,
235 [statusDicts addObject:statusDict];
238 #pragma mark Status menus
240 * @brief Generate and return a menu of status types (Away, Be right back, etc.)
242 * @param service The service for which to return a specific list of types, or nil to return all available types
243 * @param target The target for the menu items, which will have an action of @selector(selectStatus:)
245 * @result The menu of statuses, separated by available and away status types
247 - (NSMenu *)menuOfStatusesForService:(AIService *)service withTarget:(id)target
249 NSMenu *menu = [[NSMenu allocWithZone:[NSMenu menuZone]] init];
250 NSEnumerator *enumerator;
251 NSMenuItem *menuItem;
252 NSString *serviceCodeUniqueID = [service serviceCodeUniqueID];
255 for (type = AIAvailableStatusType ; type < STATUS_TYPES_COUNT ; type++) {
256 NSArray *menuItemArray;
258 menuItemArray = [self _menuItemsForStatusesOfType:type
259 forServiceCodeUniqueID:serviceCodeUniqueID
262 //Add a separator between each type after available
263 if ((type > AIAvailableStatusType) && [menuItemArray count]) {
264 [menu addItem:[NSMenuItem separatorItem]];
267 //Add the items for this type
268 enumerator = [menuItemArray objectEnumerator];
269 while ((menuItem = [enumerator nextObject])) {
270 [menu addItem:menuItem];
274 return [menu autorelease];
278 * @brief Return an array of menu items for an AIStatusType and service
280 * @pram type The AIStatusType for which to return statuses
281 * @param inServiceCodeUniqueID The service for which to return active statuses. If nil, return all statuses for online services.
282 * @param target The target for the menu items
284 * @result An <tt>NSArray</tt> of <tt>NSMenuItem</tt> objects.
286 - (NSArray *)_menuItemsForStatusesOfType:(AIStatusType)type forServiceCodeUniqueID:(NSString *)inServiceCodeUniqueID withTarget:(id)target
288 NSMutableArray *menuItems = [[NSMutableArray alloc] init];
289 NSMutableSet *alreadyAddedTitles = [NSMutableSet set];
291 //First, add our built-in items (so they will be at the top of the array and service-specific 'copies' won't replace them)
292 [self _addMenuItemsForStatusOfType:type
294 fromSet:builtInStatusTypes[type]
296 alreadyAddedTitles:alreadyAddedTitles];
298 //Now, add items for this service, or from all available services, as appropriate
299 if (inServiceCodeUniqueID) {
302 //Obtain the status dicts for this type and service code unique ID
303 if ((statusDicts = [statusDictsByServiceCodeUniqueID[type] objectForKey:inServiceCodeUniqueID])) {
305 [self _addMenuItemsForStatusOfType:type
309 alreadyAddedTitles:alreadyAddedTitles];
313 NSEnumerator *enumerator;
316 enumerator = [[[adium accountController] activeServicesIncludingCompatibleServices:NO] objectEnumerator];
317 while ((service = [enumerator nextObject])) {
320 //Obtain the status dicts for this type and service code unique ID
321 if ((statusDicts = [statusDictsByServiceCodeUniqueID[type] objectForKey:[service serviceCodeUniqueID]])) {
323 [self _addMenuItemsForStatusOfType:type
327 alreadyAddedTitles:alreadyAddedTitles];
333 [menuItems sortUsingSelector:@selector(titleCompare:)];
335 return [menuItems autorelease];
339 * @brief Add menu items for a particular type of status
341 * @param type The AIStatusType, used for determining the icon of the menu items
342 * @param target The target of the created menu items
343 * @param statusDicts An NSSet of NSDictionary objects, which should each represent a status of the passed type
344 * @param menuItems The NSMutableArray to which to add the menuItems
345 * @param alreadyAddedTitles NSMutableSet of NSString titles which have already been added and should not be duplicated. Will be updated as items are added.
347 - (void)_addMenuItemsForStatusOfType:(AIStatusType)type
348 withTarget:(id)target
349 fromSet:(NSSet *)statusDicts
350 toArray:(NSMutableArray *)menuItems
351 alreadyAddedTitles:(NSMutableSet *)alreadyAddedTitles
353 NSEnumerator *statusDictEnumerator = [statusDicts objectEnumerator];
354 NSDictionary *statusDict;
356 //Enumerate the status dicts
357 while ((statusDict = [statusDictEnumerator nextObject])) {
358 NSString *title = [statusDict objectForKey:KEY_STATUS_DESCRIPTION];
361 * Only add if it has not already been added by another service.... Services need to use unique titles if they have
362 * unique state names, but are welcome to share common name/description combinations, which is why the #defines
365 if (![alreadyAddedTitles containsObject:title]) {
367 NSMenuItem *menuItem;
369 menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:title
371 action:@selector(selectStatus:)
374 image = [AIStatusIcons statusIconForStatusName:[statusDict objectForKey:KEY_STATUS_NAME]
376 iconType:AIStatusIconMenu
377 direction:AIIconNormal];
379 [menuItem setRepresentedObject:statusDict];
380 [menuItem setImage:image];
381 [menuItem setEnabled:YES];
382 [menuItems addObject:menuItem];
385 [alreadyAddedTitles addObject:title];
390 #pragma mark Status State Descriptions
391 - (NSString *)localizedDescriptionForCoreStatusName:(NSString *)statusName
393 static NSDictionary *coreLocalizedStatusDescriptions = nil;
394 if(!coreLocalizedStatusDescriptions){
395 coreLocalizedStatusDescriptions = [[NSDictionary dictionaryWithObjectsAndKeys:
396 AILocalizedStringFromTable(@"Available", @"Statuses", "Name of a status"), STATUS_NAME_AVAILABLE,
397 AILocalizedStringFromTable(@"Free for chat", @"Statuses", "Name of a status"), STATUS_NAME_FREE_FOR_CHAT,
398 AILocalizedStringFromTable(@"Available for friends only", @"Statuses", "Name of a status"), STATUS_NAME_AVAILABLE_FRIENDS_ONLY,
399 AILocalizedStringFromTable(@"Away", @"Statuses", "Name of a status"), STATUS_NAME_AWAY,
400 AILocalizedStringFromTable(@"Extended away", @"Statuses", "Name of a status"), STATUS_NAME_EXTENDED_AWAY,
401 AILocalizedStringFromTable(@"Away for friends only", @"Statuses", "Name of a status"), STATUS_NAME_AWAY_FRIENDS_ONLY,
402 AILocalizedStringFromTable(@"Do not disturb", @"Statuses", "Name of a status"), STATUS_NAME_DND,
403 AILocalizedStringFromTable(@"Not available", @"Statuses", "Name of a status"), STATUS_NAME_NOT_AVAILABLE,
404 AILocalizedStringFromTable(@"Occupied", @"Statuses", "Name of a status"), STATUS_NAME_OCCUPIED,
405 AILocalizedStringFromTable(@"Be right back", @"Statuses", "Name of a status"), STATUS_NAME_BRB,
406 AILocalizedStringFromTable(@"Busy", @"Statuses", "Name of a status"), STATUS_NAME_BUSY,
407 AILocalizedStringFromTable(@"On the phone", @"Statuses", "Name of a status"), STATUS_NAME_PHONE,
408 AILocalizedStringFromTable(@"Out to lunch", @"Statuses", "Name of a status"), STATUS_NAME_LUNCH,
409 AILocalizedStringFromTable(@"Not at home", @"Statuses", "Name of a status"), STATUS_NAME_NOT_AT_HOME,
410 AILocalizedStringFromTable(@"Not at my desk", @"Statuses", "Name of a status"), STATUS_NAME_NOT_AT_DESK,
411 AILocalizedStringFromTable(@"Not in the office", @"Statuses", "Name of a status"), STATUS_NAME_NOT_IN_OFFICE,
412 AILocalizedStringFromTable(@"On vacation", @"Statuses", "Name of a status"), STATUS_NAME_VACATION,
413 AILocalizedStringFromTable(@"Stepped out", @"Statuses", "Name of a status"), STATUS_NAME_STEPPED_OUT,
414 AILocalizedStringFromTable(@"Invisible", @"Statuses", "Name of a status"), STATUS_NAME_INVISIBLE,
415 AILocalizedStringFromTable(@"Offline", @"Statuses", "Name of a status"), STATUS_NAME_OFFLINE,
419 return (statusName ? [coreLocalizedStatusDescriptions objectForKey:statusName] : nil);
422 - (NSString *)localizedDescriptionForStatusName:(NSString *)statusName statusType:(AIStatusType)statusType
424 NSString *description = nil;
427 !(description = [self localizedDescriptionForCoreStatusName:statusName])) {
428 NSEnumerator *enumerator = [statusDictsByServiceCodeUniqueID[statusType] objectEnumerator];
431 while (!description && (set = [enumerator nextObject])) {
432 NSEnumerator *statusDictsEnumerator = [set objectEnumerator];
433 NSDictionary *statusDict;
434 while (!description && (statusDict = [statusDictsEnumerator nextObject])) {
435 if ([[statusDict objectForKey:KEY_STATUS_NAME] isEqualToString:statusName]){
436 description = [statusDict objectForKey:KEY_STATUS_DESCRIPTION];
446 * @brief Return the localized description for the sate of the passed status
448 * This could be stored with the statusState, but that would break if the locale changed. This way, the nonlocalized
449 * string is used to look up the appropriate localized one.
451 * @result A localized description such as @"Away" or @"Out to Lunch" of the state used by statusState
453 - (NSString *)descriptionForStateOfStatus:(AIStatus *)statusState
455 return [self localizedDescriptionForStatusName:[statusState statusName]
456 statusType:[statusState statusType]];
460 * @brief The status name to use by default for a passed type
462 * This is the name which will be used for new AIStatus objects of this type.
464 - (NSString *)defaultStatusNameForType:(AIStatusType)statusType
466 //Set the default status name
467 switch (statusType) {
468 case AIAvailableStatusType:
469 return STATUS_NAME_AVAILABLE;
471 case AIAwayStatusType:
472 return STATUS_NAME_AWAY;
474 case AIInvisibleStatusType:
475 return STATUS_NAME_INVISIBLE;
477 case AIOfflineStatusType:
478 return STATUS_NAME_OFFLINE;
485 #pragma mark Setting Status States
487 * @brief Set the active status state
489 * Sets the currently active status state. This applies throughout Adium and to all accounts. The state will become
490 * effective immediately.
492 - (void)setActiveStatusState:(AIStatus *)statusState
494 //Apply the state to our accounts and notify (delay to the next run loop to improve perceived speed)
495 [self performSelector:@selector(applyState:toAccounts:)
496 withObject:statusState
497 withObject:[[adium accountController] accounts]
501 * @brief Set the active status state for some account
503 * Sets the currently active status state for the specified account.
504 * This applies throughout Adium and to all accounts. The state will become
505 * effective immediately.
507 - (void)setActiveStatusState:(AIStatus *)state forAccount:(AIAccount *)account
509 [self removeIfNecessaryTemporaryStatusState:[account statusState]];
510 [self applyState:state toAccounts:[NSArray arrayWithObject:account]];
514 * @brief Return the <tt>AIStatus</tt> to be used by accounts as they are created
516 - (AIStatus *)defaultInitialStatusState
518 return [self availableStatus];
522 * @brief Reset the active status state
524 * All active status states cache will also reset. Posts an active status changed notification. The active state
525 * will be regenerated the next time it is requested.
527 - (void)_resetActiveStatusState
529 //Clear the active status state. It will be rebuilt next time it is requested
530 [_activeStatusState release]; _activeStatusState = nil;
531 [_allActiveStatusStates release]; _allActiveStatusStates = nil;
533 //Let observers know the active state has changed
534 if (!activeStatusUpdateDelays) {
535 [[adium notificationCenter] postNotificationName:AIStatusActiveStateChangedNotification object:nil];
540 * @brief Account status changed.
542 * Rebuild all our state menus
544 - (NSSet *)updateListObject:(AIListObject *)inObject keys:(NSSet *)inModifiedKeys silent:(BOOL)silent
546 if ([inObject isKindOfClass:[AIAccount class]]) {
547 if ([inModifiedKeys containsObject:@"Online"] ||
548 [inModifiedKeys containsObject:@"IdleSince"] ||
549 [inModifiedKeys containsObject:@"StatusState"] ||
550 [inModifiedKeys containsObject:KEY_ENABLED]) {
552 [self _resetActiveStatusState];
561 * @brief Delay activee status menu updates
563 * This should be called to prevent duplicative updates when multiple accounts are changing status simultaneously.
565 - (void)setDelayActiveStatusUpdates:(BOOL)shouldDelay
568 activeStatusUpdateDelays++;
570 activeStatusUpdateDelays--;
572 if (!activeStatusUpdateDelays) {
573 [[adium notificationCenter] postNotificationName:AIStatusActiveStateChangedNotification object:nil];
578 * @brief Delay activee status menu updates
580 * This should be called to prevent duplicative rebuilds when the status menu will change multple times.
582 - (void)setDelayStatusMenuRebuilding:(BOOL)shouldDelay
585 statusMenuRebuildDelays++;
587 statusMenuRebuildDelays--;
589 if (!statusMenuRebuildDelays) {
590 [[adium notificationCenter] postNotificationName:AIStatusStateArrayChangedNotification object:nil];
595 * @brief Apply a state to multiple accounts
597 - (void)applyState:(AIStatus *)statusState toAccounts:(NSArray *)accountArray
599 NSEnumerator *enumerator;
601 AIStatus *aStatusState;
602 BOOL shouldRebuild = NO;
603 BOOL isOfflineStatus = ([statusState statusType] == AIOfflineStatusType);
604 [self setDelayActiveStatusUpdates:YES];
606 /* If we're going offline, determine what accounts are currently online or connecting/reconnecting, first,
607 * so that we can restore that when an online state is chosen later.
609 if (isOfflineStatus && [[adium accountController] oneOrMoreConnectedOrConnectingAccounts]) {
610 [accountsToConnect removeAllObjects];
612 enumerator = [accountArray objectEnumerator];
613 while ((account = [enumerator nextObject])) {
614 // Save the account if we're online or trying to be online.
615 if ([account online] || [[account statusObjectForKey:@"Connecting"] boolValue] || [account statusObjectForKey:@"Waiting to Reconnect"])
616 [accountsToConnect addObject:account];
620 // Don't consider "connecting" accounts when connecting previously offline.
621 if (![[adium accountController] oneOrMoreConnectedAccounts]) {
622 /* No connected accounts: Connect all enabled accounts which were set offline previously.
623 * If we have no such list of accounts, connect 'em all.
625 BOOL noAccountsToConnectCount = ([accountsToConnect count] == 0);
626 enumerator = [accountArray objectEnumerator];
627 while ((account = [enumerator nextObject])) {
628 if ([account enabled] &&
629 ([accountsToConnect containsObject:account] || noAccountsToConnectCount)) {
630 [account setStatusState:statusState];
633 [account setStatusStateAndRemainOffline:statusState];
638 //At least one account is online. Just change its status without taking any other accounts online.
639 enumerator = [accountArray objectEnumerator];
640 while ((account = [enumerator nextObject])) {
641 if ([account online] || isOfflineStatus) {
642 [account setStatusState:statusState];
645 [account setStatusStateAndRemainOffline:statusState];
651 //If this is not an offline status, we've now made use of accountsToConnect and should clear it so it isn't used again.
652 if (!isOfflineStatus) {
653 [accountsToConnect removeAllObjects];
656 //Any objects in the temporary state array which aren't the state we just set should now be removed.
657 enumerator = [[[temporaryStateArray copy] autorelease] objectEnumerator];
658 while ((aStatusState = [enumerator nextObject])) {
659 if (aStatusState != statusState) {
660 [temporaryStateArray removeObject:aStatusState];
665 //Add to our temporary status array if it's not in our state array
666 if (![[self flatStatusSet] containsObject:statusState] &&
667 ![temporaryStateArray containsObject:statusState]) {
668 [temporaryStateArray addObject:statusState];
673 [self notifyOfChangedStatusArray];
676 [self setDelayActiveStatusUpdates:NO];
679 #pragma mark Retrieving Status States
681 * @brief Access to Adium's user-defined states
683 * Returns the root AIStatusGroup of user-defined states
685 - (AIStatusGroup *)rootStateGroup
687 if (!_rootStateGroup) {
688 NSData *savedStateData = [[adium preferenceController] preferenceForKey:KEY_SAVED_STATUS
689 group:PREF_GROUP_SAVED_STATUS];
690 if (savedStateData) {
691 id archivedObject = [NSKeyedUnarchiver unarchiveObjectWithData:savedStateData];
693 if ([archivedObject isKindOfClass:[AIStatusGroup class]]) {
694 //Adium 1.0 archives an AIStatusGroup
695 _rootStateGroup = [archivedObject retain];
697 } else if ([archivedObject isKindOfClass:[NSArray class]]) {
698 //Adium 0.8x archived an NSArray
699 _rootStateGroup = [[AIStatusGroup statusGroupWithContainedStatusItems:archivedObject] retain];
703 if (!_rootStateGroup) _rootStateGroup = [[AIStatusGroup statusGroup] retain];
705 //Upgrade Adium 0.7x away messages
706 [self _upgradeSavedAwaysToSavedStates];
709 return _rootStateGroup;
713 * @brief Return the array of built-in states
715 * These are basic Available and Away states which should always be visible and are (by convention) immutable.
716 * The first state in BUILT_IN_STATE_ARRAY will be used as the default for accounts as they are created.
718 - (NSArray *)builtInStateArray
720 if (!builtInStateArray) {
721 NSArray *savedBuiltInStateArray = [NSArray arrayNamed:BUILT_IN_STATE_ARRAY forClass:[self class]];
722 NSEnumerator *enumerator;
725 builtInStateArray = [[NSMutableArray alloc] initWithCapacity:[savedBuiltInStateArray count]];
727 enumerator = [savedBuiltInStateArray objectEnumerator];
728 while ((dict = [enumerator nextObject])) {
729 AIStatus *status = [AIStatus statusWithDictionary:dict];
730 [builtInStateArray addObject:status];
732 //Store a reference to our offline state if we just loaded it
733 if ([status statusType] == AIOfflineStatusType) {
734 [offlineStatusState release];
735 offlineStatusState = [status retain];
740 return builtInStateArray;
744 * @brief Create and add the built-in status types; even if no service explicitly registers these, they are available.
746 * The built-in status types are basic, generic "Available" and "Away" states.
748 - (void)buildBuiltInStatusTypes
750 NSDictionary *statusDict;
752 builtInStatusTypes[AIAvailableStatusType] = [[NSMutableSet alloc] init];
753 statusDict = [NSDictionary dictionaryWithObjectsAndKeys:
754 STATUS_NAME_AVAILABLE, KEY_STATUS_NAME,
755 [self localizedDescriptionForCoreStatusName:STATUS_NAME_AVAILABLE], KEY_STATUS_DESCRIPTION,
756 [NSNumber numberWithInt:AIAvailableStatusType], KEY_STATUS_TYPE,
758 [builtInStatusTypes[AIAvailableStatusType] addObject:statusDict];
760 builtInStatusTypes[AIAwayStatusType] = [[NSMutableSet alloc] init];
761 statusDict = [NSDictionary dictionaryWithObjectsAndKeys:
762 STATUS_NAME_AWAY, KEY_STATUS_NAME,
763 [self localizedDescriptionForCoreStatusName:STATUS_NAME_AWAY], KEY_STATUS_DESCRIPTION,
764 [NSNumber numberWithInt:AIAwayStatusType], KEY_STATUS_TYPE,
766 [builtInStatusTypes[AIAwayStatusType] addObject:statusDict];
770 * @brief Returns the built in available status
772 - (AIStatus *)availableStatus
774 return [[[self builtInStateArray] filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"statusType == %i",AIAvailableStatusType]] objectAtIndex:0];
777 * @brief Returns the built in away status
779 - (AIStatus *)awayStatus
781 return [[[self builtInStateArray] filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"statusType == %i",AIAwayStatusType]] objectAtIndex:0];
784 * @brief Returns the built in invisible status
786 - (AIStatus *)invisibleStatus
788 return [[[self builtInStateArray] filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"statusType == %i",AIInvisibleStatusType]] objectAtIndex:0];
791 * @brief Returns the built in offline status
793 * This method duplicates the functionality found in - [AIStatusController offlineStatusState].
794 * However, this has the same method signature format as the other statuses.
796 - (AIStatus *)offlineStatus
798 return [[[self builtInStateArray] filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"statusType == %i",AIOfflineStatusType]] objectAtIndex:0];
801 - (AIStatus *)offlineStatusState
803 //Ensure the built in states have been loaded
804 [self builtInStateArray];
806 NSAssert(offlineStatusState != nil, @"Nil offline status state");
807 return offlineStatusState;
811 * @brief Return a sorted state array for use in menu item creation
813 * The array is created by adding the built in states to the user states, then sorting using _statusArraySort
814 * The resulting array may contain AIStatus and AIStatusGroup objects.
816 * @result A cached NSArray which is sorted by status type (available, away), built-in vs. user-made, and then original ordering.
818 - (NSArray *)sortedFullStateArray
820 if (!_sortedFullStateArray) {
821 NSArray *originalStateArray;
822 NSMutableArray *tempArray;
824 //Start with everything contained 1) in our built-in array and then 2) in our root group
825 originalStateArray = [[self builtInStateArray] arrayByAddingObjectsFromArray:[[self rootStateGroup] containedStatusItems]];
827 tempArray = [originalStateArray mutableCopy];
829 //Now add the temporary statues
830 [tempArray addObjectsFromArray:[temporaryStateArray allObjects]];
832 //Pass the original array so its indexes can be used for comparison of saved state ordering
833 [AIStatusGroup sortArrayOfStatusItems:tempArray context:originalStateArray];
835 _sortedFullStateArray = tempArray;
838 return _sortedFullStateArray;
842 * @brief Generate and return an array of AIStatus objects which are all known saved, temporary, and built-in statuses
844 - (NSSet *)flatStatusSet
846 if (!_flatStatusSet) {
847 NSMutableSet *tempArray = [[[self rootStateGroup] flatStatusSet] mutableCopy];
849 //Add built in states
850 [tempArray addObjectsFromArray:[self builtInStateArray]];
853 [tempArray addObjectsFromArray:[temporaryStateArray allObjects]];
855 _flatStatusSet = tempArray;
858 return _flatStatusSet;
862 * @brief Retrieve active status state
864 * @result The currently active status state.
866 * This is defined as the status state which the most accounts are currently using. The behavior in case of a tie
867 * is currently undefined but will yield one of the tying states.
869 - (AIStatus *)activeStatusState
871 if (!_activeStatusState) {
872 NSEnumerator *enumerator = [[[adium accountController] accounts] objectEnumerator];
873 NSCountedSet *statusCounts = [NSCountedSet set];
875 AIStatus *statusState;
876 unsigned highestCount = 0;
877 //This was "oneOrMoreConnectedOrConnectingAccounts" before... was there a good reason?
878 BOOL accountsAreOnline = [[adium accountController] oneOrMoreConnectedAccounts];
880 if (accountsAreOnline) {
881 AIStatus *bestStatusState = nil;
883 while ((account = [enumerator nextObject])) {
884 if ([account online]) {
885 AIStatus *accountStatusState = [account statusState];
886 [statusCounts addObject:(accountStatusState ?
888 [self defaultInitialStatusState])];
892 enumerator = [statusCounts objectEnumerator];
893 while ((statusState = [enumerator nextObject])) {
894 unsigned thisCount = [statusCounts countForObject:statusState];
895 if (thisCount > highestCount) {
896 bestStatusState = statusState;
897 highestCount = thisCount;
901 _activeStatusState = (bestStatusState ? [bestStatusState retain]: [offlineStatusState retain]);
904 _activeStatusState = [offlineStatusState retain];
908 return _activeStatusState;
912 * @brief Find the 'active' AIStatusType
914 * The active type is the one used by the largest number of accounts. In case of a tie, the order of the AIStatusType
917 * @param invisibleIsAway If YES, AIInvisibleStatusType is trated as AIAwayStatusType
918 * @result The active AIStatusType for online accounts, or AIOfflineStatusType if all accounts are offline
920 - (AIStatusType)activeStatusTypeTreatingInvisibleAsAway:(BOOL)invisibleIsAway
922 NSEnumerator *enumerator = [[[adium accountController] accounts] objectEnumerator];
924 int statusTypeCount[STATUS_TYPES_COUNT];
925 AIStatusType activeStatusType = AIOfflineStatusType;
926 unsigned highestCount = 0;
929 for (i = 0 ; i < STATUS_TYPES_COUNT ; i++) {
930 statusTypeCount[i] = 0;
933 while ((account = [enumerator nextObject])) {
934 if ([account online] || [account integerStatusObjectForKey:@"Connecting"]) {
935 AIStatusType statusType = [[account statusState] statusType];
937 //If invisibleIsAway, pretend that invisible is away
938 if (invisibleIsAway && (statusType == AIInvisibleStatusType)) statusType = AIAwayStatusType;
940 statusTypeCount[statusType]++;
944 for (i = 0 ; i < STATUS_TYPES_COUNT ; i++) {
945 if (statusTypeCount[i] > highestCount) {
946 activeStatusType = i;
947 highestCount = statusTypeCount[i];
951 return activeStatusType;
955 * @brief All active status states
957 * A status state is active if any enabled account is currently in that state.
959 * The return value of this method is cached.
961 * @result An <tt>NSSet</tt> of <tt>AIStatus</tt> objects
963 - (NSSet *)allActiveStatusStates
965 if (!_allActiveStatusStates) {
966 _allActiveStatusStates = [[NSMutableSet alloc] init];
967 NSEnumerator *enumerator = [[[adium accountController] accounts] objectEnumerator];
970 while ((account = [enumerator nextObject])) {
971 if ([account enabled]) {
972 [_allActiveStatusStates addObject:[account statusState]];
977 return _allActiveStatusStates;
981 * @brief Return the set of all unavailable statuses in use by online or connection accounts
983 * @param activeUnvailableStatusType Pointer to an AIStatusType; returns by reference the most popular unavailable type
984 * @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
985 * @param allOnlineAccountsAreUnvailable Pointer to a BOOL; returns by reference YES is all online accounts are unavailable, NO if one or more is available
987 - (NSSet *)activeUnavailableStatusesAndType:(AIStatusType *)activeUnvailableStatusType withName:(NSString **)activeUnvailableStatusName allOnlineAccountsAreUnvailable:(BOOL *)allOnlineAccountsAreUnvailable
989 NSEnumerator *enumerator = [[[adium accountController] accounts] objectEnumerator];
991 NSMutableSet *activeUnvailableStatuses = [NSMutableSet set];
992 BOOL foundStatusName = NO;
993 int statusTypeCount[STATUS_TYPES_COUNT];
995 statusTypeCount[AIAwayStatusType] = 0;
996 statusTypeCount[AIInvisibleStatusType] = 0;
998 //Assume all accounts are unavailable until proven otherwise
999 if (allOnlineAccountsAreUnvailable != NULL) {
1000 *allOnlineAccountsAreUnvailable = YES;
1003 while ((account = [enumerator nextObject])) {
1004 if ([account online] || [account integerStatusObjectForKey:@"Connecting"]) {
1005 AIStatus *statusState = [account statusState];
1006 AIStatusType statusType = [statusState statusType];
1008 if ((statusType == AIAwayStatusType) || (statusType == AIInvisibleStatusType)) {
1009 NSString *statusName = [statusState statusName];
1011 [activeUnvailableStatuses addObject:statusState];
1013 statusTypeCount[statusType]++;
1015 if (foundStatusName) {
1016 //Once we find a status name, we only want to return it if all our status names are the same.
1017 if ((activeUnvailableStatusName != NULL) &&
1018 (*activeUnvailableStatusName != nil) &&
1019 ![*activeUnvailableStatusName isEqualToString:statusName]) {
1020 *activeUnvailableStatusName = nil;
1023 //We haven't found a status name yet, so store this one as the active status name
1024 if (activeUnvailableStatusName != NULL) {
1025 *activeUnvailableStatusName = [statusState statusName];
1027 foundStatusName = YES;
1030 //An online account isn't unavailable
1031 if (allOnlineAccountsAreUnvailable != NULL) {
1032 *allOnlineAccountsAreUnvailable = NO;
1038 if (activeUnvailableStatusType != NULL) {
1039 if (statusTypeCount[AIAwayStatusType] > statusTypeCount[AIInvisibleStatusType]) {
1040 *activeUnvailableStatusType = AIAwayStatusType;
1042 *activeUnvailableStatusType = AIInvisibleStatusType;
1046 return activeUnvailableStatuses;
1050 * @brief Find the status state with the requested uniqueStatusID
1052 - (AIStatus *)statusStateWithUniqueStatusID:(NSNumber *)uniqueStatusID
1054 AIStatus *statusState = nil;
1056 if (uniqueStatusID) {
1057 NSEnumerator *enumerator = [[self flatStatusSet] objectEnumerator];
1059 while ((statusState = [enumerator nextObject])) {
1060 if ([[statusState uniqueStatusID] compare:uniqueStatusID] == NSOrderedSame)
1068 //State Editing --------------------------------------------------------------------------------------------------------
1069 #pragma mark State Editing
1071 * @brief Add a state
1073 * Add a new state to Adium's state array.
1074 * @param state AIState to add
1076 - (void)addStatusState:(AIStatus *)statusState
1078 AIStatusMutabilityType mutabilityType = [statusState mutabilityType];
1080 if ((mutabilityType == AILockedStatusState) ||
1081 (mutabilityType == AISecondaryLockedStatusState)) {
1082 //If we are adding a locked status, add it to the built-in statuses
1083 [(NSMutableArray *)[self builtInStateArray] addObject:statusState];
1085 [self notifyOfChangedStatusArray];
1088 //Otherwise, add it to the user-created statuses
1089 [[self rootStateGroup] addStatusItem:statusState atIndex:-1];
1094 * @brief Remove a state
1096 * Remove a new state from Adium's state array.
1097 * @param state AIStatus to remove
1099 - (void)removeStatusState:(AIStatus *)statusState
1101 NSLog(@"shouldn't be calling this.");
1102 // [stateArray removeObject:statusState];
1103 [self savedStatusesChanged];
1106 - (void)notifyOfChangedStatusArray
1108 //Clear the sorted menu items array since our state array changed.
1109 [_sortedFullStateArray release]; _sortedFullStateArray = nil;
1110 [_flatStatusSet release]; _flatStatusSet = nil;
1112 if (!statusMenuRebuildDelays) {
1113 [[adium notificationCenter] postNotificationName:AIStatusStateArrayChangedNotification object:nil];
1118 * @brief Save changes to the state array and notify observers
1120 * Saves any outstanding changes to the state array. There should be no need to call this manually, since all the
1121 * state array modifying methods in this class call it automatically after making changes.
1123 * After the state array is saved, observers are notified that is has changed. Call after making any changes to the
1124 * state array from within the controller.
1126 - (void)savedStatusesChanged
1128 //XXX change this back sometime before 1.0 release
1129 // [[adium preferenceController] setPreference:[NSKeyedArchiver archivedDataWithRootObject:[self rootStateGroup]]
1130 [[adium preferenceController] setPreference:[NSKeyedArchiver archivedDataWithRootObject:[[self rootStateGroup] containedStatusItems]]
1131 forKey:KEY_SAVED_STATUS
1132 group:PREF_GROUP_SAVED_STATUS];
1133 [self notifyOfChangedStatusArray];
1136 - (void)statusStateDidSetUniqueStatusID
1138 //XXX change this back sometime before 1.0 release
1139 // [[adium preferenceController] setPreference:[NSKeyedArchiver archivedDataWithRootObject:[self rootStateGroup]]
1140 [[adium preferenceController] setPreference:[NSKeyedArchiver archivedDataWithRootObject:[[self rootStateGroup] containedStatusItems]]
1141 forKey:KEY_SAVED_STATUS
1142 group:PREF_GROUP_SAVED_STATUS];
1146 * @brief Called when a state could potentially need to removed from the temporary (non-saved) list
1148 * If originalState is in the temporary status array, and it is being used on one or zero accounts, it
1149 * is removed from the temporary status array. This method should be used when one or more accounts have stopped
1150 * using a single status state to determine if that status state is both non-saved and unused.
1152 * Note that while it would seem logical to post AIStatusStateArrayChangedNotification when this method would
1153 * return YES, we don't want to force observers of the notification to update immediately since there may be further
1154 * processing. We therefore let the calling method take action if it chooses to.
1156 * @result YES if the state was removed
1158 - (BOOL)removeIfNecessaryTemporaryStatusState:(AIStatus *)originalState
1160 BOOL didRemove = NO;
1162 /* If the original (old) status state is in our temporary array and is not being used in more than 1 account,
1163 * then we should remove it.
1165 if ([temporaryStateArray containsObject:originalState]) {
1166 NSEnumerator *enumerator;
1170 enumerator = [[[adium accountController] accounts] objectEnumerator];
1171 while ((account = [enumerator nextObject])) {
1172 if ([account actualStatusState] == originalState) {
1173 if (++count > 1) break;
1178 [temporaryStateArray removeObject:originalState];
1186 - (void)saveStatusAsLastUsed:(AIStatus *)statusState
1188 NSMutableDictionary *lastStatusStates;
1190 lastStatusStates = [[[adium preferenceController] preferenceForKey:@"LastStatusStates"
1191 group:PREF_GROUP_STATUS_PREFERENCES] mutableCopy];
1192 if (!lastStatusStates) lastStatusStates = [NSMutableDictionary dictionary];
1194 [lastStatusStates setObject:[NSKeyedArchiver archivedDataWithRootObject:statusState]
1195 forKey:[NSNumber numberWithInt:[statusState statusType]]];
1197 [[adium preferenceController] setPreference:lastStatusStates
1198 forKey:@"LastStatusStates"
1199 group:PREF_GROUP_STATUS_PREFERENCES];
1201 //Status state menu support ---------------------------------------------------------------------------------------------------
1202 #pragma mark Status state menu support
1204 * @brief Apply a custom state
1206 * Invoked when the custom state window is closed by the user clicking OK. In response this method sets the custom
1207 * state as the active state.
1209 - (void)customStatusState:(AIStatus *)originalState changedTo:(AIStatus *)newState forAccount:(AIAccount *)account
1211 BOOL shouldRebuild = NO;
1213 if ([newState mutabilityType] != AITemporaryEditableStatusState) {
1214 [[adium statusController] addStatusState:newState];
1218 shouldRebuild = [self removeIfNecessaryTemporaryStatusState:originalState];
1220 //Now set the newState for the account
1221 [account setStatusState:newState];
1223 //Enable the account if it isn't currently enabled
1224 if (![account enabled]) {
1225 [account setEnabled:YES];
1228 //Add to our temporary status array if it's not in our state array
1229 if (shouldRebuild || (![[self flatStatusSet] containsObject:newState])) {
1230 [temporaryStateArray addObject:newState];
1232 [self notifyOfChangedStatusArray];
1236 //Set the state for all accounts. This will clear out the temporaryStatusArray as necessary and update its contents.
1237 [self setActiveStatusState:newState];
1240 [self saveStatusAsLastUsed:newState];
1244 #pragma mark Upgrade code
1246 * @brief Temporary upgrade code for 0.7x -> 0.8
1248 * Versions 0.7x and prior stored their away messages in a different format. This code allows a seamless
1249 * transition from 0.7x to 0.8. We can easily recognize the old format because the away messages are of
1250 * type "Away" instead of type "State", which is used for all 0.8 and later saved states.
1251 * Since we are changing the array as we scan it, an enumerator will not work here.
1253 #define OLD_KEY_SAVED_AWAYS @"Saved Away Messages"
1254 #define OLD_GROUP_AWAY_MESSAGES @"Away Messages"
1255 #define OLD_STATE_SAVED_AWAY @"Away"
1256 #define OLD_STATE_AWAY @"Message"
1257 #define OLD_STATE_AUTO_REPLY @"Autoresponse"
1258 #define OLD_STATE_TITLE @"Title"
1259 - (void)_upgradeSavedAwaysToSavedStates
1261 NSArray *savedAways = [[adium preferenceController] preferenceForKey:OLD_KEY_SAVED_AWAYS
1262 group:OLD_GROUP_AWAY_MESSAGES];
1265 NSEnumerator *enumerator = [savedAways objectEnumerator];
1266 NSDictionary *state;
1268 AILog(@"*** Upgrading Adium 0.7x saved aways: %@", savedAways);
1270 [self setDelayStatusMenuRebuilding:YES];
1272 //Update all the away messages to states.
1273 while ((state = [enumerator nextObject])) {
1274 if ([[state objectForKey:@"Type"] isEqualToString:OLD_STATE_SAVED_AWAY]) {
1275 AIStatus *statusState;
1277 //Extract the away message information from this old record
1278 NSData *statusMessageData = [state objectForKey:OLD_STATE_AWAY];
1279 NSData *autoReplyMessageData = [state objectForKey:OLD_STATE_AUTO_REPLY];
1280 NSString *title = [state objectForKey:OLD_STATE_TITLE];
1282 //Create an AIStatus from this information
1283 statusState = [AIStatus status];
1285 //General category: It's an away type
1286 [statusState setStatusType:AIAwayStatusType];
1288 //Specific state: It's the generic away. Funny how that works out.
1289 [statusState setStatusName:STATUS_NAME_AWAY];
1291 //Set the status message (which is just the away message).
1292 [statusState setStatusMessage:[NSAttributedString stringWithData:statusMessageData]];
1294 //It has an auto reply.
1295 [statusState setHasAutoReply:YES];
1297 if (autoReplyMessageData) {
1298 //Use the custom auto reply if it was set.
1299 [statusState setAutoReply:[NSAttributedString stringWithData:autoReplyMessageData]];
1301 //If no autoReplyMesssage, use the status message.
1302 [statusState setAutoReplyIsStatusMessage:YES];
1305 if (title) [statusState setTitle:title];
1307 //Add the updated state to our state array.
1308 [self addStatusState:statusState];
1312 AILog(@"*** Finished upgrading old saved statuses");
1314 //Save these changes and delete the old aways so we don't need to do this again.
1315 [self setDelayStatusMenuRebuilding:NO];
1317 [[adium preferenceController] setPreference:nil
1318 forKey:OLD_KEY_SAVED_AWAYS
1319 group:OLD_GROUP_AWAY_MESSAGES];