* Fixed the objectSpecifier of `AIStatusItem`s to be based on the unique ID rather...
[adiumx.git] / Source / AIStatusController.m
blob277e856540cda02f1bdcead5a0061c97653baf88
1 /* 
2  * Adium is the legal property of its developers, whose names are listed in the copyright file included
3  * with this source distribution.
4  * 
5  * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
6  * General Public License as published by the Free Software Foundation; either version 2 of the License,
7  * or (at your option) any later version.
8  * 
9  * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
10  * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
11  * Public License for more details.
12  * 
13  * You should have received a copy of the GNU General Public License along with this program; if not,
14  * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
15  */
17 #import "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>
38 //State menu
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
50                                                   withTarget:(id)target
51                                                          fromSet:(NSSet *)sourceArray
52                                                          toArray:(NSMutableArray *)menuItems
53                                   alreadyAddedTitles:(NSMutableSet *)alreadyAddedTitles;
54 - (void)buildBuiltInStatusTypes;
55 - (void)notifyOfChangedStatusArray;
56 @end
58 /*!
59  * @class AIStatusController
60  * @brief Core status & state methods
61  *
62  * This class provides a foundation for Adium's status and status state systems.
63  */
64 @implementation AIStatusController
66 static  NSMutableSet                    *temporaryStateArray = nil;
68 /*!
69  * Init the status controller
70  */
71 - (id)init
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];
82                 
83                 accountsToConnect = [[NSMutableSet alloc] init];
84                 
85                 idleManager = [[AdiumIdleManager alloc] init];
86         }
87         
88         return self;
91 /*!
92  * @brief Finish initing the status controller
93  *
94  * Set our initial status state, and restore our array of accounts to connect when a global state is selected.
95  */
96 - (void)controllerDidLoad
98         NSEnumerator                    *enumerator;
99         AIAccount                               *account;
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;
114                 if (lastStatusData)
115                         lastStatus = [NSKeyedUnarchiver unarchiveObjectWithData:lastStatusData];
117                 if (lastStatus && [lastStatus isKindOfClass:[AIStatus class]]) {
118                         AIStatus        *existingStatus;
119                         
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
122                          * will be nil.
123                          */
124                         existingStatus = [self statusStateWithUniqueStatusID:[lastStatus uniqueStatusID]];
125                         
126                         if (existingStatus) {
127                                 lastStatus = existingStatus;
128                         } else {
129                                 //Add to our temporary status array
130                                 [temporaryStateArray addObject:lastStatus];
131                                 
132                                 /* We could clear out _flatStatusSet for the next iteration, but we _know_ what changed,
133                                  * so modify it directly for efficiency.
134                                  */
135                                 [_flatStatusSet addObject:lastStatus];
137                                 needToRebuildMenus = YES;
138                         }
139                         if (!prevStatus)
140                                 prevStatus = lastStatus;
141                         else if (prevStatus != lastStatus)
142                                 allStatusesInSameState = NO;
143                         [account setStatusStateAndRemainOffline:lastStatus];
144                 }
145         }
147         if (needToRebuildMenus) {
148                 [self notifyOfChangedStatusArray];
149         }
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.
158  */
159 - (void)controllerWillClose
161         NSEnumerator    *enumerator;
162         AIAccount               *account;
164         enumerator = [[[adium accountController] accounts] objectEnumerator];
165         while ((account = [enumerator nextObject])) {
166                 /* Store the current status state for use on next launch.
167                  *
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.
172                  */
173                 AIStatus        *currentStatus = [account statusObjectForKey:@"StatusState"];
174                 [account setPreference:((currentStatus && (currentStatus != offlineStatusState)) ?
175                                                                 [NSKeyedArchiver archivedDataWithRootObject:currentStatus] :
176                                                                 nil)
177                                                 forKey:@"LastStatus"
178                                                  group:GROUP_ACCOUNT_STATUS];
179         }
180         
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];
193  * @brief Deallocate
194  */
195 - (void)dealloc
197         [_rootStateGroup release]; _rootStateGroup = nil;
198         [_sortedFullStateArray release]; _sortedFullStateArray = nil;
199         [super dealloc];
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
214  */
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];
226         }
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,
233                 nil];
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
246  */
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];
253         AIStatusType    type;
255         for (type = AIAvailableStatusType ; type < STATUS_TYPES_COUNT ; type++) {
256                 NSArray         *menuItemArray;
258                 menuItemArray = [self _menuItemsForStatusesOfType:type
259                                                                    forServiceCodeUniqueID:serviceCodeUniqueID
260                                                                                            withTarget:target];
262                 //Add a separator between each type after available
263                 if ((type > AIAvailableStatusType) && [menuItemArray count]) {
264                         [menu addItem:[NSMenuItem separatorItem]];
265                 }
267                 //Add the items for this type
268                 enumerator = [menuItemArray objectEnumerator];
269                 while ((menuItem = [enumerator nextObject])) {
270                         [menu addItem:menuItem];
271                 }
272         }
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.
285  */
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
293                                                         withTarget:target
294                                                            fromSet:builtInStatusTypes[type]
295                                                            toArray:menuItems
296                                         alreadyAddedTitles:alreadyAddedTitles];
298         //Now, add items for this service, or from all available services, as appropriate
299         if (inServiceCodeUniqueID) {
300                 NSSet   *statusDicts;
302                 //Obtain the status dicts for this type and service code unique ID
303                 if ((statusDicts = [statusDictsByServiceCodeUniqueID[type] objectForKey:inServiceCodeUniqueID])) {
304                         //And add them
305                         [self _addMenuItemsForStatusOfType:type
306                                                                         withTarget:target
307                                                                            fromSet:statusDicts
308                                                                            toArray:menuItems
309                                                         alreadyAddedTitles:alreadyAddedTitles];
310                 }
312         } else {
313                 NSEnumerator    *enumerator;
314                 AIService               *service;
316                 enumerator = [[[adium accountController] activeServicesIncludingCompatibleServices:NO] objectEnumerator];
317                 while ((service = [enumerator nextObject])) {
318                         NSSet   *statusDicts;
319                         
320                         //Obtain the status dicts for this type and service code unique ID
321                         if ((statusDicts = [statusDictsByServiceCodeUniqueID[type] objectForKey:[service serviceCodeUniqueID]])) {
322                                 //And add them
323                                 [self _addMenuItemsForStatusOfType:type
324                                                                                 withTarget:target
325                                                                                    fromSet:statusDicts
326                                                                                    toArray:menuItems
327                                                                 alreadyAddedTitles:alreadyAddedTitles];
328                         }
329                         
330                 }
331         }
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.
346  */
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];
360                 /*
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
363                  * exist.
364                  */
365                 if (![alreadyAddedTitles containsObject:title]) {
366                         NSImage         *image;
367                         NSMenuItem      *menuItem;
369                         menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:title
370                                                                                                                                                         target:target
371                                                                                                                                                         action:@selector(selectStatus:)
372                                                                                                                                          keyEquivalent:@""];
374                         image = [AIStatusIcons statusIconForStatusName:[statusDict objectForKey:KEY_STATUS_NAME]
375                                                                                                   statusType:type
376                                                                                                         iconType:AIStatusIconMenu
377                                                                                                    direction:AIIconNormal];
379                         [menuItem setRepresentedObject:statusDict];
380                         [menuItem setImage:image];
381                         [menuItem setEnabled:YES];
382                         [menuItems addObject:menuItem];
383                         [menuItem release];
385                         [alreadyAddedTitles addObject:title];
386                 }
387         }
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,
416                         nil] retain];
417         }
418         
419         return (statusName ? [coreLocalizedStatusDescriptions objectForKey:statusName] : nil);
422 - (NSString *)localizedDescriptionForStatusName:(NSString *)statusName statusType:(AIStatusType)statusType
424         NSString *description = nil;
426         if (statusName &&
427                 !(description = [self localizedDescriptionForCoreStatusName:statusName])) {
428                 NSEnumerator    *enumerator = [statusDictsByServiceCodeUniqueID[statusType] objectEnumerator];
429                 NSSet                   *set;
430                 
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];
437                                 }
438                         }
439                 }               
440         }
441         
442         return 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
452  */
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.
463  */
464 - (NSString *)defaultStatusNameForType:(AIStatusType)statusType
466         //Set the default status name
467         switch (statusType) {
468                 case AIAvailableStatusType:
469                         return STATUS_NAME_AVAILABLE;
470                         break;
471                 case AIAwayStatusType:
472                         return STATUS_NAME_AWAY;
473                         break;
474                 case AIInvisibleStatusType:
475                         return STATUS_NAME_INVISIBLE;
476                         break;
477                 case AIOfflineStatusType:
478                         return STATUS_NAME_OFFLINE;
479                         break;
480         }
482         return nil;
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.
491  */
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]
498                            afterDelay:0];
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.
506  */
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
515  */
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.
526  */
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];
536         }
540  * @brief Account status changed.
542  * Rebuild all our state menus
543  */
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]) {
551                         
552                         [self _resetActiveStatusState];
553                 }
554         }
555         
556     return nil;
561  * @brief Delay activee status menu updates
563  * This should be called to prevent duplicative updates when multiple accounts are changing status simultaneously.
564  */
565 - (void)setDelayActiveStatusUpdates:(BOOL)shouldDelay
567         if (shouldDelay)
568                 activeStatusUpdateDelays++;
569         else
570                 activeStatusUpdateDelays--;
571         
572         if (!activeStatusUpdateDelays) {
573                 [[adium notificationCenter] postNotificationName:AIStatusActiveStateChangedNotification object:nil];
574         }
578  * @brief Delay activee status menu updates
580  * This should be called to prevent duplicative rebuilds when the status menu will change multple times.
581  */
582 - (void)setDelayStatusMenuRebuilding:(BOOL)shouldDelay
584         if (shouldDelay)
585                 statusMenuRebuildDelays++;
586         else
587                 statusMenuRebuildDelays--;
588         
589         if (!statusMenuRebuildDelays) {
590                 [[adium notificationCenter] postNotificationName:AIStatusStateArrayChangedNotification object:nil];     
591         }
595  * @brief Apply a state to multiple accounts
596  */
597 - (void)applyState:(AIStatus *)statusState toAccounts:(NSArray *)accountArray
599         NSEnumerator    *enumerator;
600         AIAccount               *account;
601         AIStatus                *aStatusState;
602         BOOL                    shouldRebuild = NO;
603         BOOL                    isOfflineStatus = ([statusState statusType] == AIOfflineStatusType);
604         [self setDelayActiveStatusUpdates:YES];
605         
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.
608          */
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];
617                 }
618         }
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.
624                  */
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];
632                         } else {
633                                 [account setStatusStateAndRemainOffline:statusState];   
634                         }
635                 }
637         } else {
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];
643                                 
644                         } else {
645                                 [account setStatusStateAndRemainOffline:statusState];                   
646                         }
647                 }
648                 shouldRebuild = YES;
649         }
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];
654         }
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];
661                         shouldRebuild = YES;
662                 }
663         }
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];
669                 shouldRebuild = YES;
670         }
672         if (shouldRebuild) {
673                 [self notifyOfChangedStatusArray];
674         }
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
684  */
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];
696                         
697                         } else if  ([archivedObject isKindOfClass:[NSArray class]]) {
698                                 //Adium 0.8x archived an NSArray
699                                 _rootStateGroup = [[AIStatusGroup statusGroupWithContainedStatusItems:archivedObject] retain];
700                         }
701                 }
703                 if (!_rootStateGroup) _rootStateGroup = [[AIStatusGroup statusGroup] retain];
705                 //Upgrade Adium 0.7x away messages
706                 [self _upgradeSavedAwaysToSavedStates];
707         }
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.
717  */
718 - (NSArray *)builtInStateArray
720         if (!builtInStateArray) {
721                 NSArray                 *savedBuiltInStateArray = [NSArray arrayNamed:BUILT_IN_STATE_ARRAY forClass:[self class]];
722                 NSEnumerator    *enumerator;
723                 NSDictionary    *dict;
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];
736                         }
737                 }
738         }
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.
747  */
748 - (void)buildBuiltInStatusTypes
750         NSDictionary    *statusDict;
751         
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,
757                 nil];
758         [builtInStatusTypes[AIAvailableStatusType] addObject:statusDict];
759         
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,
765                 nil];
766         [builtInStatusTypes[AIAwayStatusType] addObject:statusDict];
770  * @brief Returns the built in available status
771  */
772 - (AIStatus *)availableStatus
774         return [[[self builtInStateArray] filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"statusType == %i",AIAvailableStatusType]] objectAtIndex:0];
777  * @brief Returns the built in away status
778  */
779 - (AIStatus *)awayStatus
781         return [[[self builtInStateArray] filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"statusType == %i",AIAwayStatusType]] objectAtIndex:0];
784  * @brief Returns the built in invisible status
785  */
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.
795  */
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.
817  */
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]];
826                 
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;
836         }
838         return _sortedFullStateArray;
842  * @brief Generate and return an array of AIStatus objects which are all known saved, temporary, and built-in statuses
843  */
844 - (NSSet *)flatStatusSet
846         if (!_flatStatusSet) {
847                 NSMutableSet    *tempArray = [[[self rootStateGroup] flatStatusSet] mutableCopy];
849                 //Add built in states
850                 [tempArray addObjectsFromArray:[self builtInStateArray]];
852                 //Add temporary ones
853                 [tempArray addObjectsFromArray:[temporaryStateArray allObjects]];
855                 _flatStatusSet = tempArray;
856         }
857         
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.
868  */
869 - (AIStatus *)activeStatusState
871         if (!_activeStatusState) {
872                 NSEnumerator            *enumerator = [[[adium accountController] accounts] objectEnumerator];
873                 NSCountedSet            *statusCounts = [NSCountedSet set];
874                 AIAccount                       *account;
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 ?
887                                                                                          accountStatusState :
888                                                                                          [self defaultInitialStatusState])];
889                                 }
890                         }
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;
898                                 }
899                         }
901                         _activeStatusState = (bestStatusState ? [bestStatusState retain]: [offlineStatusState retain]);
903                 } else {
904                         _activeStatusState = [offlineStatusState retain];
905                 }
906         }
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
915  * enum is respected
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
919  */
920 - (AIStatusType)activeStatusTypeTreatingInvisibleAsAway:(BOOL)invisibleIsAway
922         NSEnumerator            *enumerator = [[[adium accountController] accounts] objectEnumerator];
923         AIAccount                       *account;
924         int                                     statusTypeCount[STATUS_TYPES_COUNT];
925         AIStatusType            activeStatusType = AIOfflineStatusType;
926         unsigned                        highestCount = 0;
928         unsigned i;
929         for (i = 0 ; i < STATUS_TYPES_COUNT ; i++) {
930                 statusTypeCount[i] = 0;
931         }
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]++;
941                 }
942         }
944         for (i = 0 ; i < STATUS_TYPES_COUNT ; i++) {
945                 if (statusTypeCount[i] > highestCount) {
946                         activeStatusType = i;
947                         highestCount = statusTypeCount[i];
948                 }
949         }
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
962  */
963 - (NSSet *)allActiveStatusStates
965         if (!_allActiveStatusStates) {
966                 _allActiveStatusStates = [[NSMutableSet alloc] init];
967                 NSEnumerator            *enumerator = [[[adium accountController] accounts] objectEnumerator];
968                 AIAccount                       *account;
970                 while ((account = [enumerator nextObject])) {
971                         if ([account enabled]) {
972                                 [_allActiveStatusStates addObject:[account statusState]];
973                         }
974                 }
975         }
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
986  */
987 - (NSSet *)activeUnavailableStatusesAndType:(AIStatusType *)activeUnvailableStatusType withName:(NSString **)activeUnvailableStatusName allOnlineAccountsAreUnvailable:(BOOL *)allOnlineAccountsAreUnvailable
989         NSEnumerator            *enumerator = [[[adium accountController] accounts] objectEnumerator];
990         AIAccount                       *account;
991         NSMutableSet            *activeUnvailableStatuses = [NSMutableSet set];
992         BOOL                            foundStatusName = NO;
993         int                                     statusTypeCount[STATUS_TYPES_COUNT];
995         statusTypeCount[AIAwayStatusType] = 0;
996         statusTypeCount[AIInvisibleStatusType] = 0;
997         
998         //Assume all accounts are unavailable until proven otherwise
999         if (allOnlineAccountsAreUnvailable != NULL) {
1000                 *allOnlineAccountsAreUnvailable = YES;
1001         }
1002         
1003         while ((account = [enumerator nextObject])) {
1004                 if ([account online] || [account integerStatusObjectForKey:@"Connecting"]) {
1005                         AIStatus        *statusState = [account statusState];
1006                         AIStatusType statusType = [statusState statusType];
1007                         
1008                         if ((statusType == AIAwayStatusType) || (statusType == AIInvisibleStatusType)) {
1009                                 NSString        *statusName = [statusState statusName];
1010                                 
1011                                 [activeUnvailableStatuses addObject:statusState];
1012                                 
1013                                 statusTypeCount[statusType]++;
1014                                 
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;
1021                                         }
1022                                 } else {
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];
1026                                         }
1027                                         foundStatusName = YES;
1028                                 }
1029                         } else {
1030                                 //An online account isn't unavailable
1031                                 if (allOnlineAccountsAreUnvailable != NULL) {
1032                                         *allOnlineAccountsAreUnvailable = NO;
1033                                 }
1034                         }
1035                 }
1036         }
1037         
1038         if (activeUnvailableStatusType != NULL) {
1039                 if (statusTypeCount[AIAwayStatusType] > statusTypeCount[AIInvisibleStatusType]) {
1040                         *activeUnvailableStatusType = AIAwayStatusType;
1041                 } else {
1042                         *activeUnvailableStatusType = AIInvisibleStatusType;            
1043                 }
1044         }
1045         
1046         return activeUnvailableStatuses;
1050  * @brief Find the status state with the requested uniqueStatusID
1051  */
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)
1061                                 break;
1062                 }
1063         }
1065         return statusState;
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
1075  */
1076 - (void)addStatusState:(AIStatus *)statusState
1078         AIStatusMutabilityType mutabilityType = [statusState mutabilityType];
1079         
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];
1087         } else {
1088                 //Otherwise, add it to the user-created statuses
1089                 [[self rootStateGroup] addStatusItem:statusState atIndex:-1];
1090         }
1094  * @brief Remove a state
1096  * Remove a new state from Adium's state array.
1097  * @param state AIStatus to remove
1098  */
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];     
1114         }
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.
1125  */
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
1157  */
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.
1164         */
1165         if ([temporaryStateArray containsObject:originalState]) {
1166                 NSEnumerator    *enumerator;
1167                 AIAccount               *account;
1168                 int                             count = 0;
1169                 
1170                 enumerator = [[[adium accountController] accounts] objectEnumerator];
1171                 while ((account = [enumerator nextObject])) {
1172                         if ([account actualStatusState] == originalState) {
1173                                 if (++count > 1) break;
1174                         }
1175                 }
1177                 if (count <= 1) {
1178                         [temporaryStateArray removeObject:originalState];
1179                         didRemove = YES;
1180                 }
1181         }
1183         return didRemove;
1186 - (void)saveStatusAsLastUsed:(AIStatus *)statusState
1188         NSMutableDictionary *lastStatusStates;
1189         
1190         lastStatusStates = [[[adium preferenceController] preferenceForKey:@"LastStatusStates"
1191                                                                                                                                  group:PREF_GROUP_STATUS_PREFERENCES] mutableCopy];
1192         if (!lastStatusStates) lastStatusStates = [NSMutableDictionary dictionary];
1193         
1194         [lastStatusStates setObject:[NSKeyedArchiver archivedDataWithRootObject:statusState]
1195                                                  forKey:[NSNumber numberWithInt:[statusState statusType]]];
1196         
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.
1208  */
1209 - (void)customStatusState:(AIStatus *)originalState changedTo:(AIStatus *)newState forAccount:(AIAccount *)account
1211         BOOL shouldRebuild = NO;
1212         
1213         if ([newState mutabilityType] != AITemporaryEditableStatusState) {
1214                 [[adium statusController] addStatusState:newState];
1215         }
1217         if (account) {
1218                 shouldRebuild = [self removeIfNecessaryTemporaryStatusState:originalState];
1220                 //Now set the newState for the account
1221                 [account setStatusState:newState];
1222                 
1223                 //Enable the account if it isn't currently enabled
1224                 if (![account enabled]) {
1225                         [account setEnabled:YES];
1226                 }               
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];
1231                         
1232                         [self notifyOfChangedStatusArray];
1233                 }
1234                 
1235         } else {
1236                 //Set the state for all accounts.  This will clear out the temporaryStatusArray as necessary and update its contents.
1237                 [self setActiveStatusState:newState];
1238         }
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.
1252  */
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];
1264         if (savedAways) {
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]];
1300                                 } else {
1301                                         //If no autoReplyMesssage, use the status message.
1302                                         [statusState setAutoReplyIsStatusMessage:YES];
1303                                 }
1305                                 if (title) [statusState setTitle:title];
1307                                 //Add the updated state to our state array.
1308                                 [self addStatusState:statusState];
1309                         }
1310                 }
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];
1320         }
1323 @end