Use initialize, not load, wherever possible.
[adiumx.git] / Source / AIStatusController.m
blobf233cee3a1bc4d771c07c9aaab928cb8bda236e4
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 #define TOP_STATUS_STATE_ID                     @"TopStatusID"
45 @interface AIStatusController (PRIVATE)
46 - (NSArray *)builtInStateArray;
48 - (void)_upgradeSavedAwaysToSavedStates;
50 - (NSArray *)_menuItemsForStatusesOfType:(AIStatusType)type forServiceCodeUniqueID:(NSString *)inServiceCodeUniqueID withTarget:(id)target;
51 - (void)_addMenuItemsForStatusOfType:(AIStatusType)type
52                                                   withTarget:(id)target
53                                                          fromSet:(NSSet *)sourceArray
54                                                          toArray:(NSMutableArray *)menuItems
55                                   alreadyAddedTitles:(NSMutableSet *)alreadyAddedTitles;
56 - (void)buildBuiltInStatusTypes;
57 - (void)notifyOfChangedStatusArray;
58 @end
60 /*!
61  * @class AIStatusController
62  * @brief Core status & state methods
63  *
64  * This class provides a foundation for Adium's status and status state systems.
65  */
66 @implementation AIStatusController
68 static  NSMutableSet                    *temporaryStateArray = nil;
70 /*!
71  * Init the status controller
72  */
73 - (id)init
75         if ((self = [super init])) {
76                 stateMenuItemArraysDict = [[NSMutableDictionary alloc] init];
77                 stateMenuPluginsArray = [[NSMutableArray alloc] init];
78                 stateMenuItemsNeedingUpdating = [[NSMutableSet alloc] init];
79                 activeStatusUpdateDelays = 0;
80                 _sortedFullStateArray = nil;
81                 _activeStatusState = nil;
82                 _allActiveStatusStates = nil;
83                 temporaryStateArray = [[NSMutableSet alloc] init];
84                 
85                 accountsToConnect = [[NSMutableSet alloc] init];
86                 
87                 idleManager = [[AdiumIdleManager alloc] init];
88         }
89         
90         return self;
93 /*!
94  * @brief Finish initing the status controller
95  *
96  * Set our initial status state, and restore our array of accounts to connect when a global state is selected.
97  */
98 - (void)controllerDidLoad
100         NSEnumerator                    *enumerator;
101         AIAccount                               *account;
103         [[adium contactController] registerListObjectObserver:self];
105         [self buildBuiltInStatusTypes];
107         //Put each account into the status it was in last time we quit.
108         BOOL            needToRebuildMenus = NO;
109         BOOL            allStatusesInSameState = YES;
110         AIStatus *prevStatus = nil;
111         enumerator = [[[adium accountController] accounts] objectEnumerator];
112         while ((account = [enumerator nextObject])) {
113                 NSData          *lastStatusData = [account preferenceForKey:@"LastStatus"
114                                                                                                                   group:GROUP_ACCOUNT_STATUS];
115                 AIStatus        *lastStatus = nil;
116                 if (lastStatusData)
117                         lastStatus = [NSKeyedUnarchiver unarchiveObjectWithData:lastStatusData];
119                 if (lastStatus && [lastStatus isKindOfClass:[AIStatus class]]) {
120                         AIStatus        *existingStatus;
121                         
122                         /* We want to use a loaded status instance if one exists.  This will be the case if the account
123                          * was last in a built-in or user defined and saved state.  If the last state was unsaved, existingStatus
124                          * will be nil.
125                          */
126                         existingStatus = [self statusStateWithUniqueStatusID:[lastStatus uniqueStatusID]];
127                         
128                         if (existingStatus) {
129                                 lastStatus = existingStatus;
130                         } else {
131                                 //Add to our temporary status array
132                                 [temporaryStateArray addObject:lastStatus];
133                                 
134                                 /* We could clear out _flatStatusSet for the next iteration, but we _know_ what changed,
135                                  * so modify it directly for efficiency.
136                                  */
137                                 [_flatStatusSet addObject:lastStatus];
139                                 needToRebuildMenus = YES;
140                         }
141                         if (!prevStatus)
142                                 prevStatus = lastStatus;
143                         else if (prevStatus != lastStatus)
144                                 allStatusesInSameState = NO;
145                         [account setStatusStateAndRemainOffline:lastStatus];
146                 }
147         }
149         if (needToRebuildMenus) {
150                 [self notifyOfChangedStatusArray];
151         }
155  * @brief Begin closing the status controller
157  * Save the online accounts; they will be the accounts connected by a global status change
159  * Also save the current status state of each account so it can be restored on next launch.
160  */
161 - (void)controllerWillClose
163         NSEnumerator    *enumerator;
164         AIAccount               *account;
166         enumerator = [[[adium accountController] accounts] objectEnumerator];
167         while ((account = [enumerator nextObject])) {
168                 /* Store the current status state for use on next launch.
169                  *
170                  * We use the statusObjectForKey:@"StatusState" accessor rather than [account statusState]
171                  * because we don't want anything besides the account's actual status state.  That is, we don't
172                  * want the default available state if the account doesn't have a state yet, and we want the
173                  * real last-state-which-was-set (not the offline one) if the account is offline.
174                  */
175                 AIStatus        *currentStatus = [account statusObjectForKey:@"StatusState"];
176                 [account setPreference:((currentStatus && (currentStatus != offlineStatusState)) ?
177                                                                 [NSKeyedArchiver archivedDataWithRootObject:currentStatus] :
178                                                                 nil)
179                                                 forKey:@"LastStatus"
180                                                  group:GROUP_ACCOUNT_STATUS];
181         }
182         
183         //XXX change this back sometime before 1.0 release
184 //      [[adium preferenceController] setPreference:[NSKeyedArchiver archivedDataWithRootObject:[self rootStateGroup]]
185         [[adium preferenceController] setPreference:[NSKeyedArchiver archivedDataWithRootObject:[[self rootStateGroup] containedStatusItems]]
186                                                                                  forKey:KEY_SAVED_STATUS
187                                                                                   group:PREF_GROUP_SAVED_STATUS];
189         [[adium notificationCenter] removeObserver:self];
190         [[adium preferenceController] unregisterPreferenceObserver:self];
191         [[adium contactController] unregisterListObjectObserver:self];
195  * @brief Deallocate
196  */
197 - (void)dealloc
199         [_rootStateGroup release]; _rootStateGroup = nil;
200         [_sortedFullStateArray release]; _sortedFullStateArray = nil;
201         [super dealloc];
204 #pragma mark Status registration
206  * @brief Register a status for a service
208  * Implementation note: Each AIStatusType has its own NSMutableDictionary, statusDictsByServiceCodeUniqueID.
209  * statusDictsByServiceCodeUniqueID is keyed by serviceCodeUniqueID; each object is an NSMutableSet of NSDictionaries.
210  * Each of these dictionaries has KEY_STATUS_NAME, KEY_STATUS_DESCRIPTION, and KEY_STATUS_TYPE.
212  * @param statusName A name which will be passed back to accounts of this service.  Internal use only.  Use the AIStatusController.h #defines where appropriate.
213  * @param description A human-readable localized description which will be shown to the user.  Use the AIStatusController.h #defines where appropriate.
214  * @param type An AIStatusType, the general type of this status.
215  * @param service The AIService for which to register the status
216  */
217 - (void)registerStatus:(NSString *)statusName withDescription:(NSString *)description ofType:(AIStatusType)type forService:(AIService *)service
219         NSMutableSet    *statusDicts;
220         NSString                *serviceCodeUniqueID = [service serviceCodeUniqueID];
222         //Create the set if necessary
223         if (!statusDictsByServiceCodeUniqueID[type]) statusDictsByServiceCodeUniqueID[type] = [[NSMutableDictionary alloc] init];
224         if (!(statusDicts = [statusDictsByServiceCodeUniqueID[type] objectForKey:serviceCodeUniqueID])) {
225                 statusDicts = [NSMutableSet set];
226                 [statusDictsByServiceCodeUniqueID[type] setObject:statusDicts
227                                                                                                    forKey:serviceCodeUniqueID];
228         }
230         //Create a dictionary for this status entry
231         NSDictionary *statusDict = [NSDictionary dictionaryWithObjectsAndKeys:
232                 statusName, KEY_STATUS_NAME,
233                 description, KEY_STATUS_DESCRIPTION,
234                 [NSNumber numberWithInt:type], KEY_STATUS_TYPE,
235                 nil];
237         [statusDicts addObject:statusDict];
240 #pragma mark Status menus
242  * @brief Generate and return a menu of status types (Away, Be right back, etc.)
244  * @param service The service for which to return a specific list of types, or nil to return all available types
245  * @param target The target for the menu items, which will have an action of @selector(selectStatus:)
247  * @result The menu of statuses, separated by available and away status types
248  */
249 - (NSMenu *)menuOfStatusesForService:(AIService *)service withTarget:(id)target
251         NSMenu                  *menu = [[NSMenu allocWithZone:[NSMenu menuZone]] init];
252         NSEnumerator    *enumerator;
253         NSMenuItem              *menuItem;
254         NSString                *serviceCodeUniqueID = [service serviceCodeUniqueID];
255         AIStatusType    type;
257         for (type = AIAvailableStatusType ; type < STATUS_TYPES_COUNT ; type++) {
258                 NSArray         *menuItemArray;
260                 menuItemArray = [self _menuItemsForStatusesOfType:type
261                                                                    forServiceCodeUniqueID:serviceCodeUniqueID
262                                                                                            withTarget:target];
264                 //Add a separator between each type after available
265                 if ((type > AIAvailableStatusType) && [menuItemArray count]) {
266                         [menu addItem:[NSMenuItem separatorItem]];
267                 }
269                 //Add the items for this type
270                 enumerator = [menuItemArray objectEnumerator];
271                 while ((menuItem = [enumerator nextObject])) {
272                         [menu addItem:menuItem];
273                 }
274         }
276         return [menu autorelease];
280  * @brief Return an array of menu items for an AIStatusType and service
282  * @pram type The AIStatusType for which to return statuses
283  * @param inServiceCodeUniqueID The service for which to return active statuses.  If nil, return all statuses for online services.
284  * @param target The target for the menu items
286  * @result An <tt>NSArray</tt> of <tt>NSMenuItem</tt> objects.
287  */
288 - (NSArray *)_menuItemsForStatusesOfType:(AIStatusType)type forServiceCodeUniqueID:(NSString *)inServiceCodeUniqueID withTarget:(id)target
290         NSMutableArray  *menuItems = [[NSMutableArray alloc] init];
291         NSMutableSet    *alreadyAddedTitles = [NSMutableSet set];
293         //First, add our built-in items (so they will be at the top of the array and service-specific 'copies' won't replace them)
294         [self _addMenuItemsForStatusOfType:type
295                                                         withTarget:target
296                                                            fromSet:builtInStatusTypes[type]
297                                                            toArray:menuItems
298                                         alreadyAddedTitles:alreadyAddedTitles];
300         //Now, add items for this service, or from all available services, as appropriate
301         if (inServiceCodeUniqueID) {
302                 NSSet   *statusDicts;
304                 //Obtain the status dicts for this type and service code unique ID
305                 if ((statusDicts = [statusDictsByServiceCodeUniqueID[type] objectForKey:inServiceCodeUniqueID])) {
306                         //And add them
307                         [self _addMenuItemsForStatusOfType:type
308                                                                         withTarget:target
309                                                                            fromSet:statusDicts
310                                                                            toArray:menuItems
311                                                         alreadyAddedTitles:alreadyAddedTitles];
312                 }
314         } else {
315                 NSEnumerator    *enumerator;
316                 AIService               *service;
318                 enumerator = [[[adium accountController] activeServicesIncludingCompatibleServices:NO] objectEnumerator];
319                 while ((service = [enumerator nextObject])) {
320                         NSSet   *statusDicts;
321                         
322                         //Obtain the status dicts for this type and service code unique ID
323                         if ((statusDicts = [statusDictsByServiceCodeUniqueID[type] objectForKey:[service serviceCodeUniqueID]])) {
324                                 //And add them
325                                 [self _addMenuItemsForStatusOfType:type
326                                                                                 withTarget:target
327                                                                                    fromSet:statusDicts
328                                                                                    toArray:menuItems
329                                                                 alreadyAddedTitles:alreadyAddedTitles];
330                         }
331                         
332                 }
333         }
335         [menuItems sortUsingSelector:@selector(titleCompare:)];
337         return [menuItems autorelease];
341  * @brief Add menu items for a particular type of status
343  * @param type The AIStatusType, used for determining the icon of the menu items
344  * @param target The target of the created menu items
345  * @param statusDicts An NSSet of NSDictionary objects, which should each represent a status of the passed type
346  * @param menuItems The NSMutableArray to which to add the menuItems
347  * @param alreadyAddedTitles NSMutableSet of NSString titles which have already been added and should not be duplicated. Will be updated as items are added.
348  */
349 - (void)_addMenuItemsForStatusOfType:(AIStatusType)type
350                                                   withTarget:(id)target
351                                                          fromSet:(NSSet *)statusDicts
352                                                          toArray:(NSMutableArray *)menuItems
353                                   alreadyAddedTitles:(NSMutableSet *)alreadyAddedTitles
355         NSEnumerator    *statusDictEnumerator = [statusDicts objectEnumerator];
356         NSDictionary    *statusDict;
358         //Enumerate the status dicts
359         while ((statusDict = [statusDictEnumerator nextObject])) {
360                 NSString        *title = [statusDict objectForKey:KEY_STATUS_DESCRIPTION];
362                 /*
363                  * Only add if it has not already been added by another service.... Services need to use unique titles if they have
364                  * unique state names, but are welcome to share common name/description combinations, which is why the #defines
365                  * exist.
366                  */
367                 if (![alreadyAddedTitles containsObject:title]) {
368                         NSImage         *image;
369                         NSMenuItem      *menuItem;
371                         menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:title
372                                                                                                                                                         target:target
373                                                                                                                                                         action:@selector(selectStatus:)
374                                                                                                                                          keyEquivalent:@""];
376                         image = [AIStatusIcons statusIconForStatusName:[statusDict objectForKey:KEY_STATUS_NAME]
377                                                                                                   statusType:type
378                                                                                                         iconType:AIStatusIconMenu
379                                                                                                    direction:AIIconNormal];
381                         [menuItem setRepresentedObject:statusDict];
382                         [menuItem setImage:image];
383                         [menuItem setEnabled:YES];
384                         [menuItems addObject:menuItem];
385                         [menuItem release];
387                         [alreadyAddedTitles addObject:title];
388                 }
389         }
392 #pragma mark Status State Descriptions
393 - (NSString *)localizedDescriptionForCoreStatusName:(NSString *)statusName
395         static NSDictionary     *coreLocalizedStatusDescriptions = nil;
396         if(!coreLocalizedStatusDescriptions){
397                 coreLocalizedStatusDescriptions = [[NSDictionary dictionaryWithObjectsAndKeys:
398                         AILocalizedStringFromTable(@"Available", @"Statuses", "Name of a status"), STATUS_NAME_AVAILABLE,
399                         AILocalizedStringFromTable(@"Free for chat", @"Statuses", "Name of a status"), STATUS_NAME_FREE_FOR_CHAT,
400                         AILocalizedStringFromTable(@"Available for friends only", @"Statuses", "Name of a status"), STATUS_NAME_AVAILABLE_FRIENDS_ONLY,
401                         AILocalizedStringFromTable(@"Away", @"Statuses", "Name of a status"), STATUS_NAME_AWAY,
402                         AILocalizedStringFromTable(@"Extended away", @"Statuses", "Name of a status"), STATUS_NAME_EXTENDED_AWAY,
403                         AILocalizedStringFromTable(@"Away for friends only", @"Statuses", "Name of a status"), STATUS_NAME_AWAY_FRIENDS_ONLY,
404                         AILocalizedStringFromTable(@"Do not disturb", @"Statuses", "Name of a status"), STATUS_NAME_DND,
405                         AILocalizedStringFromTable(@"Not available", @"Statuses", "Name of a status"), STATUS_NAME_NOT_AVAILABLE,
406                         AILocalizedStringFromTable(@"Occupied", @"Statuses", "Name of a status"), STATUS_NAME_OCCUPIED,
407                         AILocalizedStringFromTable(@"Be right back", @"Statuses", "Name of a status"), STATUS_NAME_BRB,
408                         AILocalizedStringFromTable(@"Busy", @"Statuses", "Name of a status"), STATUS_NAME_BUSY,
409                         AILocalizedStringFromTable(@"On the phone", @"Statuses", "Name of a status"), STATUS_NAME_PHONE,
410                         AILocalizedStringFromTable(@"Out to lunch", @"Statuses", "Name of a status"), STATUS_NAME_LUNCH,
411                         AILocalizedStringFromTable(@"Not at home", @"Statuses", "Name of a status"), STATUS_NAME_NOT_AT_HOME,
412                         AILocalizedStringFromTable(@"Not at my desk", @"Statuses", "Name of a status"), STATUS_NAME_NOT_AT_DESK,
413                         AILocalizedStringFromTable(@"Not in the office", @"Statuses", "Name of a status"), STATUS_NAME_NOT_IN_OFFICE,
414                         AILocalizedStringFromTable(@"On vacation", @"Statuses", "Name of a status"), STATUS_NAME_VACATION,
415                         AILocalizedStringFromTable(@"Stepped out", @"Statuses", "Name of a status"), STATUS_NAME_STEPPED_OUT,
416                         AILocalizedStringFromTable(@"Invisible", @"Statuses", "Name of a status"), STATUS_NAME_INVISIBLE,
417                         AILocalizedStringFromTable(@"Offline", @"Statuses", "Name of a status"), STATUS_NAME_OFFLINE,
418                         nil] retain];
419         }
420         
421         return (statusName ? [coreLocalizedStatusDescriptions objectForKey:statusName] : nil);
424 - (NSString *)localizedDescriptionForStatusName:(NSString *)statusName statusType:(AIStatusType)statusType
426         NSString *description = nil;
428         if (statusName &&
429                 !(description = [self localizedDescriptionForCoreStatusName:statusName])) {
430                 NSEnumerator    *enumerator = [statusDictsByServiceCodeUniqueID[statusType] objectEnumerator];
431                 NSSet                   *set;
432                 
433                 while (!description && (set = [enumerator nextObject])) {
434                         NSEnumerator    *statusDictsEnumerator = [set objectEnumerator];
435                         NSDictionary    *statusDict;
436                         while (!description && (statusDict = [statusDictsEnumerator nextObject])) {
437                                 if ([[statusDict objectForKey:KEY_STATUS_NAME] isEqualToString:statusName]){
438                                         description = [statusDict objectForKey:KEY_STATUS_DESCRIPTION];
439                                 }
440                         }
441                 }               
442         }
443         
444         return description;
448  * @brief Return the localized description for the sate of the passed status
450  * This could be stored with the statusState, but that would break if the locale changed.  This way, the nonlocalized
451  * string is used to look up the appropriate localized one.
453  * @result A localized description such as @"Away" or @"Out to Lunch" of the state used by statusState
454  */
455 - (NSString *)descriptionForStateOfStatus:(AIStatus *)statusState
457         return [self localizedDescriptionForStatusName:[statusState statusName]
458                                                                                 statusType:[statusState statusType]];
462  * @brief The status name to use by default for a passed type
464  * This is the name which will be used for new AIStatus objects of this type.
465  */
466 - (NSString *)defaultStatusNameForType:(AIStatusType)statusType
468         //Set the default status name
469         switch (statusType) {
470                 case AIAvailableStatusType:
471                         return STATUS_NAME_AVAILABLE;
472                         break;
473                 case AIAwayStatusType:
474                         return STATUS_NAME_AWAY;
475                         break;
476                 case AIInvisibleStatusType:
477                         return STATUS_NAME_INVISIBLE;
478                         break;
479                 case AIOfflineStatusType:
480                         return STATUS_NAME_OFFLINE;
481                         break;
482         }
484         return nil;
487 #pragma mark Setting Status States
489  * @brief Set the active status state
491  * Sets the currently active status state.  This applies throughout Adium and to all accounts.  The state will become
492  * effective immediately.
493  */
494 - (void)setActiveStatusState:(AIStatus *)statusState
496         //Apply the state to our accounts and notify (delay to the next run loop to improve perceived speed)
497         [self performSelector:@selector(applyState:toAccounts:)
498                            withObject:statusState
499                            withObject:[[adium accountController] accounts]
500                            afterDelay:0];
504  * @brief Return the <tt>AIStatus</tt> to be used by accounts as they are created
505  */
506 - (AIStatus *)defaultInitialStatusState
508         return [[self builtInStateArray] objectAtIndex:0];
512  * @brief Reset the active status state
514  * All active status states cache will also reset.  Posts an active status changed notification.  The active state
515  * will be regenerated the next time it is requested.
516  */
517 - (void)_resetActiveStatusState
519         //Clear the active status state.  It will be rebuilt next time it is requested
520         [_activeStatusState release]; _activeStatusState = nil;
521         [_allActiveStatusStates release]; _allActiveStatusStates = nil;
523         //Let observers know the active state has changed
524         if (!activeStatusUpdateDelays) {
525                 [[adium notificationCenter] postNotificationName:AIStatusActiveStateChangedNotification object:nil];
526         }
530  * @brief Account status changed.
532  * Rebuild all our state menus
533  */
534 - (NSSet *)updateListObject:(AIListObject *)inObject keys:(NSSet *)inModifiedKeys silent:(BOOL)silent
536         if ([inObject isKindOfClass:[AIAccount class]]) {
537                 if ([inModifiedKeys containsObject:@"Online"] ||
538                         [inModifiedKeys containsObject:@"IdleSince"] ||
539                         [inModifiedKeys containsObject:@"StatusState"] ||
540                         [inModifiedKeys containsObject:KEY_ENABLED]) {
541                         
542                         [self _resetActiveStatusState];
543                 }
544         }
545         
546     return nil;
551  * @brief Delay activee status menu updates
553  * This should be called to prevent duplicative updates when multiple accounts are changing status simultaneously.
554  */
555 - (void)setDelayActiveStatusUpdates:(BOOL)shouldDelay
557         if (shouldDelay)
558                 activeStatusUpdateDelays++;
559         else
560                 activeStatusUpdateDelays--;
561         
562         if (!activeStatusUpdateDelays) {
563                 [[adium notificationCenter] postNotificationName:AIStatusActiveStateChangedNotification object:nil];
564         }
568  * @brief Delay activee status menu updates
570  * This should be called to prevent duplicative rebuilds when the status menu will change multple times.
571  */
572 - (void)setDelayStatusMenuRebuilding:(BOOL)shouldDelay
574         if (shouldDelay)
575                 statusMenuRebuildDelays++;
576         else
577                 statusMenuRebuildDelays--;
578         
579         if (!statusMenuRebuildDelays) {
580                 [[adium notificationCenter] postNotificationName:AIStatusStateArrayChangedNotification object:nil];     
581         }
585  * @brief Apply a state to multiple accounts
586  */
587 - (void)applyState:(AIStatus *)statusState toAccounts:(NSArray *)accountArray
589         NSEnumerator    *enumerator;
590         AIAccount               *account;
591         AIStatus                *aStatusState;
592         BOOL                    shouldRebuild = NO;
593         BOOL                    noConnectedAccounts = ![[adium accountController] oneOrMoreConnectedOrConnectingAccounts];
594         BOOL                    isOfflineStatus = ([statusState statusType] == AIOfflineStatusType);
595         [self setDelayActiveStatusUpdates:YES];
596         
597         /* If we're going offline, determine what accounts are currently online, first, so that we can restore that when an online state
598          * is chosen later.
599          */
600         if  (isOfflineStatus && !noConnectedAccounts) {
601                 [accountsToConnect removeAllObjects];
603                 enumerator = [accountArray objectEnumerator];
604                 while ((account = [enumerator nextObject])) {
605                         if ([account online]) [accountsToConnect addObject:account];
606                 }
607         }
609         if (noConnectedAccounts) {
610                 /* No connected accounts: Connect all enabled accounts which were set offline previously.
611                  * If we have no such list of accounts, connect 'em all.
612                  */
613                 BOOL noAccountsToConnectCount = ([accountsToConnect count] == 0);
614                 enumerator = [accountArray objectEnumerator];
615                 while ((account = [enumerator nextObject])) {
616                         if ([account enabled] &&
617                                 ([accountsToConnect containsObject:account] || noAccountsToConnectCount)) {
618                                 [account setStatusState:statusState];
620                         } else {
621                                 [account setStatusStateAndRemainOffline:statusState];   
622                         }
623                 }
625         } else {
626                 //At least one account is online.  Just change its status without taking any other accounts online.
627                 enumerator = [accountArray objectEnumerator];
628                 while ((account = [enumerator nextObject])) {
629                         if ([account online] || isOfflineStatus) {
630                                 [account setStatusState:statusState];
631                                 
632                         } else {
633                                 [account setStatusStateAndRemainOffline:statusState];                   
634                         }
635                 }
636         }
638         //If this is not an offline status, we've now made use of accountsToConnect and should clear it so it isn't used again.
639         if (!isOfflineStatus) {
640                 [accountsToConnect removeAllObjects];
641         }
643         //Any objects in the temporary state array which aren't the state we just set should now be removed.
644         enumerator = [[[temporaryStateArray copy] autorelease] objectEnumerator];
645         while ((aStatusState = [enumerator nextObject])) {
646                 if (aStatusState != statusState) {
647                         [temporaryStateArray removeObject:aStatusState];
648                         shouldRebuild = YES;
649                 }
650         }
652         //Add to our temporary status array if it's not in our state array
653         if (![[self flatStatusSet] containsObject:statusState] &&
654                 ![temporaryStateArray containsObject:statusState]) {
655                 [temporaryStateArray addObject:statusState];
656                 shouldRebuild = YES;
657         }
659         if (shouldRebuild) {
660                 [self notifyOfChangedStatusArray];
661         }
663         [self setDelayActiveStatusUpdates:NO];
666 #pragma mark Retrieving Status States
668  * @brief Access to Adium's user-defined states
670  * Returns the root AIStatusGroup of user-defined states
671  */
672 - (AIStatusGroup *)rootStateGroup
674         if (!_rootStateGroup) {
675                 NSData  *savedStateData = [[adium preferenceController] preferenceForKey:KEY_SAVED_STATUS
676                                                                                                                                                    group:PREF_GROUP_SAVED_STATUS];
677                 if (savedStateData) {
678                         id archivedObject = [NSKeyedUnarchiver unarchiveObjectWithData:savedStateData];
680                         if ([archivedObject isKindOfClass:[AIStatusGroup class]]) {
681                                 //Adium 1.0 archives an AIStatusGroup
682                                 _rootStateGroup = [archivedObject retain];
683                         
684                         } else if  ([archivedObject isKindOfClass:[NSArray class]]) {
685                                 //Adium 0.8x archived an NSArray
686                                 _rootStateGroup = [[AIStatusGroup statusGroupWithContainedStatusItems:archivedObject] retain];
687                         }
688                 }
690                 if (!_rootStateGroup) _rootStateGroup = [[AIStatusGroup statusGroup] retain];
692                 //Upgrade Adium 0.7x away messages
693                 [self _upgradeSavedAwaysToSavedStates];
694         }
696         return _rootStateGroup;
700  * @brief Return the array of built-in states
702  * These are basic Available and Away states which should always be visible and are (by convention) immutable.
703  * The first state in BUILT_IN_STATE_ARRAY will be used as the default for accounts as they are created.
704  */
705 - (NSArray *)builtInStateArray
707         if (!builtInStateArray) {
708                 NSArray                 *savedBuiltInStateArray = [NSArray arrayNamed:BUILT_IN_STATE_ARRAY forClass:[self class]];
709                 NSEnumerator    *enumerator;
710                 NSDictionary    *dict;
712                 builtInStateArray = [[NSMutableArray alloc] initWithCapacity:[savedBuiltInStateArray count]];
714                 enumerator = [savedBuiltInStateArray objectEnumerator];
715                 while ((dict = [enumerator nextObject])) {
716                         AIStatus        *status = [AIStatus statusWithDictionary:dict];
717                         [builtInStateArray addObject:status];
719                         //Store a reference to our offline state if we just loaded it
720                         if ([status statusType] == AIOfflineStatusType) {
721                                 [offlineStatusState release];
722                                 offlineStatusState = [status retain];
723                         }
724                 }
725         }
727         return builtInStateArray;
731 * @brief Create and add the built-in status types; even if no service explicitly registers these, they are available.
733  * The built-in status types are basic, generic "Available" and "Away" states.
734  */
735 - (void)buildBuiltInStatusTypes
737         NSDictionary    *statusDict;
738         
739         builtInStatusTypes[AIAvailableStatusType] = [[NSMutableSet alloc] init];
740         statusDict = [NSDictionary dictionaryWithObjectsAndKeys:
741                 STATUS_NAME_AVAILABLE, KEY_STATUS_NAME,
742                 [self localizedDescriptionForCoreStatusName:STATUS_NAME_AVAILABLE], KEY_STATUS_DESCRIPTION,
743                 [NSNumber numberWithInt:AIAvailableStatusType], KEY_STATUS_TYPE,
744                 nil];
745         [builtInStatusTypes[AIAvailableStatusType] addObject:statusDict];
746         
747         builtInStatusTypes[AIAwayStatusType] = [[NSMutableSet alloc] init];
748         statusDict = [NSDictionary dictionaryWithObjectsAndKeys:
749                 STATUS_NAME_AWAY, KEY_STATUS_NAME,
750                 [self localizedDescriptionForCoreStatusName:STATUS_NAME_AWAY], KEY_STATUS_DESCRIPTION,
751                 [NSNumber numberWithInt:AIAwayStatusType], KEY_STATUS_TYPE,
752                 nil];
753         [builtInStatusTypes[AIAwayStatusType] addObject:statusDict];
757 - (AIStatus *)offlineStatusState
759         //Ensure the built in states have been loaded
760         [self builtInStateArray];
762         NSAssert(offlineStatusState != nil, @"Nil offline status state");
763         return offlineStatusState;
767  * @brief Return a sorted state array for use in menu item creation
769  * The array is created by adding the built in states to the user states, then sorting using _statusArraySort
770  * The resulting array may contain AIStatus and AIStatusGroup objects.
772  * @result A cached NSArray which is sorted by status type (available, away), built-in vs. user-made, and then original ordering.
773  */
774 - (NSArray *)sortedFullStateArray
776         if (!_sortedFullStateArray) {
777                 NSArray                 *originalStateArray;
778                 NSMutableArray  *tempArray;
780                 //Start with everything contained 1) in our built-in array and then 2) in our root group
781                 originalStateArray = [[self builtInStateArray] arrayByAddingObjectsFromArray:[[self rootStateGroup] containedStatusItems]];
782                 
783                 tempArray = [originalStateArray mutableCopy];
785                 //Now add the temporary statues
786                 [tempArray addObjectsFromArray:[temporaryStateArray allObjects]];
788                 //Pass the original array so its indexes can be used for comparison of saved state ordering
789                 [AIStatusGroup sortArrayOfStatusItems:tempArray context:originalStateArray];
791                 _sortedFullStateArray = tempArray;
792         }
794         return _sortedFullStateArray;
798  * @brief Generate and return an array of AIStatus objects which are all known saved, temporary, and built-in statuses
799  */
800 - (NSArray *)flatStatusSet
802         if (!_flatStatusSet) {
803                 NSMutableArray  *tempArray = [[[self rootStateGroup] flatStatusSet] mutableCopy];
805                 //Add built in states
806                 [tempArray addObjectsFromArray:[self builtInStateArray]];
808                 //Add temporary ones
809                 [tempArray addObjectsFromArray:[temporaryStateArray allObjects]];
811                 _flatStatusSet = tempArray;
812         }
813         
814         return _flatStatusSet;
818  * @brief Retrieve active status state
820  * @result The currently active status state.
822  * This is defined as the status state which the most accounts are currently using.  The behavior in case of a tie
823  * is currently undefined but will yield one of the tying states.
824  */
825 - (AIStatus *)activeStatusState
827         if (!_activeStatusState) {
828                 NSEnumerator            *enumerator = [[[adium accountController] accounts] objectEnumerator];
829                 NSCountedSet            *statusCounts = [NSCountedSet set];
830                 AIAccount                       *account;
831                 AIStatus                        *statusState;
832                 unsigned                         highestCount = 0;
833                 //This was "oneOrMoreConnectedOrConnectingAccounts" before... was there a good reason?
834                 BOOL                             accountsAreOnline = [[adium accountController] oneOrMoreConnectedAccounts];
836                 if (accountsAreOnline) {
837                         AIStatus        *bestStatusState = nil;
839                         while ((account = [enumerator nextObject])) {
840                                 if ([account online]) {
841                                         AIStatus *accountStatusState = [account statusState];
842                                         [statusCounts addObject:(accountStatusState ?
843                                                                                          accountStatusState :
844                                                                                          [self defaultInitialStatusState])];
845                                 }
846                         }
848                         enumerator = [statusCounts objectEnumerator];
849                         while ((statusState = [enumerator nextObject])) {
850                                 unsigned thisCount = [statusCounts countForObject:statusState];
851                                 if (thisCount > highestCount) {
852                                         bestStatusState = statusState;
853                                         highestCount = thisCount;
854                                 }
855                         }
857                         _activeStatusState = (bestStatusState ? [bestStatusState retain]: [offlineStatusState retain]);
859                 } else {
860                         _activeStatusState = [offlineStatusState retain];
861                 }
862         }
864         return _activeStatusState;
868  * @brief Find the 'active' AIStatusType
870  * The active type is the one used by the largest number of accounts.  In case of a tie, the order of the AIStatusType
871  * enum is respected
873  * @param invisibleIsAway If YES, AIInvisibleStatusType is trated as AIAwayStatusType
874  * @result The active AIStatusType for online accounts, or AIOfflineStatusType if all accounts are  offline
875  */
876 - (AIStatusType)activeStatusTypeTreatingInvisibleAsAway:(BOOL)invisibleIsAway
878         NSEnumerator            *enumerator = [[[adium accountController] accounts] objectEnumerator];
879         AIAccount                       *account;
880         int                                     statusTypeCount[STATUS_TYPES_COUNT];
881         AIStatusType            activeStatusType = AIOfflineStatusType;
882         unsigned                        highestCount = 0;
884         unsigned i;
885         for (i = 0 ; i < STATUS_TYPES_COUNT ; i++) {
886                 statusTypeCount[i] = 0;
887         }
889         while ((account = [enumerator nextObject])) {
890                 if ([account online] || [account integerStatusObjectForKey:@"Connecting"]) {
891                         AIStatusType statusType = [[account statusState] statusType];
893                         //If invisibleIsAway, pretend that invisible is away
894                         if (invisibleIsAway && (statusType == AIInvisibleStatusType)) statusType = AIAwayStatusType;
896                         statusTypeCount[statusType]++;
897                 }
898         }
900         for (i = 0 ; i < STATUS_TYPES_COUNT ; i++) {
901                 if (statusTypeCount[i] > highestCount) {
902                         activeStatusType = i;
903                         highestCount = statusTypeCount[i];
904                 }
905         }
907         return activeStatusType;
911  * @brief All active status states
913  * A status state is active if any enabled account is currently in that state.
915  * The return value of this method is cached.
917  * @result An <tt>NSSet</tt> of <tt>AIStatus</tt> objects
918  */
919 - (NSSet *)allActiveStatusStates
921         if (!_allActiveStatusStates) {
922                 _allActiveStatusStates = [[NSMutableSet alloc] init];
923                 NSEnumerator            *enumerator = [[[adium accountController] accounts] objectEnumerator];
924                 AIAccount                       *account;
926                 while ((account = [enumerator nextObject])) {
927                         if ([account enabled]) {
928                                 [_allActiveStatusStates addObject:[account statusState]];
929                         }
930                 }
931         }
933         return _allActiveStatusStates;
937  * @brief Return the set of all unavailable statuses in use by online or connection accounts
939  * @param activeUnvailableStatusType Pointer to an AIStatusType; returns by reference the most popular unavailable type
940  * @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
941  * @param allOnlineAccountsAreUnvailable Pointer to a BOOL; returns by reference YES is all online accounts are unavailable, NO if one or more is available
942  */
943 - (NSSet *)activeUnavailableStatusesAndType:(AIStatusType *)activeUnvailableStatusType withName:(NSString **)activeUnvailableStatusName allOnlineAccountsAreUnvailable:(BOOL *)allOnlineAccountsAreUnvailable
945         NSEnumerator            *enumerator = [[[adium accountController] accounts] objectEnumerator];
946         AIAccount                       *account;
947         NSMutableSet            *activeUnvailableStatuses = [NSMutableSet set];
948         BOOL                            foundStatusName = NO;
949         int                                     statusTypeCount[STATUS_TYPES_COUNT];
951         statusTypeCount[AIAwayStatusType] = 0;
952         statusTypeCount[AIInvisibleStatusType] = 0;
953         
954         //Assume all accounts are unavailable until proven otherwise
955         if (allOnlineAccountsAreUnvailable != NULL) {
956                 *allOnlineAccountsAreUnvailable = YES;
957         }
958         
959         while ((account = [enumerator nextObject])) {
960                 if ([account online] || [account integerStatusObjectForKey:@"Connecting"]) {
961                         AIStatus        *statusState = [account statusState];
962                         AIStatusType statusType = [statusState statusType];
963                         
964                         if ((statusType == AIAwayStatusType) || (statusType == AIInvisibleStatusType)) {
965                                 NSString        *statusName = [statusState statusName];
966                                 
967                                 [activeUnvailableStatuses addObject:statusState];
968                                 
969                                 statusTypeCount[statusType]++;
970                                 
971                                 if (foundStatusName) {
972                                         //Once we find a status name, we only want to return it if all our status names are the same.
973                                         if ((activeUnvailableStatusName != NULL) &&
974                                            (*activeUnvailableStatusName != nil) && 
975                                            ![*activeUnvailableStatusName isEqualToString:statusName]) {
976                                                 *activeUnvailableStatusName = nil;
977                                         }
978                                 } else {
979                                         //We haven't found a status name yet, so store this one as the active status name
980                                         if (activeUnvailableStatusName != NULL) {
981                                                 *activeUnvailableStatusName = [statusState statusName];
982                                         }
983                                         foundStatusName = YES;
984                                 }
985                         } else {
986                                 //An online account isn't unavailable
987                                 if (allOnlineAccountsAreUnvailable != NULL) {
988                                         *allOnlineAccountsAreUnvailable = NO;
989                                 }
990                         }
991                 }
992         }
993         
994         if (activeUnvailableStatusType != NULL) {
995                 if (statusTypeCount[AIAwayStatusType] > statusTypeCount[AIInvisibleStatusType]) {
996                         *activeUnvailableStatusType = AIAwayStatusType;
997                 } else {
998                         *activeUnvailableStatusType = AIInvisibleStatusType;            
999                 }
1000         }
1001         
1002         return activeUnvailableStatuses;
1007  * @brief Next available unique status ID
1008  */
1009 - (NSNumber *)nextUniqueStatusID
1011         NSNumber        *nextUniqueStatusID;
1013         //Retain and autorelease since we'll be replacing this value (and therefore releasing it) via the preferenceController.
1014         nextUniqueStatusID = [[[[adium preferenceController] preferenceForKey:TOP_STATUS_STATE_ID
1015                                                                                                                                   group:PREF_GROUP_SAVED_STATUS] retain] autorelease];
1016         if (!nextUniqueStatusID) nextUniqueStatusID = [NSNumber numberWithInt:1];
1018         [[adium preferenceController] setPreference:[NSNumber numberWithInt:([nextUniqueStatusID intValue] + 1)]
1019                                                                                  forKey:TOP_STATUS_STATE_ID
1020                                                                                   group:PREF_GROUP_SAVED_STATUS];
1022         return nextUniqueStatusID;
1026  * @brief Find the status state with the requested uniqueStatusID
1027  */
1028 - (AIStatus *)statusStateWithUniqueStatusID:(NSNumber *)uniqueStatusID
1030         AIStatus                *statusState = nil;
1032         if (uniqueStatusID) {
1033                 NSEnumerator    *enumerator = [[self flatStatusSet] objectEnumerator];
1035                 while ((statusState = [enumerator nextObject])) {
1036                         if ([[statusState uniqueStatusID] compare:uniqueStatusID] == NSOrderedSame)
1037                                 break;
1038                 }
1039         }
1041         return statusState;
1044 //State Editing --------------------------------------------------------------------------------------------------------
1045 #pragma mark State Editing
1047  * @brief Add a state
1049  * Add a new state to Adium's state array.
1050  * @param state AIState to add
1051  */
1052 - (void)addStatusState:(AIStatus *)statusState
1054         AIStatusMutabilityType mutabilityType = [statusState mutabilityType];
1055         
1056         if ((mutabilityType == AILockedStatusState) ||
1057                 (mutabilityType == AISecondaryLockedStatusState)) {
1058                 //If we are adding a locked status, add it to the built-in statuses
1059                 [(NSMutableArray *)[self builtInStateArray] addObject:statusState];
1061                 [self notifyOfChangedStatusArray];
1063         } else {
1064                 //Otherwise, add it to the user-created statuses
1065                 [[self rootStateGroup] addStatusItem:statusState atIndex:-1];
1066         }
1070  * @brief Remove a state
1072  * Remove a new state from Adium's state array.
1073  * @param state AIStatus to remove
1074  */
1075 - (void)removeStatusState:(AIStatus *)statusState
1077         NSLog(@"shouldn't be calling this.");
1078 //      [stateArray removeObject:statusState];
1079         [self savedStatusesChanged];
1082 - (void)notifyOfChangedStatusArray
1084         //Clear the sorted menu items array since our state array changed.
1085         [_sortedFullStateArray release]; _sortedFullStateArray = nil;
1086         [_flatStatusSet release]; _flatStatusSet = nil;
1088         if (!statusMenuRebuildDelays) {
1089                 [[adium notificationCenter] postNotificationName:AIStatusStateArrayChangedNotification object:nil];     
1090         }
1094  * @brief Save changes to the state array and notify observers
1096  * Saves any outstanding changes to the state array.  There should be no need to call this manually, since all the
1097  * state array modifying methods in this class call it automatically after making changes.
1099  * After the state array is saved, observers are notified that is has changed.  Call after making any changes to the
1100  * state array from within the controller.
1101  */
1102 - (void)savedStatusesChanged
1104         //XXX change this back sometime before 1.0 release
1105 //      [[adium preferenceController] setPreference:[NSKeyedArchiver archivedDataWithRootObject:[self rootStateGroup]]
1106         [[adium preferenceController] setPreference:[NSKeyedArchiver archivedDataWithRootObject:[[self rootStateGroup] containedStatusItems]]
1107                                                                                  forKey:KEY_SAVED_STATUS
1108                                                                                   group:PREF_GROUP_SAVED_STATUS];
1109         [self notifyOfChangedStatusArray];
1112 - (void)statusStateDidSetUniqueStatusID
1114         //XXX change this back sometime before 1.0 release
1115 //      [[adium preferenceController] setPreference:[NSKeyedArchiver archivedDataWithRootObject:[self rootStateGroup]]
1116         [[adium preferenceController] setPreference:[NSKeyedArchiver archivedDataWithRootObject:[[self rootStateGroup] containedStatusItems]]
1117                                                                                  forKey:KEY_SAVED_STATUS
1118                                                                                   group:PREF_GROUP_SAVED_STATUS];
1122 * @brief Called when a state could potentially need to removed from the temporary (non-saved) list
1124  * If originalState is in the temporary status array, and it is being used on one or zero accounts, it 
1125  * is removed from the temporary status array. This method should be used when one or more accounts have stopped
1126  * using a single status state to determine if that status state is both non-saved and unused.
1128  * Note that while it would seem logical to post AIStatusStateArrayChangedNotification when this method would
1129  * return YES, we don't want to force observers of the notification to update immediately since there may be further
1130  * processing. We therefore let the calling method take action if it chooses to.
1132  * @result YES if the state was removed
1133  */
1134 - (BOOL)removeIfNecessaryTemporaryStatusState:(AIStatus *)originalState
1136         BOOL didRemove = NO;
1138         /* If the original (old) status state is in our temporary array and is not being used in more than 1 account, 
1139         * then we should remove it.
1140         */
1141         if ([temporaryStateArray containsObject:originalState]) {
1142                 NSEnumerator    *enumerator;
1143                 AIAccount               *account;
1144                 int                             count = 0;
1145                 
1146                 enumerator = [[[adium accountController] accounts] objectEnumerator];
1147                 while ((account = [enumerator nextObject])) {
1148                         if ([account actualStatusState] == originalState) {
1149                                 if (++count > 1) break;
1150                         }
1151                 }
1153                 if (count <= 1) {
1154                         [temporaryStateArray removeObject:originalState];
1155                         didRemove = YES;
1156                 }
1157         }
1159         return didRemove;
1162 - (void)saveStatusAsLastUsed:(AIStatus *)statusState
1164         NSMutableDictionary *lastStatusStates;
1165         
1166         lastStatusStates = [[[adium preferenceController] preferenceForKey:@"LastStatusStates"
1167                                                                                                                                  group:PREF_GROUP_STATUS_PREFERENCES] mutableCopy];
1168         if (!lastStatusStates) lastStatusStates = [NSMutableDictionary dictionary];
1169         
1170         [lastStatusStates setObject:[NSKeyedArchiver archivedDataWithRootObject:statusState]
1171                                                  forKey:[NSNumber numberWithInt:[statusState statusType]]];
1172         
1173         [[adium preferenceController] setPreference:lastStatusStates
1174                                                                                  forKey:@"LastStatusStates"
1175                                                                                   group:PREF_GROUP_STATUS_PREFERENCES]; 
1177 //Status state menu support ---------------------------------------------------------------------------------------------------
1178 #pragma mark Status state menu support
1180  * @brief Apply a custom state
1182  * Invoked when the custom state window is closed by the user clicking OK.  In response this method sets the custom
1183  * state as the active state.
1184  */
1185 - (void)customStatusState:(AIStatus *)originalState changedTo:(AIStatus *)newState forAccount:(AIAccount *)account
1187         BOOL shouldRebuild = NO;
1188         
1189         if ([newState mutabilityType] != AITemporaryEditableStatusState) {
1190                 [[adium statusController] addStatusState:newState];
1191         }
1193         if (account) {
1194                 shouldRebuild = [self removeIfNecessaryTemporaryStatusState:originalState];
1196                 //Now set the newState for the account
1197                 [account setStatusState:newState];
1198                 
1199                 //Enable the account if it isn't currently enabled
1200                 if (![account enabled]) {
1201                         [account setEnabled:YES];
1202                 }               
1204                 //Add to our temporary status array if it's not in our state array
1205                 if (shouldRebuild || (![[self flatStatusSet] containsObject:newState])) {
1206                         [temporaryStateArray addObject:newState];
1207                         
1208                         [self notifyOfChangedStatusArray];
1209                 }
1210                 
1211         } else {
1212                 //Set the state for all accounts.  This will clear out the temporaryStatusArray as necessary and update its contents.
1213                 [self setActiveStatusState:newState];
1214         }
1216         [self saveStatusAsLastUsed:newState];
1220 #pragma mark Upgrade code
1222  * @brief Temporary upgrade code for 0.7x -> 0.8
1224  * Versions 0.7x and prior stored their away messages in a different format.  This code allows a seamless
1225  * transition from 0.7x to 0.8.  We can easily recognize the old format because the away messages are of
1226  * type "Away" instead of type "State", which is used for all 0.8 and later saved states.
1227  * Since we are changing the array as we scan it, an enumerator will not work here.
1228  */
1229 #define OLD_KEY_SAVED_AWAYS                     @"Saved Away Messages"
1230 #define OLD_GROUP_AWAY_MESSAGES         @"Away Messages"
1231 #define OLD_STATE_SAVED_AWAY            @"Away"
1232 #define OLD_STATE_AWAY                          @"Message"
1233 #define OLD_STATE_AUTO_REPLY            @"Autoresponse"
1234 #define OLD_STATE_TITLE                         @"Title"
1235 - (void)_upgradeSavedAwaysToSavedStates
1237         NSArray *savedAways = [[adium preferenceController] preferenceForKey:OLD_KEY_SAVED_AWAYS
1238                                                                                                                                    group:OLD_GROUP_AWAY_MESSAGES];
1240         if (savedAways) {
1241                 NSEnumerator    *enumerator = [savedAways objectEnumerator];
1242                 NSDictionary    *state;
1244                 AILog(@"*** Upgrading Adium 0.7x saved aways: %@", savedAways);
1246                 [self setDelayStatusMenuRebuilding:YES];
1248                 //Update all the away messages to states.
1249                 while ((state = [enumerator nextObject])) {
1250                         if ([[state objectForKey:@"Type"] isEqualToString:OLD_STATE_SAVED_AWAY]) {
1251                                 AIStatus        *statusState;
1253                                 //Extract the away message information from this old record
1254                                 NSData          *statusMessageData = [state objectForKey:OLD_STATE_AWAY];
1255                                 NSData          *autoReplyMessageData = [state objectForKey:OLD_STATE_AUTO_REPLY];
1256                                 NSString        *title = [state objectForKey:OLD_STATE_TITLE];
1258                                 //Create an AIStatus from this information
1259                                 statusState = [AIStatus status];
1261                                 //General category: It's an away type
1262                                 [statusState setStatusType:AIAwayStatusType];
1264                                 //Specific state: It's the generic away. Funny how that works out.
1265                                 [statusState setStatusName:STATUS_NAME_AWAY];
1267                                 //Set the status message (which is just the away message).
1268                                 [statusState setStatusMessage:[NSAttributedString stringWithData:statusMessageData]];
1270                                 //It has an auto reply.
1271                                 [statusState setHasAutoReply:YES];
1273                                 if (autoReplyMessageData) {
1274                                         //Use the custom auto reply if it was set.
1275                                         [statusState setAutoReply:[NSAttributedString stringWithData:autoReplyMessageData]];
1276                                 } else {
1277                                         //If no autoReplyMesssage, use the status message.
1278                                         [statusState setAutoReplyIsStatusMessage:YES];
1279                                 }
1281                                 if (title) [statusState setTitle:title];
1283                                 //Add the updated state to our state array.
1284                                 [self addStatusState:statusState];
1285                         }
1286                 }
1288                 AILog(@"*** Finished upgrading old saved statuses");
1290                 //Save these changes and delete the old aways so we don't need to do this again.
1291                 [self setDelayStatusMenuRebuilding:NO];
1293                 [[adium preferenceController] setPreference:nil
1294                                                                                          forKey:OLD_KEY_SAVED_AWAYS
1295                                                                                           group:OLD_GROUP_AWAY_MESSAGES];
1296         }
1299 @end