Revert the "never use last saved status" behavior. Re-fixes #6227.
The behavior was #ifdef'd out to appease MSN users, who wanted their status messages to propagate. Find some middle ground here: if a status message is currently set, a custom new-status window will retain it. Otherwise, it'll use the saved one. And we'll always use the last-saved status settings otherwise, so "autoreply" saving always works.
5 // Created by Evan Schoenberg on 11/23/05.
8 #import <Adium/AIStatusMenu.h>
9 #import <Adium/AIStatus.h>
10 #import <Adium/AIStatusGroup.h>
11 #import <Adium/AIAccount.h>
12 #import <Adium/AIStatusControllerProtocol.h>
13 #import <Adium/AIEditStateWindowController.h>
14 #import <Adium/AIStatusIcons.h>
15 #import <Adium/AISocialNetworkingStatusMenu.h>
16 #import <Adium/AIAccountControllerProtocol.h>
17 #import <Adium/AIMenuControllerProtocol.h>
18 #import <AIUtilities/AIArrayAdditions.h>
19 #import <AIUtilities/AIEventAdditions.h>
20 #import <AIUtilities/AIMenuAdditions.h>
21 #import <AIUtilities/AIStringAdditions.h>
23 #define STATUS_TITLE_CUSTOM [AILocalizedString(@"Custom", nil) stringByAppendingEllipsis]
24 #define STATE_TITLE_MENU_LENGTH 30
26 @interface AIStatusMenu ()
27 - (id)initWithDelegate:(id<AIStatusMenuDelegate>)inDelegate;
30 @implementation AIStatusMenu
32 + (id)statusMenuWithDelegate:(id<AIStatusMenuDelegate>)inDelegate
34 return [[[self alloc] initWithDelegate:inDelegate] autorelease];
37 - (id)initWithDelegate:(id<AIStatusMenuDelegate>)inDelegate
39 if ((self = [super init])) {
40 self.delegate = inDelegate;
42 NSParameterAssert([delegate respondsToSelector:@selector(statusMenu:didRebuildStatusMenuItems:)]);
44 menuItemArray = [[NSMutableArray alloc] init];
45 stateMenuItemsAlreadyValidated = [[NSMutableSet alloc] init];
49 [[NSNotificationCenter defaultCenter] addObserver:self
50 selector:@selector(stateArrayChanged:)
51 name:AIStatusStateArrayChangedNotification
53 [[NSNotificationCenter defaultCenter] addObserver:self
54 selector:@selector(activeStatusStateChanged:)
55 name:AIStatusActiveStateChangedNotification
58 //Update our state menus when the state array or status icon set changes
59 [[NSNotificationCenter defaultCenter] addObserver:self
60 selector:@selector(statusIconSetChanged:)
61 name:AIStatusIconSetDidChangeNotification
70 [[NSNotificationCenter defaultCenter] removeObserver:self];
71 [stateMenuItemsAlreadyValidated release];
72 [menuItemArray release];
82 * @brief The delegate is just too good for the menu items we've created; it will create all of the ones it wants on its own
84 - (void)delegateWillReplaceAllMenuItems
86 //Remove the menu items from needing update
87 [stateMenuItemsAlreadyValidated removeAllObjects];
89 //Clear the array itself
90 [menuItemArray removeAllObjects];
94 * @brief The delegate created its own menu items it wants us to track and update
96 - (void)delegateCreatedMenuItems:(NSArray *)addedMenuItems
98 //Now add the items we were given
99 [menuItemArray addObjectsFromArray:addedMenuItems];
102 - (void)stateArrayChanged:(NSNotification *)notification
107 - (void)activeStatusStateChanged:(NSNotification *)notification
109 [stateMenuItemsAlreadyValidated removeAllObjects];
112 - (void)statusIconSetChanged:(NSNotification *)notification
118 * @brief Generate the custom menu item for a status type
120 - (NSMenuItem *)customMenuItemForStatusType:(AIStatusType)statusType
122 NSMenuItem *menuItem;
124 menuItem = [[NSMenuItem alloc] initWithTitle:STATUS_TITLE_CUSTOM
126 action:@selector(selectCustomState:)
129 [menuItem setImage:[AIStatusIcons statusIconForStatusName:nil
130 statusType:statusType
131 iconType:AIStatusIconMenu
132 direction:AIIconNormal]];
133 [menuItem setTag:statusType];
135 return [menuItem autorelease];
139 * @brief Rebuild the menu
143 NSEnumerator *enumerator;
144 NSMenuItem *menuItem;
145 AIStatus *statusState;
146 AIStatusType currentStatusType = AIAvailableStatusType;
147 AIStatusMutabilityType currentStatusMutabilityType = AILockedStatusState;
149 [adium.menuController delayMenuItemPostProcessing];
151 if ([delegate respondsToSelector:@selector(statusMenu:willRemoveStatusMenuItems:)]) {
152 [delegate statusMenu:self willRemoveStatusMenuItems:menuItemArray];
155 [menuItemArray removeAllObjects];
156 [stateMenuItemsAlreadyValidated removeAllObjects];
158 /* Create a menu item for each state. States must first be sorted such that states of the same AIStatusType
159 * are grouped together.
161 enumerator = [[adium.statusController sortedFullStateArray] objectEnumerator];
162 while ((statusState = [enumerator nextObject])) {
163 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
164 AIStatusType thisStatusType = statusState.statusType;
165 AIStatusType thisStatusMutabilityType = [statusState mutabilityType];
167 if ((currentStatusMutabilityType != AISecondaryLockedStatusState) &&
168 (thisStatusMutabilityType == AISecondaryLockedStatusState)) {
169 //Add the custom item, as we are ending this group
170 [menuItemArray addObject:[self customMenuItemForStatusType:currentStatusType]];
172 //Add a divider when we switch to a secondary locked group
173 [menuItemArray addObject:[NSMenuItem separatorItem]];
176 //We treat Invisible statuses as being the same as Away for purposes of the menu
177 if (thisStatusType == AIInvisibleStatusType) thisStatusType = AIAwayStatusType;
179 /* Add the "Custom..." state option and a separatorItem before beginning to add items for a new statusType
180 * Sorting the menu items before enumerating means that we know our statuses are sorted first by statusType
182 if ((currentStatusType != thisStatusType) &&
183 (currentStatusType != AIOfflineStatusType)) {
185 //Don't include a Custom item after the secondary locked group, as it was already included
186 if ((currentStatusMutabilityType != AISecondaryLockedStatusState)) {
187 [menuItemArray addObject:[self customMenuItemForStatusType:currentStatusType]];
191 [menuItemArray addObject:[NSMenuItem separatorItem]];
193 currentStatusType = thisStatusType;
196 menuItem = [[NSMenuItem alloc] initWithTitle:[AIStatusMenu titleForMenuDisplayOfState:statusState]
198 action:@selector(selectState:)
201 if ([statusState isKindOfClass:[AIStatus class]]) {
202 [menuItem setToolTip:[statusState statusMessageTooltipString]];
206 [menuItem setSubmenu:[(AIStatusGroup *)statusState statusSubmenuNotifyingTarget:self
207 action:@selector(selectState:)]];
209 [menuItem setRepresentedObject:[NSDictionary dictionaryWithObject:statusState
210 forKey:@"AIStatus"]];
211 [menuItem setTag:currentStatusType];
212 [menuItem setImage:[statusState menuIcon]];
213 [menuItemArray addObject:menuItem];
216 currentStatusMutabilityType = thisStatusMutabilityType;
220 if (currentStatusType != AIOfflineStatusType) {
221 /* Add the last "Custom..." state option for the last statusType we handled,
222 * which didn't get a "Custom..." item yet. At present, our last status type should always be
223 * our AIOfflineStatusType, so this will never be executed and just exists for completeness.
225 [menuItemArray addObject:[self customMenuItemForStatusType:currentStatusType]];
228 //Now that we are done creating the menu items, tell the plugin about them
229 [delegate statusMenu:self didRebuildStatusMenuItems:menuItemArray];
231 [adium.menuController endDelayMenuItemPostProcessing];
235 * @brief Menu validation
237 * Our state menu items should always be active, so always return YES for validation.
239 * Here we lazily set the state of our menu items if our stateMenuItemsAlreadyValidated set indicates it is needed.
241 * Random note: stateMenuItemsAlreadyValidated will almost never have a count of 0 because separatorItems
242 * get included but never get validated.
244 - (BOOL)validateMenuItem:(NSMenuItem *)menuItem
246 if (![stateMenuItemsAlreadyValidated containsObject:menuItem]) {
247 NSDictionary *dict = [menuItem representedObject];
248 AIAccount *account = [dict objectForKey:@"AIAccount"];
249 AIStatus *menuItemStatusState = [dict objectForKey:@"AIStatus"];
252 /* Account-specific menu items */
253 AIStatus *appropriateActiveStatusState = account.statusState;
255 /* Our "Custom..." menu choice has a nil represented object. If the appropriate active search state is
256 * in our array of states from which we made menu items, we'll be searching to match it. If it isn't,
257 * we have a custom state and will be searching for the custom item of the right type, switching all other
258 * menu items to NSOffState.
260 if ([adium.statusController.flatStatusSet containsObject:appropriateActiveStatusState]) {
261 //If the search state is in the array so is a saved state, search for the match
262 if ((menuItemStatusState == appropriateActiveStatusState) ||
263 ([menuItemStatusState isKindOfClass:[AIStatusGroup class]] &&
264 [(AIStatusGroup *)menuItemStatusState enclosesStatusState:appropriateActiveStatusState])) {
265 if ([menuItem state] != NSOnState) [menuItem setState:NSOnState];
267 if ([menuItem state] != NSOffState) [menuItem setState:NSOffState];
270 //If there is not a status state, we are in a Custom state. Search for the correct Custom item.
271 if (menuItemStatusState) {
272 //If the menu item has an associated state, it's always off.
273 if ([menuItem state] != NSOffState) [menuItem setState:NSOffState];
275 //If it doesn't, check the tag to see if it should be on or off.
276 if ([menuItem tag] == appropriateActiveStatusState.statusType) {
277 if ([menuItem state] != NSOnState) [menuItem setState:NSOnState];
279 if ([menuItem state] != NSOffState) [menuItem setState:NSOffState];
284 /* General menu items */
285 NSSet *allActiveStatusStates = [adium.statusController allActiveStatusStates];
286 int onState = (([allActiveStatusStates count] == 1) ? NSOnState : NSMixedState);
288 if (menuItemStatusState) {
289 //If this menu item has a status state, set it to the right on state if that state is active
290 if ([allActiveStatusStates containsObject:menuItemStatusState] ||
291 ([menuItemStatusState isKindOfClass:[AIStatusGroup class]] &&
292 [(AIStatusGroup *)menuItemStatusState enclosesStatusStateInSet:allActiveStatusStates])) {
293 if ([menuItem state] != onState) [menuItem setState:onState];
295 if ([menuItem state] != NSOffState) [menuItem setState:NSOffState];
298 //If it doesn't, check the tag to see if it should be on or off by looking for a matching custom state
299 NSEnumerator *activeStatusStatesEnumerator = [allActiveStatusStates objectEnumerator];
300 NSSet *flatStatusSet = adium.statusController.flatStatusSet;
301 AIStatus *statusState;
302 BOOL foundCorrectStatusState = NO;
304 while (!foundCorrectStatusState && (statusState = [activeStatusStatesEnumerator nextObject])) {
305 /* We found a custom match if our array of menu item states doesn't contain this state and
306 * its statusType matches the menuItem's tag.
308 foundCorrectStatusState = (![flatStatusSet containsObject:statusState] &&
309 ([menuItem tag] == statusState.statusType));
312 if (foundCorrectStatusState) {
313 if ([menuItem state] != NSOnState) [menuItem setState:onState];
315 if ([menuItem state] != NSOffState) [menuItem setState:NSOffState];
320 [stateMenuItemsAlreadyValidated addObject:menuItem];
327 * @brief Select a state menu item
329 * Invoked by a state menu item, sets the state corresponding to the menu item as the active state.
331 * If the representedObject NSDictionary has an @"AIAccount" object, set the state just for the appropriate AIAccount.
332 * Otherwise, set the state globally.
334 - (void)selectState:(id)sender
336 NSDictionary *dict = [sender representedObject];
337 AIStatusItem *statusItem = [dict objectForKey:@"AIStatus"];
338 AIAccount *account = [dict objectForKey:@"AIAccount"];
340 if ([statusItem isKindOfClass:[AIStatusGroup class]]) {
341 statusItem = [(AIStatusGroup *)statusItem anyContainedStatus];
344 /* Random undocumented feature of the moment... hold option and select a state to bring up the custom status window
345 * for modifying and then setting it. Alternately, select an active status (one in the on state) to do the same.
346 * Selecting a mixed state item should still select it to switch to full-on (all accounts).
348 NSEventType eventType = [[NSApp currentEvent] type];
349 BOOL keyEvent = (eventType == NSKeyDown || eventType == NSKeyUp);
350 BOOL isOptionClick = [NSEvent optionKey] && !keyEvent;
352 (([sender state] == NSOnState) && (statusItem.statusType != AIOfflineStatusType))) {
353 [AIEditStateWindowController editCustomState:(AIStatus *)statusItem
354 forType:statusItem.statusType
358 notifyingTarget:adium.statusController];
364 shouldRebuild = [adium.statusController removeIfNecessaryTemporaryStatusState:account.statusState];
365 [account setStatusState:(AIStatus *)statusItem];
367 //Enable the account if it isn't currently enabled
368 if (!account.enabled && statusItem.statusType != AIOfflineStatusType) {
369 [account setEnabled:YES];
373 //Rebuild our menus if there was a change
374 [[NSNotificationCenter defaultCenter] postNotificationName:AIStatusStateArrayChangedNotification object:nil];
378 [adium.statusController setActiveStatusState:(AIStatus *)statusItem];
384 * @brief Select the custom state menu item
386 * Invoked by the custom state menu item, opens a custom state window.
387 * If the representedObject NSDictionary has an @"AIAccount" object, configure just for the appropriate AIAccount.
388 * Otherwise, configure globally.
390 - (IBAction)selectCustomState:(id)sender
392 NSDictionary *dict = [sender representedObject];
393 AIAccount *account = [dict objectForKey:@"AIAccount"];
394 AIStatusType statusType = [sender tag];
395 AIStatus *baseStatusState;
398 baseStatusState = account.statusState;
400 baseStatusState = adium.statusController.activeStatusState;
403 /* If we are going to a custom state of a different type, we don't want to prefill with baseStatusState as it stands.
404 * Instead, we load the last used status of that type.
406 if ((baseStatusState.statusType != statusType)) {
407 NSDictionary *lastStatusStates = [adium.preferenceController preferenceForKey:@"LastStatusStates"
408 group:PREF_GROUP_STATUS_PREFERENCES];
409 NSData *lastStatusStateData = [lastStatusStates objectForKey:[[NSNumber numberWithInt:statusType] stringValue]];
410 AIStatus *lastStatusStateOfThisType = (lastStatusStateData ?
411 [NSKeyedUnarchiver unarchiveObjectWithData:lastStatusStateData] :
413 if (lastStatusStateOfThisType) {
414 // Restore the current status message into this last-saved variety, since users tend want to keep them.
415 // If it doesn't exist, use the last-saved status message.
416 if (baseStatusState.statusMessage.length) {
417 lastStatusStateOfThisType.statusMessage = baseStatusState.statusMessage;
420 baseStatusState = [[lastStatusStateOfThisType retain] autorelease];
424 [AIEditStateWindowController editCustomState:baseStatusState
429 notifyingTarget:adium.statusController];
433 #pragma mark Class methods
434 + (NSMenu *)staticStatusStatesMenuNotifyingTarget:(id)target selector:(SEL)selector
436 NSMenu *statusStatesMenu = [[NSMenu allocWithZone:[NSMenu menuZone]] init];
437 NSEnumerator *enumerator;
438 AIStatus *statusState;
439 AIStatusType currentStatusType = AIAvailableStatusType;
440 NSMenuItem *menuItem;
442 [statusStatesMenu setMenuChangedMessagesEnabled:NO];
443 [statusStatesMenu setAutoenablesItems:NO];
445 if (!target && !selector) {
446 //Need to set a target and action for items with submenus (AIStatusGroups) to be selectable... so if we're not given one, set one.
448 selector = @selector(dummyAction:);
451 /* Create a menu item for each state. States must first be sorted such that states of the same AIStatusType
452 * are grouped together.
454 enumerator = [[adium.statusController sortedFullStateArray] objectEnumerator];
455 while ((statusState = [enumerator nextObject])) {
456 AIStatusType thisStatusType = statusState.statusType;
458 //We treat Invisible statuses as being the same as Away for purposes of the menu
459 if (thisStatusType == AIInvisibleStatusType) thisStatusType = AIAwayStatusType;
461 if (currentStatusType != thisStatusType) {
462 //Add a divider between each type of status
463 [statusStatesMenu addItem:[NSMenuItem separatorItem]];
464 currentStatusType = thisStatusType;
467 menuItem = [[NSMenuItem alloc] initWithTitle:[AIStatusMenu titleForMenuDisplayOfState:statusState]
472 [menuItem setImage:[statusState menuIcon]];
473 [menuItem setTag:statusState.statusType];
474 [menuItem setRepresentedObject:[NSDictionary dictionaryWithObject:statusState
475 forKey:@"AIStatus"]];
476 if ([statusState isKindOfClass:[AIStatus class]]) {
477 [menuItem setToolTip:[statusState statusMessageTooltipString]];
481 [menuItem setSubmenu:[(AIStatusGroup *)statusState statusSubmenuNotifyingTarget:target
485 [statusStatesMenu addItem:menuItem];
489 [statusStatesMenu setMenuChangedMessagesEnabled:YES];
491 return [statusStatesMenu autorelease];
495 * @brief Determine a string to use as a menu title
497 * This method truncates a state title string for display as a menu item.
498 * Wide menus aren't pretty and may cause crashing in certain versions of OS X, so all state
499 * titles should be run through this method before being used as menu item titles.
501 * @param statusState The state for which we want a title
503 * @result An appropriate NSString title
505 + (NSString *)titleForMenuDisplayOfState:(AIStatusItem *)statusState
507 NSString *title = [statusState title];
509 /* Why plus 3? Say STATE_TITLE_MENU_LENGTH was 7, and the title is @"ABCDEFGHIJ".
510 * The shortened title will be @"ABCDEFG..." which looks to be just as long - even
511 * if the ellipsis is an ellipsis character and therefore technically two characters
512 * shorter. Better to just use the full string, which appears as being the same length.
514 if ([title length] > (STATE_TITLE_MENU_LENGTH + 3)) {
515 title = [title stringWithEllipsisByTruncatingToLength:STATE_TITLE_MENU_LENGTH];
521 + (void)dummyAction:(id)sender {};