Add a Reopen Closed Tab menu item to the File menu that will restore the most recently closed tab, similar to Chrome. Fixes #12537
Does not work with MSN group chats (and probably other protocols that have unnamed MUCs).
r=wix
2 * Adium is the legal property of its developers, whose names are listed in the copyright file included
3 * with this source distribution.
5 * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
6 * General Public License as published by the Free Software Foundation; either version 2 of the License,
7 * or (at your option) any later version.
9 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
10 * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
11 * Public License for more details.
13 * You should have received a copy of the GNU General Public License along with this program; if not,
14 * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
19 #import "AIInterfaceController.h"
21 #import <Adium/AIAccountControllerProtocol.h>
22 #import <Adium/AIContactControllerProtocol.h>
23 #import <Adium/AIChatControllerProtocol.h>
24 #import <Adium/AIContentControllerProtocol.h>
25 #import <Adium/AIMenuControllerProtocol.h>
26 #import <Adium/AIAuthorizationRequestsWindowController.h>
27 #import <AIUtilities/AIAttributedStringAdditions.h>
28 #import <AIUtilities/AIColorAdditions.h>
29 #import <AIUtilities/AIFontAdditions.h>
30 #import <AIUtilities/AIImageDrawingAdditions.h>
31 #import <AIUtilities/AIMenuAdditions.h>
32 #import <AIUtilities/AIStringAdditions.h>
33 #import <AIUtilities/AITooltipUtilities.h>
34 #import <AIUtilities/AIWindowAdditions.h>
35 #import <AIUtilities/AITextAttributes.h>
36 #import <AIUtilities/AIWindowControllerAdditions.h>
37 #import <Adium/AIChat.h>
38 #import <Adium/AIListContact.h>
39 #import <Adium/AIListGroup.h>
40 #import <Adium/AIListObject.h>
41 #import <Adium/AIMetaContact.h>
42 #import <Adium/AIService.h>
43 #import <Adium/AIServiceIcons.h>
44 #import <Adium/AISortController.h>
45 #import "AIMessageTabViewItem.h"
46 #import "KNShelfSplitview.h"
47 #import <Adium/AIContactList.h>
48 #import "AIListOutlineView.h"
50 #import "AIMessageViewController.h"
52 #define ERROR_MESSAGE_WINDOW_TITLE AILocalizedString(@"Adium : Error","Error message window title")
53 #define LABEL_ENTRY_SPACING 4.0f
54 #define DISPLAY_IMAGE_ON_RIGHT NO
56 #define PREF_GROUP_FORMATTING @"Formatting"
57 #define KEY_FORMATTING_FONT @"Default Font"
59 #define MESSAGES_WINDOW_MENU_TITLE AILocalizedString(@"Chats","Title for the messages window menu item")
61 //#define LOG_RESPONDER_CHAIN
63 @interface NSObject (AIInterfaceController_WindowPrefsTarget)
64 - (void)selectedWindowLevel:(id)sender;
67 @interface AIInterfaceController ()
68 - (void)_resetOpenChatsCache;
69 - (void)_addItemToMainMenuAndDock:(NSMenuItem *)item;
70 - (NSAttributedString *)_tooltipTitleForObject:(AIListObject *)object;
71 - (NSAttributedString *)_tooltipBodyForObject:(AIListObject *)object;
72 - (void)_pasteWithPreferredSelector:(SEL)preferredSelector sender:(id)sender;
73 - (void)updateCloseMenuKeys;
75 - (void)saveContainers;
76 - (void)restoreSavedContainers;
77 - (void)saveContainersOnQuit:(NSNotification *)notification;
79 - (void)toggleUserlist:(id)sender;
80 - (void)toggleUserlistSide:(id)sender;
81 - (void)clearDisplay:(id)sender;
82 - (void)closeContextualChat:(id)sender;
83 - (void)openAuthorizationWindow:(id)sender;
84 - (void)didReceiveContent:(NSNotification *)notification;
85 - (void)adiumDidFinishLoading:(NSNotification *)inNotification;
86 - (void)flashTimer:(NSTimer *)inTimer;
89 - (void)updateActiveWindowMenuItem;
90 - (void)buildWindowMenu;
92 - (AIChat *)mostRecentActiveChat;
96 * @class AIInterfaceController
97 * @brief Interface controller
99 * Chat window related requests, such as opening and closing chats, are routed through the interface controller
100 * to the appropriate component. The interface controller keeps track of the most recently active chat, handles chat
101 * cycling (switching between chats), chat sorting, and so on. The interface controller also handles switching to
102 * an appropriate window or chat when the dock icon is clicked for a 'reopen' event.
104 * Contact list window requests, such as toggling window visibilty are routed to the contact list controller component.
106 * Error messages are routed through the interface controller.
108 * Tooltips, such as seen on hover in the contact list are generated and displayed here. Tooltip display components and
109 * plugins register with the interface controller to be queried for contact information when a tooltip is displayed.
111 * When displays in Adium flash, such as in the dock or the contact list for unviewed content, the interface controller
112 * manages keeping the flashing synchronized.
114 * Finally, the interface controller manages many menu items, providing better menu item validation and target routing
115 * than the responder chain alone would do.
117 @implementation AIInterfaceController
121 if ((self = [super init])) {
122 contactListViewArray = [[NSMutableArray alloc] init];
123 messageViewArray = [[NSMutableArray alloc] init];
124 contactListTooltipEntryArray = [[NSMutableArray alloc] init];
125 contactListTooltipSecondaryEntryArray = [[NSMutableArray alloc] init];
126 closeMenuConfiguredForChat = NO;
127 _cachedOpenChats = nil;
128 mostRecentActiveChat = nil;
131 tooltipListObject = nil;
135 flashObserverArray = nil;
139 windowMenuArray = nil;
141 recentlyClosedChats = [[NSMutableArray alloc] init];
143 #ifdef LOG_RESPONDER_CHAIN
144 [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(reportResponderChain:) userInfo:nil repeats:YES];
151 #ifdef LOG_RESPONDER_CHAIN
152 //Can be called by a timer to periodically log the responder chain
153 //[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(reportResponderChain:) userInfo:nil repeats:YES];
154 - (void)reportResponderChain:(NSTimer *)inTimer
156 NSMutableString *responderChain = [NSMutableString string];
158 NSWindow *keyWin = [[NSApplication sharedApplication] keyWindow];
159 #warning 64BIT: Check formatting arguments
160 [responderChain appendFormat:@"%@ (%i): ",keyWin,[keyWin respondsToSelector:@selector(print:)]];
162 NSResponder *responder = [keyWin firstResponder];
164 //First, walk down the responder chain looking for a responder which can handle the preferred selector
166 #warning 64BIT: Check formatting arguments
167 [responderChain appendFormat:@"%@ (%i)",responder,[responder respondsToSelector:@selector(print:)]];
168 responder = [responder nextResponder];
169 if (responder) [responderChain appendString:@" -> "];
172 NSLog(responderChain);
176 - (void)controllerDidLoad
179 [interfacePlugin openInterface];
181 //Open the contact list window
182 [self showContactList:nil];
184 //Userlist show/hide item
185 menuItem_toggleUserlist = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Toggle User List", nil)
187 action:@selector(toggleUserlist:)
189 [menuItem_toggleUserlist setKeyEquivalentModifierMask:(NSCommandKeyMask | NSAlternateKeyMask)];
191 [adium.menuController addMenuItem:menuItem_toggleUserlist toLocation:LOC_Display_General];
193 menuItem_toggleUserlistSide = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Toggle User List Side", nil)
195 action:@selector(toggleUserlistSide:)
198 [adium.menuController addMenuItem:menuItem_toggleUserlistSide toLocation:LOC_Display_General];
200 NSMenuItem *menuItem = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Toggle User List", nil)
202 action:@selector(toggleUserlist:)
203 keyEquivalent:@""] autorelease];
205 [adium.menuController addContextualMenuItem:menuItem toLocation:Context_GroupChat_Action];
208 menuItem_clearDisplay = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Clear Display", nil)
210 action:@selector(clearDisplay:)
212 [adium.menuController addMenuItem:menuItem_clearDisplay toLocation:LOC_Display_MessageControl];
214 //Contact list menu item
215 menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Contact List","Name of the window which lists contacts")
217 action:@selector(toggleContactList:)
219 [adium.menuController addMenuItem:menuItem toLocation:LOC_Window_Fixed];
220 [adium.menuController addMenuItem:[[menuItem copy] autorelease] toLocation:LOC_Dock_Status];
223 menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Close Chat","Title for the close chat menu item")
225 action:@selector(closeContextualChat:)
227 [adium.menuController addContextualMenuItem:menuItem toLocation:Context_Tab_Action];
230 // Authorization requests menu item
231 menuItem = [[NSMenuItem alloc] initWithTitle:AILocalizedStringFromTableInBundle(@"Authorization Requests",nil, [NSBundle bundleForClass:[AIAuthorizationRequestsWindowController class]], nil)
233 action:@selector(openAuthorizationWindow:)
236 [adium.menuController addMenuItem:menuItem toLocation:LOC_Window_Auxiliary];
239 //Observe content so we can open chats as necessary
240 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveContent:)
241 name:CONTENT_MESSAGE_RECEIVED object:nil];
242 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveContent:)
243 name:CONTENT_MESSAGE_RECEIVED_GROUP object:nil];
245 //Observe Adium finishing loading so we can do things which may require other components or plugins
246 [[NSNotificationCenter defaultCenter] addObserver:self
247 selector:@selector(adiumDidFinishLoading:)
248 name:AIApplicationDidFinishLoadingNotification
251 //Observe quits so we can save containers.
252 [[NSNotificationCenter defaultCenter] addObserver:self
253 selector:@selector(saveContainersOnQuit:)
254 name:AIAppWillTerminateNotification
258 - (void)controllerWillClose
260 [contactListPlugin closeContactList];
261 [interfacePlugin closeInterface];
267 [contactListViewArray release]; contactListViewArray = nil;
268 [messageViewArray release]; messageViewArray = nil;
269 [interfaceArray release]; interfaceArray = nil;
271 [tooltipListObject release]; tooltipListObject = nil;
272 [tooltipTitle release]; tooltipTitle = nil;
273 [tooltipBody release]; tooltipBody = nil;
274 [tooltipImage release]; tooltipImage = nil;
276 [[NSNotificationCenter defaultCenter] removeObserver:self];
277 [adium.preferenceController unregisterPreferenceObserver:self];
279 [recentlyClosedChats release]; recentlyClosedChats = nil;
284 - (void)adiumDidFinishLoading:(NSNotification *)inNotification
286 //Observe preference changes. This will also restore saved containers if appropriate.
287 [adium.preferenceController registerPreferenceObserver:self forGroup:PREF_GROUP_INTERFACE];
289 [[NSNotificationCenter defaultCenter] removeObserver:self
290 name:AIApplicationDidFinishLoadingNotification
294 //Registers code to handle the interface
295 - (void)registerInterfaceController:(id <AIInterfaceComponent>)inController
297 if (!interfacePlugin) interfacePlugin = [inController retain];
300 //Register code to handle the contact list
301 - (void)registerContactListController:(id <AIContactListComponent>)inController
303 if (!contactListPlugin) contactListPlugin = [inController retain];
306 //Preferences changed
307 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key
308 object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
312 tabbedChatting = [[prefDict objectForKey:KEY_TABBED_CHATTING] boolValue];
313 groupChatsByContactGroup = [[prefDict objectForKey:KEY_GROUP_CHATS_BY_GROUP] boolValue];
314 saveContainers = [[prefDict objectForKey:KEY_SAVE_CONTAINERS] boolValue];
317 if (saveContainers) {
318 //Restore saved containers
319 [self restoreSavedContainers];
320 } else if ([prefDict objectForKey:KEY_CONTAINERS]) {
321 /* We've loaded without wanting to save containers; clear any saved
322 * from a previous session.
324 [adium.preferenceController setPreference:nil
325 forKey:KEY_CONTAINERS
326 group:PREF_GROUP_INTERFACE];
332 //Handle a reopen/dock icon click
333 - (BOOL)handleReopenWithVisibleWindows:(BOOL)visibleWindows
335 if (![self contactListIsVisibleAndMain] && [[interfacePlugin openContainerIDs] count] == 0) {
336 //The contact list is not visible, and there are no chat windows. Make the contact list visible.
337 [self showContactList:nil];
340 AIChat *mostRecentUnviewedChat;
342 //If windows are open, try switching to a chat with unviewed content
343 if ((mostRecentUnviewedChat = [adium.chatController mostRecentUnviewedChat])) {
344 if ([mostRecentActiveChat unviewedContentCount]) {
345 //If the most recently active chat has unviewed content, ensure it is in the front
346 [self setActiveChat:mostRecentActiveChat];
348 //Otherwise, switch to the chat which most recently received content
349 [self setActiveChat:mostRecentUnviewedChat];
353 NSWindow *targetWindow = nil;
354 BOOL unMinimizedWindows = 0;
356 //If there was no unviewed content, ensure that atleast one of Adium's windows is unminimized
357 for (NSWindow *window in [NSApp windows]) {
358 //Check stylemask to rule out the system menu's window (Which reports itself as visible like a real window)
359 if (([window styleMask] & (NSTitledWindowMask | NSClosableWindowMask | NSMiniaturizableWindowMask))) {
360 if (!targetWindow) targetWindow = window;
361 if (![window isMiniaturized]) unMinimizedWindows++;
365 //If there are no unminimized windows, unminimize the last one
366 if (unMinimizedWindows == 0 && targetWindow) {
367 [targetWindow deminiaturize:nil];
375 //Contact List ---------------------------------------------------------------------------------------------------------
376 #pragma mark Contact list
378 * @brief Toggles contact list between visible and hiden
380 - (IBAction)toggleContactList:(id)sender
382 if ([self contactListIsVisibleAndMain]) {
383 [self closeContactList:nil];
385 [[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
386 [self showContactList:nil];
391 * @brief Brings contact list to the front
393 - (IBAction)showContactList:(id)sender
395 [contactListPlugin showContactListAndBringToFront:YES];
399 * @brief Show the contact list window and bring Adium to the front
401 - (IBAction)showContactListAndBringToFront:(id)sender
403 [contactListPlugin showContactListAndBringToFront:YES];
404 [[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
408 * @brief Close the contact list window
410 - (IBAction)closeContactList:(id)sender
412 [contactListPlugin closeContactList];
416 * @returns YES if contact list is visible and selected, otherwise NO
418 - (BOOL)contactListIsVisibleAndMain
420 return [contactListPlugin contactListIsVisibleAndMain];
424 * @returns YES if contact list is visible, otherwise NO
426 - (BOOL)contactListIsVisible
428 return [contactListPlugin contactListIsVisible];
431 //Detachable Contact List ----------------------------------------------------------------------------------------------
432 #pragma mark Detachable Contact List
435 * @returns Created contact list controller for detached contact list
437 - (AIListWindowController *)detachContactList:(AIContactList *)aContactList
439 return [contactListPlugin detachContactList:aContactList];
443 #pragma mark Container Saving
445 * @brief Restores containers saved from a previous session
447 - (void)restoreSavedContainers
449 NSData *savedData = [adium.preferenceController preferenceForKey:KEY_CONTAINERS
450 group:PREF_GROUP_INTERFACE];
452 // If there's no data, we can't restore anything.
456 for (NSDictionary *dict in [NSKeyedUnarchiver unarchiveObjectWithData:savedData]) {
457 AIMessageWindowController *windowController = [self openContainerWithID:[dict objectForKey:@"ID"]
458 name:[dict objectForKey:@"Name"]];
459 AIChat *containerActiveChat = nil;
461 // Position the container where it was last saved (using -savedFrameFromString: to prevent going offscreen)
462 [[windowController window] setFrame:[windowController savedFrameFromString:[dict objectForKey:@"Frame"]] display:YES];
464 for (NSDictionary *chatDict in [dict objectForKey:@"Content"]) {
466 AIService *service = [adium.accountController firstServiceWithServiceID:[chatDict objectForKey:@"serviceID"]];
467 AIAccount *account = [adium.accountController accountWithInternalObjectID:[chatDict objectForKey:@"AccountID"]];
469 if ([[chatDict objectForKey:@"IsGroupChat"] boolValue]) {
470 chat = [adium.chatController chatWithName:[chatDict objectForKey:@"Name"]
473 chatCreationInfo:[chatDict objectForKey:@"ChatCreationInfo"]];
475 AIListContact *contact = [adium.contactController contactWithService:service
477 UID:[chatDict objectForKey:@"UID"]];
479 chat = [adium.chatController chatWithContact:contact];
482 // Tag the chat as restored.
483 [chat setValue:[NSNumber numberWithBool:YES]
484 forProperty:@"Restored Chat"
487 if ([[chatDict objectForKey:@"ActiveChat"] boolValue]) {
488 containerActiveChat = chat;
491 // Open the chat into the container we've created above.
492 [self openChat:chat inContainerWithID:[dict objectForKey:@"ID"] atIndex:-1];
495 if (containerActiveChat)
496 [self setActiveChat:containerActiveChat];
501 * @brief Saves open container information with their content when Adium quits
503 - (void)saveContainersOnQuit:(NSNotification *)notification
505 [self saveContainers];
509 * @brief Save opened containers and windows
511 * @param withContent Save the current buffer of the window to restore at a later point
513 - (void)saveContainers
515 if (!saveContainers) {
516 // Don't save anything if we're not set to.
520 // Save active containers.
521 NSMutableArray *savedContainers = [NSMutableArray array];
523 for (NSDictionary *dict in [interfacePlugin openContainersAndChats]) {
524 NSMutableArray *containerContents = [NSMutableArray array];
526 for (AIChat *chat in [dict objectForKey:@"Content"]) {
527 NSMutableDictionary *newContainerDict = [NSMutableDictionary dictionary];
529 [newContainerDict setObject:chat.account.internalObjectID forKey:@"AccountID"];
531 // Save chat-specific information.
532 if (chat.isGroupChat) {
533 // -chatCreationDictionary may be nil, so put it last.
534 [newContainerDict addEntriesFromDictionary:[NSDictionary dictionaryWithObjectsAndKeys:
535 [NSNumber numberWithBool:YES], @"IsGroupChat",
536 [NSNumber numberWithBool:([dict objectForKey:@"ActiveChat"] == chat)], @"ActiveChat",
538 [chat chatCreationDictionary], @"ChatCreationInfo",nil]];
540 [newContainerDict addEntriesFromDictionary:[NSDictionary dictionaryWithObjectsAndKeys:
541 [NSNumber numberWithBool:([dict objectForKey:@"ActiveChat"] == chat)], @"ActiveChat",
542 chat.listObject.UID, @"UID",
543 chat.account.service.serviceID, @"serviceID",
544 chat.account.internalObjectID, @"AccountID",nil]];
547 [containerContents addObject:newContainerDict];
550 // Replace the "Content" key in -openContainersAndChats with our version of the content.
551 // Remove the ActiveChat reference
552 // We use the same keys otherwise that -openContainersAndChats provides (Name, ID, Frame)
553 NSMutableDictionary *saveDict = [[dict mutableCopy] autorelease];
555 [saveDict removeObjectForKey:@"ActiveChat"];
557 [saveDict setObject:containerContents
560 [savedContainers addObject:saveDict];
563 [adium.preferenceController setPreference:[NSKeyedArchiver archivedDataWithRootObject:savedContainers]
564 forKey:KEY_CONTAINERS
565 group:PREF_GROUP_INTERFACE];
568 //Messaging ------------------------------------------------------------------------------------------------------------
569 //Methods for instructing the interface to provide a representation of chats, and to determine which chat has user focus
570 #pragma mark Messaging
573 * @brief Opens window for chat
575 - (void)openChat:(AIChat *)inChat
577 NSArray *containerIDs = [interfacePlugin openContainerIDs];
578 NSString *containerID = nil;
579 NSString *containerName = nil;
581 //Determine the correct container for this chat
583 if (!tabbedChatting) {
584 //We're not using tabs; each chat starts in its own container, based on the destination object or the chat name
585 if ([inChat listObject]) {
586 containerID = inChat.listObject.internalObjectID;
588 containerID = inChat.name;
591 } else if (groupChatsByContactGroup) {
592 if (inChat.isGroupChat) {
593 containerID = AILocalizedString(@"Group Chats",nil);
596 //XXX multiple containers: this is "correct" but maybe not desirable, as it is non-deterministic
597 AIListGroup *group = inChat.listObject.parentContact.groups.anyObject;
599 //If the contact is in the contact list root, we don't have a group
600 if (group && ![group isKindOfClass:[AIContactList class]]) {
601 containerID = group.displayName;
605 containerName = containerID;
609 //Open new chats into the first container (if not available, create a new one)
610 if ([containerIDs count] > 0) {
611 containerID = [containerIDs objectAtIndex:0];
617 //Determine the correct placement for this chat within the container
618 [interfacePlugin openChat:inChat inContainerWithID:containerID withName:containerName atIndex:-1];
619 if (![inChat isOpen]) {
620 [inChat setIsOpen:YES];
622 //Post the notification last, so observers receive a chat whose isOpen flag is yes.
623 [[NSNotificationCenter defaultCenter] postNotificationName:Chat_DidOpen object:inChat userInfo:nil];
627 - (id)openChat:(AIChat *)inChat inContainerWithID:(NSString *)containerID atIndex:(NSUInteger)idx
629 NSArray *openContainerIDs = [interfacePlugin openContainerIDs];
632 //Open new chats into the first container (if not available, create a new one)
633 if ([openContainerIDs count] > 0) {
634 containerID = [openContainerIDs objectAtIndex:0];
636 containerID = AILocalizedString(@"Chats",nil);
640 //Determine the correct placement for this chat within the container
641 id tabViewItem = [interfacePlugin openChat:inChat inContainerWithID:containerID withName:nil atIndex:idx];
642 if (![inChat isOpen]) {
643 [inChat setIsOpen:YES];
645 //Post the notification last, so observers receive a chat whose isOpen flag is yes.
646 [[NSNotificationCenter defaultCenter] postNotificationName:Chat_DidOpen object:inChat userInfo:nil];
652 * @brief Opens a container with a specific ID
654 * Asks the interfacePlugin to openContainerWithID:
656 - (AIMessageWindowController *)openContainerWithID:(NSString *)containerID name:(NSString *)containerName
658 return [interfacePlugin openContainerWithID:containerID name:containerName];
662 * @brief Close the interface for a chat
664 * Tell the interface plugin to close the chat.
666 - (void)closeChat:(AIChat *)inChat
669 if ([adium.chatController closeChat:inChat]) {
671 NSMutableDictionary *newRecentlyClosedChat = [NSMutableDictionary dictionary];
673 [newRecentlyClosedChat setObject:inChat.account.internalObjectID forKey:@"AccountID"];
675 if (inChat.isGroupChat) {
676 // -chatCreationDictionary may be nil, so put it last.
677 [newRecentlyClosedChat addEntriesFromDictionary:[NSDictionary dictionaryWithObjectsAndKeys:
678 [NSNumber numberWithBool:YES], @"IsGroupChat",
679 inChat.name, @"Name",
680 [inChat chatCreationDictionary], @"ChatCreationInfo",nil]];
682 [newRecentlyClosedChat addEntriesFromDictionary:[NSDictionary dictionaryWithObjectsAndKeys:
683 inChat.listObject.UID, @"UID",
684 inChat.account.service.serviceID, @"serviceID",
685 inChat.account.internalObjectID, @"AccountID",nil]];
688 [recentlyClosedChats insertObject:newRecentlyClosedChat atIndex:0];
690 // this sounds like a sensible limit: no-one will remember what chat they had in the closed tab beyond these
691 while (recentlyClosedChats.count > 16) {
692 [recentlyClosedChats removeLastObject];
695 [interfacePlugin closeChat:inChat];
701 * @brief Consolidate chats into a single container
703 - (void)consolidateChats
705 //We work with copies of these arrays, since moving chats may change their contents
706 NSArray *openContainerIDs = [[interfacePlugin openContainerIDs] copy];
707 NSEnumerator *containerEnumerator = [openContainerIDs objectEnumerator];
708 NSString *firstContainerID = [containerEnumerator nextObject];
709 NSString *containerID;
711 //For all containers but the first, move the chats they contain to the first container
712 while ((containerID = [containerEnumerator nextObject])) {
713 NSArray *openChats = [[interfacePlugin openChatsInContainerWithID:containerID] copy];
714 NSEnumerator *chatEnumerator = [openChats objectEnumerator];
717 //Move all the chats, providing a target index if chat sorting is enabled
718 while ((chat = [chatEnumerator nextObject])) {
719 [interfacePlugin moveChat:chat
720 toContainerWithID:firstContainerID
727 [self chatOrderDidChange];
729 [openContainerIDs release];
732 - (void)moveChatToNewContainer:(AIChat *)inChat
734 [interfacePlugin moveChatToNewContainer:inChat];
738 * @returns Active chat
740 - (AIChat *)activeChat
746 * @brief Set the active chat window
748 - (void)setActiveChat:(AIChat *)inChat
750 [interfacePlugin setActiveChat:inChat];
754 * @returns Last chat to be active, nil if not chat is open
756 - (AIChat *)mostRecentActiveChat
758 return mostRecentActiveChat;
762 * @brief Sets active chat window based on chat
764 - (void)setMostRecentActiveChat:(AIChat *)inChat
766 [self setActiveChat:inChat];
770 * @returns Array of open chats (cached, so call as frequently as desired)
772 - (NSArray *)openChats
774 if (!_cachedOpenChats) {
775 _cachedOpenChats = [[interfacePlugin openChats] retain];
778 return _cachedOpenChats;
781 - (NSArray *)openContainerIDs
783 return [interfacePlugin openContainerIDs];
787 * @param containerID ID for chat window
789 * @returns Array of all chats in chat window
791 - (NSArray *)openChatsInContainerWithID:(NSString *)containerID
793 return [interfacePlugin openChatsInContainerWithID:containerID];
797 * @brief The container ID for a chat
799 * @param chat The chat to look up
800 * @returns The container ID for the container the chat is in.
802 - (NSString *)containerIDForChat:(AIChat *)chat
804 return [interfacePlugin containerIDForChat:chat];
808 * @brief Resets the cache of open chats
810 - (void)_resetOpenChatsCache
812 [_cachedOpenChats release]; _cachedOpenChats = nil;
815 - (IBAction)reopenChat:(id)sender
817 if (recentlyClosedChats.count == 0) {
818 AILogWithSignature(@"Can't open recently closed tab: no recently closed tabs!");
822 NSDictionary *chatDict = [[[recentlyClosedChats objectAtIndex:0] retain] autorelease];
823 [recentlyClosedChats removeObjectAtIndex:0];
826 AIService *service = [adium.accountController firstServiceWithServiceID:[chatDict objectForKey:@"serviceID"]];
827 AIAccount *account = [adium.accountController accountWithInternalObjectID:[chatDict objectForKey:@"AccountID"]];
829 if ([[chatDict objectForKey:@"IsGroupChat"] boolValue]) {
830 chat = [adium.chatController chatWithName:[chatDict objectForKey:@"Name"]
833 chatCreationInfo:[chatDict objectForKey:@"ChatCreationInfo"]];
835 AIListContact *contact = [adium.contactController contactWithService:service
837 UID:[chatDict objectForKey:@"UID"]];
839 if (contact) chat = [adium.chatController chatWithContact:contact];
843 NSRunAlertPanel(AILocalizedString(@"Restoring chat failed", nil),
844 AILocalizedString(@"Restoring the last closed tab failed. Perhaps the account not exist anymore?", nil),
845 AILocalizedString(@"OK", nil),
851 // Tag the chat as restored.
852 [chat setValue:[NSNumber numberWithBool:YES]
853 forProperty:@"Restored Chat"
856 [self openChat:chat inContainerWithID:nil atIndex:-1];
857 [self setActiveChat:chat];
861 //Interface plugin callbacks -------------------------------------------------------------------------------------------
862 //These methods are called by the interface to let us know what's going on. We're informed of chats opening, closing,
863 //changing order, etc.
864 #pragma mark Interface plugin callbacks
866 * @brief A chat window did open: rebuild our window menu to show the new chat
868 * This should be called by the interface plugin (e.g. AIDualWindowInterfacePlugin) after a chat opens
870 * @param inChat Newly created chat
872 - (void)chatDidOpen:(AIChat *)inChat
874 [self _resetOpenChatsCache];
875 [self buildWindowMenu];
876 [self saveContainers];
880 * @brief A chat has become active: update our chat closing keys and flag this chat as selected in the window menu
882 * @param inChat Chat which has become active
884 - (void)chatDidBecomeActive:(AIChat *)inChat
886 AIChat *previouslyActiveChat = activeChat;
888 activeChat = [inChat retain];
890 [self updateCloseMenuKeys];
891 [self updateActiveWindowMenuItem];
893 if (inChat && (inChat != mostRecentActiveChat)) {
894 [mostRecentActiveChat release]; mostRecentActiveChat = nil;
895 mostRecentActiveChat = [inChat retain];
898 [[NSNotificationCenter defaultCenter] postNotificationName:Chat_BecameActive
900 userInfo:(previouslyActiveChat ?
901 [NSDictionary dictionaryWithObject:previouslyActiveChat
902 forKey:@"PreviouslyActiveChat"] :
906 /* Clear the unviewed content on the next event loop so other methods have a chance to react to the chat becoming
907 * active. Specifically, this lets the handleReopenWithVisibleWindows: method have a chance to know that this chat
908 * had unviewed content.
910 [inChat performSelector:@selector(clearUnviewedContentCount)
915 [previouslyActiveChat release];
919 * @brief A chat has become visible: send out a notification for components and plugins to take action
921 * @param inChat Chat that has become active
922 * @param nWindow Containing chat window
924 - (void)chatDidBecomeVisible:(AIChat *)inChat inWindow:(NSWindow *)inWindow
926 [[NSNotificationCenter defaultCenter] postNotificationName:@"AIChatDidBecomeVisible"
928 userInfo:[NSDictionary dictionaryWithObject:inWindow
929 forKey:@"NSWindow"]];
933 * @brief Find the window currently displaying a chat
935 * @returns Window for chat otherwise if the chat is not in any window, or is not visible in any window, returns nil
937 - (NSWindow *)windowForChat:(AIChat *)inChat
939 return [interfacePlugin windowForChat:inChat];
943 * @brief Find the chat active in a window
945 * If the window does not have an active chat, nil is returned
947 - (AIChat *)activeChatInWindow:(NSWindow *)window
949 return [interfacePlugin activeChatInWindow:window];
953 * @brief A chat window did close: rebuild our window menu to remove the chat
955 * @param inChat Chat that closed
957 - (void)chatDidClose:(AIChat *)inChat
959 [self _resetOpenChatsCache];
960 [inChat clearUnviewedContentCount];
961 [self buildWindowMenu];
963 if (!adium.isQuitting) {
964 // Don't save containers when the chats are closed while quitting
965 [self saveContainers];
968 if (inChat == activeChat) {
969 [activeChat release]; activeChat = nil;
972 if (inChat == mostRecentActiveChat) {
973 [mostRecentActiveChat release]; mostRecentActiveChat = nil;
978 * @brief The order of chats has changed: rebuild our window menu to reflect the new order
980 - (void)chatOrderDidChange
982 [self _resetOpenChatsCache];
983 [self buildWindowMenu];
985 if (!adium.isQuitting) {
986 // Don't save containers when the chats are closed while quitting
987 [self saveContainers];
990 [[NSNotificationCenter defaultCenter] postNotificationName:Chat_OrderDidChange object:nil userInfo:nil];
994 #pragma mark Unviewed content
997 * @breif Content was received, increase the unviewed content count of the chat (if it's not currently active)
999 - (void)didReceiveContent:(NSNotification *)notification
1001 AIChat *chat = [[notification userInfo] objectForKey:@"AIChat"];
1003 if (chat != activeChat) {
1004 [chat incrementUnviewedContentCount];
1009 //Chat close menus -----------------------------------------------------------------------------------------------------
1010 #pragma mark Chat close menus
1013 * @brief Closes currently active window
1015 - (IBAction)closeMenu:(id)sender
1017 [[[NSApplication sharedApplication] keyWindow] performClose:nil];
1021 * @brief Closes currently active chat (if there is an active chat)
1023 - (IBAction)closeChatMenu:(id)sender
1025 if (activeChat) [self closeChat:activeChat];
1029 * @brief Closes currently selected chat based on current chat contextual menu
1031 - (IBAction)closeContextualChat:(id)sender
1033 [self closeChat:[adium.menuController currentContextMenuChat]];
1037 * @brief Loop through open chats and close them
1039 - (IBAction)closeAllChats:(id)sender
1041 for (AIChat *chatToClose in [[interfacePlugin.openChats copy] autorelease]) {
1042 [self closeChat:chatToClose];
1047 * @brief Updates the key equivalents on 'close' and 'close chat' (dynamically changed to make cmd-w less destructive)
1049 - (void)updateCloseMenuKeys
1051 if (activeChat && !closeMenuConfiguredForChat) {
1052 [menuItem_close setKeyEquivalent:@"W"];
1053 [menuItem_closeChat setKeyEquivalent:@"w"];
1054 closeMenuConfiguredForChat = YES;
1055 } else if (!activeChat && closeMenuConfiguredForChat) {
1056 [menuItem_close setKeyEquivalent:@"w"];
1057 [menuItem_closeChat removeKeyEquivalent];
1058 closeMenuConfiguredForChat = NO;
1063 //Window Menu ----------------------------------------------------------------------------------------------------------
1064 #pragma mark Window Menu
1067 * @brief Open the authorization requests window.
1069 - (void)openAuthorizationWindow:(id)sender
1071 [[AIAuthorizationRequestsWindowController sharedController] showWindow:nil];
1075 * @brief Make a chat window active
1077 * Invoked by a selection in the window menu
1079 - (IBAction)showChatWindow:(id)sender
1081 [self setActiveChat:[sender representedObject]];
1082 [[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
1086 * @brief Updates the 'check' icon so it's next to the active window
1088 - (void)updateActiveWindowMenuItem
1092 for (item in windowMenuArray) {
1093 if ([item representedObject]) [item setState:([item representedObject] == activeChat ? NSOnState : NSOffState)];
1098 * @brief Builds the window menu
1100 * This function gets called whenever chats are opened, closed, or re-ordered - so improvements and optimizations here
1101 * would probably be helpful
1103 - (void)buildWindowMenu
1106 NSInteger windowKey = 1;
1108 //Remove any existing menus
1109 for (item in windowMenuArray) {
1110 [adium.menuController removeMenuItem:item];
1112 [windowMenuArray release]; windowMenuArray = [[NSMutableArray alloc] init];
1114 //Messages window and any open messasges
1115 for (NSDictionary *containerDict in [interfacePlugin openContainersAndChats]) {
1116 NSString *containerName = [containerDict objectForKey:@"Name"];
1117 NSArray *contentArray = [containerDict objectForKey:@"Content"];
1119 //Add a menu item for the container
1120 if (contentArray.count > 1) {
1121 item = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:([containerName length] ? containerName : AILocalizedString(@"Chats", nil))
1125 [self _addItemToMainMenuAndDock:item];
1129 //Add items for the chats it contains
1130 for (AIChat *chat in [contentArray objectEnumerator]) {
1131 NSString *windowKeyString;
1133 //Prepare a key equivalent for the controller
1134 if (windowKey < 10) {
1135 windowKeyString = [NSString stringWithFormat:@"%ld", (windowKey)];
1136 } else if (windowKey == 10) {
1137 windowKeyString = [NSString stringWithString:@"0"];
1139 windowKeyString = [NSString stringWithString:@""];
1142 item = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:chat.displayName
1144 action:@selector(showChatWindow:)
1145 keyEquivalent:windowKeyString];
1146 if ([contentArray count] > 1) [item setIndentationLevel:1];
1147 [item setRepresentedObject:chat];
1148 [item setImage:chat.chatMenuImage];
1149 [self _addItemToMainMenuAndDock:item];
1156 [self updateActiveWindowMenuItem];
1160 * brief Adds a menu item to the internal array, dock menu, and main menu
1162 * Should be used for adding a new window to the window menu (and dock menu)
1164 - (void)_addItemToMainMenuAndDock:(NSMenuItem *)item
1166 //Add to main menu first
1167 [adium.menuController addMenuItem:item toLocation:LOC_Window_Fixed];
1168 [windowMenuArray addObject:item];
1170 //Make a copy, and add to the dock
1172 [item setKeyEquivalent:@""];
1173 [adium.menuController addMenuItem:item toLocation:LOC_Dock_Status];
1174 [windowMenuArray addObject:item];
1179 //Chat Cycling ---------------------------------------------------------------------------------------------------------
1180 #pragma mark Chat Cycling
1183 * @brief Cycles to the next active chat
1185 - (void)nextChat:(id)sender
1187 if (!activeChat) return;
1189 NSString *containerID = [self containerIDForChat:activeChat];
1190 NSArray *chats = [self openChatsInContainerWithID:containerID];
1192 NSInteger nextChat = [chats indexOfObject:activeChat] + 1;
1194 if (nextChat >= chats.count)
1197 [self setActiveChat:[chats objectAtIndex:nextChat]];
1201 * @brief Cycles to the previus active chat
1203 - (void)previousChat:(id)sender
1205 if (!activeChat) return;
1207 NSString *containerID = [self containerIDForChat:activeChat];
1208 NSArray *chats = [self openChatsInContainerWithID:containerID];
1210 NSInteger nextChat = [chats indexOfObject:activeChat] - 1;
1213 nextChat = chats.count - 1;
1215 [self setActiveChat:[chats objectAtIndex:nextChat]];
1218 //Selected contact ------------------------------------------------
1219 #pragma mark Selected contact
1220 - (id)_performSelectorOnFirstAvailableResponder:(SEL)selector
1222 NSResponder *responder = [[[NSApplication sharedApplication] mainWindow] firstResponder];
1223 //Check the first responder
1224 if ([responder respondsToSelector:selector]) {
1225 return [responder performSelector:selector];
1228 //Search the responder chain
1230 responder = [responder nextResponder];
1231 if ([responder respondsToSelector:selector]) {
1232 return [responder performSelector:selector];
1235 } while (responder != nil);
1237 //None found, return nil
1240 - (id)_performSelectorOnFirstAvailableResponder:(SEL)selector conformingToProtocol:(Protocol *)protocol
1242 NSResponder *responder = [[[NSApplication sharedApplication] mainWindow] firstResponder];
1243 //Check the first responder
1244 if ([responder conformsToProtocol:protocol] && [responder respondsToSelector:selector]) {
1245 return [responder performSelector:selector];
1248 //Search the responder chain
1250 responder = [responder nextResponder];
1251 if ([responder conformsToProtocol:protocol] && [responder respondsToSelector:selector]) {
1252 return [responder performSelector:selector];
1255 } while (responder != nil);
1257 //None found, return nil
1262 * @returns The "selected"(represented) contact (By finding the first responder that returns a contact)
1263 * If no listObject is found, try to find a list object selected in a group chat
1265 - (AIListObject *)selectedListObject
1267 AIListObject *listObject = [self _performSelectorOnFirstAvailableResponder:@selector(listObject)];
1269 listObject = [self _performSelectorOnFirstAvailableResponder:@selector(preferredListObject)];
1274 - (AIListObject *)selectedListObjectInContactList
1276 return [self _performSelectorOnFirstAvailableResponder:@selector(listObject) conformingToProtocol:@protocol(ContactListOutlineView)];
1278 - (NSArray *)arrayOfSelectedListObjectsInContactList
1280 return [self _performSelectorOnFirstAvailableResponder:@selector(arrayOfListObjects) conformingToProtocol:@protocol(ContactListOutlineView)];
1282 - (NSArray *)arrayOfSelectedListObjectsWithGroupsInContactList
1284 return [self _performSelectorOnFirstAvailableResponder:@selector(arrayOfListObjectsWithGroups) conformingToProtocol:@protocol(ContactListOutlineView)];
1287 //Message View ---------------------------------------------------------------------------------------------------------
1288 //Message view is abstracted from the containing interface, since they're not directly related to eachother
1289 #pragma mark Message View
1290 //Registers a view to handle the contact list
1291 - (void)registerMessageDisplayPlugin:(id <AIMessageDisplayPlugin>)inPlugin
1293 [messageViewArray addObject:inPlugin];
1295 - (void)unregisterMessageDisplayPlugin:(id <AIMessageDisplayPlugin>)inPlugin
1297 [messageViewArray removeObject:inPlugin];
1299 - (id <AIMessageDisplayController>)messageDisplayControllerForChat:(AIChat *)inChat
1301 //Sometimes our users find it amusing to disable plugins that are located within the Adium bundle. This error
1302 //trap prevents us from crashing if they happen to disable all the available message view plugins.
1303 //PUT THAT PLUGIN BACK IT WAS IMPORTANT!
1304 if ([messageViewArray count] == 0) {
1305 NSRunCriticalAlertPanel(@"No Message View Plugin Installed",
1306 @"Adium cannot find its message view plugin. Please re-install. If you've manually disabled Adium's message view plugin, please re-enable it.",
1310 [NSApp terminate:nil];
1313 return [[messageViewArray objectAtIndex:0] messageDisplayControllerForChat:inChat];
1317 //Error Display --------------------------------------------------------------------------------------------------------
1318 #pragma mark Error Display
1319 - (void)handleErrorMessage:(NSString *)inTitle withDescription:(NSString *)inDesc
1321 [self handleMessage:inTitle withDescription:inDesc withWindowTitle:ERROR_MESSAGE_WINDOW_TITLE];
1324 - (void)handleMessage:(NSString *)inTitle withDescription:(NSString *)inDesc withWindowTitle:(NSString *)inWindowTitle;
1326 NSDictionary *errorDict;
1328 //Post a notification that an error was recieved
1329 errorDict = [NSDictionary dictionaryWithObjectsAndKeys:inTitle,@"Title",inDesc,@"Description",inWindowTitle,@"Window Title",nil];
1330 [[NSNotificationCenter defaultCenter] postNotificationName:Interface_ShouldDisplayErrorMessage object:nil userInfo:errorDict];
1333 //Display then clear the last disconnection error
1334 - (void)account:(AIAccount *)inAccount disconnectedWithError:(NSString *)disconnectionError
1339 //Question Display -----------------------------------------------------------------------------------------------------
1340 #pragma mark Question Display
1341 - (void)displayQuestion:(NSString *)inTitle withAttributedDescription:(NSAttributedString *)inDesc withWindowTitle:(NSString *)inWindowTitle
1342 defaultButton:(NSString *)inDefaultButton alternateButton:(NSString *)inAlternateButton otherButton:(NSString *)inOtherButton suppression:(NSString *)inSuppression
1343 target:(id)inTarget selector:(SEL)inSelector userInfo:(id)inUserInfo
1345 NSMutableDictionary *questionDict = [NSMutableDictionary dictionary];
1348 [questionDict setObject:inTitle forKey:@"Title"];
1350 [questionDict setObject:inDesc forKey:@"Description"];
1351 if(inWindowTitle != nil)
1352 [questionDict setObject:inWindowTitle forKey:@"Window Title"];
1353 if(inDefaultButton != nil)
1354 [questionDict setObject:inDefaultButton forKey:@"Default Button"];
1355 if(inAlternateButton != nil)
1356 [questionDict setObject:inAlternateButton forKey:@"Alternate Button"];
1357 if(inOtherButton != nil)
1358 [questionDict setObject:inOtherButton forKey:@"Other Button"];
1359 if(inSuppression != nil)
1360 [questionDict setObject:inSuppression forKey:@"Suppression Checkbox"];
1362 [questionDict setObject:inTarget forKey:@"Target"];
1363 if(inSelector != NULL)
1364 [questionDict setObject:NSStringFromSelector(inSelector) forKey:@"Selector"];
1365 if(inUserInfo != nil)
1366 [questionDict setObject:inUserInfo forKey:@"Userinfo"];
1368 [[NSNotificationCenter defaultCenter] postNotificationName:Interface_ShouldDisplayQuestion object:nil userInfo:questionDict];
1371 - (void)displayQuestion:(NSString *)inTitle withDescription:(NSString *)inDesc withWindowTitle:(NSString *)inWindowTitle
1372 defaultButton:(NSString *)inDefaultButton alternateButton:(NSString *)inAlternateButton otherButton:(NSString *)inOtherButton suppression:(NSString *)inSuppression
1373 target:(id)inTarget selector:(SEL)inSelector userInfo:(id)inUserInfo
1375 [self displayQuestion:inTitle
1376 withAttributedDescription:[[[NSAttributedString alloc] initWithString:inDesc
1377 attributes:[NSDictionary dictionaryWithObject:[NSFont systemFontOfSize:0]
1378 forKey:NSFontAttributeName]] autorelease]
1379 withWindowTitle:inWindowTitle
1380 defaultButton:inDefaultButton
1381 alternateButton:inAlternateButton
1382 otherButton:inOtherButton
1383 suppression:inSuppression
1386 userInfo:inUserInfo];
1388 //Synchronized Flashing ------------------------------------------------------------------------------------------------
1389 #pragma mark Synchronized Flashing
1390 //Register to observe the synchronized flashing
1391 - (void)registerFlashObserver:(id <AIFlashObserver>)inObserver
1393 //Setup the timer if we don't have one yet
1394 if (!flashObserverArray) {
1395 flashObserverArray = [[NSMutableArray alloc] init];
1396 flashTimer = [[NSTimer scheduledTimerWithTimeInterval:(1.0/2.0)
1398 selector:@selector(flashTimer:)
1400 repeats:YES] retain];
1403 //Add the new observer to the array
1404 [flashObserverArray addObject:inObserver];
1407 //Unregister from observing flashing
1408 - (void)unregisterFlashObserver:(id <AIFlashObserver>)inObserver
1410 //Remove the observer from our array
1411 [flashObserverArray removeObject:inObserver];
1413 //Release the observer array and uninstall the timer
1414 if ([flashObserverArray count] == 0) {
1415 [flashObserverArray release]; flashObserverArray = nil;
1416 [flashTimer invalidate];
1417 [flashTimer release]; flashTimer = nil;
1421 //Timer, invoke a flash
1422 - (void)flashTimer:(NSTimer *)inTimer
1426 for (id<AIFlashObserver>observer in [[flashObserverArray copy] autorelease]) {
1427 [observer flash:flashState];
1431 //Current state of flashing. This is an integer the increases by 1 with every flash. Mod to whatever range is desired
1438 //Tooltips -------------------------------------------------------------------------------------------------------------
1439 #pragma mark Tooltips
1440 //Registers code to display tooltip info about a contact
1441 - (void)registerContactListTooltipEntry:(id <AIContactListTooltipEntry>)inEntry secondaryEntry:(BOOL)isSecondary
1444 [contactListTooltipSecondaryEntryArray addObject:inEntry];
1446 [contactListTooltipEntryArray addObject:inEntry];
1449 //Unregisters code to display tooltip info about a contact
1450 - (void)unregisterContactListTooltipEntry:(id <AIContactListTooltipEntry>)inEntry secondaryEntry:(BOOL)isSecondary
1453 [contactListTooltipSecondaryEntryArray removeObject:inEntry];
1455 [contactListTooltipEntryArray removeObject:inEntry];
1458 - (NSArray *)contactListTooltipPrimaryEntries
1460 return contactListTooltipEntryArray;
1463 - (NSArray *)contactListTooltipSecondaryEntries
1465 return contactListTooltipSecondaryEntryArray;
1468 //list object tooltips
1469 - (void)showTooltipForListObject:(AIListObject *)object atScreenPoint:(NSPoint)point onWindow:(NSWindow *)inWindow
1472 if (object == tooltipListObject) { //If we already have this tooltip open
1473 //Move the existing tooltip
1474 [AITooltipUtilities showTooltipWithTitle:tooltipTitle
1477 imageOnRight:DISPLAY_IMAGE_ON_RIGHT
1480 orientation:TooltipBelow];
1482 } else { //This is a new tooltip
1484 NSMutableParagraphStyle *paragraphStyleTitle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
1485 NSMutableParagraphStyle *paragraphStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
1487 //Hold onto the new object
1488 [tooltipListObject release]; tooltipListObject = [object retain];
1491 [tooltipImage release];
1492 tooltipImage = [[tooltipListObject userIcon] retain];
1493 if (!tooltipImage) tooltipImage = [[AIServiceIcons serviceIconForObject:tooltipListObject
1494 type:AIServiceIconLarge
1495 direction:AIIconNormal] retain];
1497 //Reset the maxLabelWidth for the tooltip generation
1500 //Build a tooltip string for the primary information
1501 [tooltipTitle release]; tooltipTitle = [[self _tooltipTitleForObject:object] retain];
1503 //If there is an image, set the title tab and indentation settings independently
1505 //Set a right-align tab at the maximum label width and a left-align just past it
1506 tabArray = [[NSArray alloc] initWithObjects:[[[NSTextTab alloc] initWithType:NSRightTabStopType
1507 location:maxLabelWidth] autorelease]
1508 ,[[[NSTextTab alloc] initWithType:NSLeftTabStopType
1509 location:maxLabelWidth + LABEL_ENTRY_SPACING] autorelease]
1512 [paragraphStyleTitle setTabStops:tabArray];
1515 [paragraphStyleTitle setHeadIndent:(maxLabelWidth + LABEL_ENTRY_SPACING)];
1517 [tooltipTitle addAttribute:NSParagraphStyleAttributeName
1518 value:paragraphStyleTitle
1519 range:NSMakeRange(0,[tooltipTitle length])];
1521 //Reset the max label width since the body will be independent
1525 //Build a tooltip string for the secondary information
1526 [tooltipBody release]; tooltipBody = nil;
1527 tooltipBody = [[self _tooltipBodyForObject:object] retain];
1529 //Set a right-align tab at the maximum label width for the body and a left-align just past it
1530 tabArray = [[NSArray alloc] initWithObjects:[[[NSTextTab alloc] initWithType:NSRightTabStopType
1531 location:maxLabelWidth] autorelease]
1532 ,[[[NSTextTab alloc] initWithType:NSLeftTabStopType
1533 location:maxLabelWidth + LABEL_ENTRY_SPACING] autorelease]
1535 [paragraphStyle setTabStops:tabArray];
1537 [paragraphStyle setHeadIndent:(maxLabelWidth + LABEL_ENTRY_SPACING)];
1539 [tooltipBody addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:NSMakeRange(0,[tooltipBody length])];
1540 //If there is no image, also use these settings for the top part
1541 if (!tooltipImage) {
1542 [tooltipTitle addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:NSMakeRange(0,[tooltipTitle length])];
1545 //Display the new tooltip
1546 [AITooltipUtilities showTooltipWithTitle:tooltipTitle
1549 imageOnRight:DISPLAY_IMAGE_ON_RIGHT
1552 orientation:TooltipBelow];
1554 [paragraphStyleTitle release];
1555 [paragraphStyle release];
1559 //Hide the existing tooltip
1560 if (tooltipListObject) {
1561 [AITooltipUtilities showTooltipWithTitle:nil
1566 orientation:TooltipBelow];
1567 [tooltipListObject release]; tooltipListObject = nil;
1569 [tooltipTitle release]; tooltipTitle = nil;
1570 [tooltipBody release]; tooltipBody = nil;
1571 [tooltipImage release]; tooltipImage = nil;
1576 - (NSAttributedString *)_tooltipTitleForObject:(AIListObject *)object
1578 NSMutableAttributedString *titleString = [[NSMutableAttributedString alloc] init];
1580 id <AIContactListTooltipEntry> tooltipEntry;
1581 NSEnumerator *labelEnumerator;
1582 NSMutableArray *labelArray = [NSMutableArray array];
1583 NSMutableArray *entryArray = [NSMutableArray array];
1584 NSMutableAttributedString *entryString;
1588 NSString *formattedUID = object.formattedUID;
1590 //Configure fonts and attributes
1591 NSFontManager *fontManager = [NSFontManager sharedFontManager];
1592 NSFont *toolTipsFont = [NSFont toolTipsFontOfSize:10];
1593 NSMutableDictionary *titleDict = [NSMutableDictionary dictionaryWithObject:[fontManager convertFont:[NSFont toolTipsFontOfSize:12] toHaveTrait:NSBoldFontMask]
1594 forKey:NSFontAttributeName];
1595 NSMutableDictionary *labelDict = [NSMutableDictionary dictionaryWithObject:[fontManager convertFont:[NSFont toolTipsFontOfSize:9] toHaveTrait:NSBoldFontMask]
1596 forKey:NSFontAttributeName];
1597 NSMutableDictionary *labelEndLineDict = [NSMutableDictionary dictionaryWithObject:[NSFont toolTipsFontOfSize:2]
1598 forKey:NSFontAttributeName];
1599 NSMutableDictionary *entryDict = [NSMutableDictionary dictionaryWithObject:toolTipsFont
1600 forKey:NSFontAttributeName];
1602 //Get the user's display name as an attributed string
1603 NSAttributedString *displayName = [[NSAttributedString alloc] initWithString:object.displayName
1604 attributes:titleDict];
1605 NSAttributedString *filteredDisplayName = [adium.contentController filterAttributedString:displayName
1606 usingFilterType:AIFilterTooltips
1607 direction:AIFilterIncoming
1610 //Append the user's display name
1611 if (filteredDisplayName) {
1612 [titleString appendAttributedString:filteredDisplayName];
1615 //Append the user's formatted UID if there is one that's different to the display name
1616 if (formattedUID && (!([[[displayName string] compactedString] isEqualToString:[formattedUID compactedString]]))) {
1617 [titleString appendString:[NSString stringWithFormat:@" (%@)", formattedUID] withAttributes:titleDict];
1619 [displayName release];
1621 if ([object isKindOfClass:[AIListContact class]]) {
1622 if ((![object isKindOfClass:[AIMetaContact class]] || [(AIMetaContact *)object containsOnlyOneService]) &&
1623 [object userIcon]) {
1624 NSImage *serviceIcon = [[AIServiceIcons serviceIconForObject:object type:AIServiceIconSmall direction:AIIconNormal]
1625 imageByScalingToSize:NSMakeSize(14,14)];
1627 NSTextAttachment *attachment;
1628 NSTextAttachmentCell *cell;
1630 cell = [[NSTextAttachmentCell alloc] init];
1631 [cell setImage:serviceIcon];
1633 attachment = [[NSTextAttachment alloc] init];
1634 [attachment setAttachmentCell:cell];
1637 [titleString appendString:@" " withAttributes:nil];
1638 [titleString appendAttributedString:[NSAttributedString attributedStringWithAttachment:attachment]];
1639 [attachment release];
1644 if ([object isKindOfClass:[AIListGroup class]]) {
1645 [titleString appendString:[NSString stringWithFormat:@" (%ld/%ld)",[(AIListGroup *)object visibleCount],[(AIListGroup *)object countOfContainedObjects]]
1646 withAttributes:titleDict];
1649 //Entries from plugins
1651 //Calculate the widest label while loading the arrays
1653 for (tooltipEntry in contactListTooltipEntryArray) {
1655 entryString = [[tooltipEntry entryForObject:object] mutableCopy];
1656 if (entryString && [entryString length]) {
1658 NSString *labelString = [tooltipEntry labelForObject:object];
1659 if (labelString && [labelString length]) {
1661 [entryArray addObject:entryString];
1662 [labelArray addObject:labelString];
1664 NSAttributedString * labelAttribString = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@:",labelString]
1665 attributes:labelDict];
1667 //The largest size should be the label's size plus the distance to the next tab at least a space past its end
1668 labelWidth = [labelAttribString size].width;
1669 [labelAttribString release];
1671 if (labelWidth > maxLabelWidth)
1672 maxLabelWidth = labelWidth;
1675 [entryString release];
1678 //Add labels plus entires to the toolTip
1679 labelEnumerator = [labelArray objectEnumerator];
1681 for (entryString in entryArray) {
1682 NSAttributedString * labelAttribString = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"\t%@:\t",[labelEnumerator nextObject]]
1683 attributes:labelDict];
1685 //Add a carriage return
1686 [titleString appendString:@"\n" withAttributes:labelEndLineDict];
1690 [titleString appendString:@"\n" withAttributes:labelEndLineDict];
1694 //Add the label (with its spacing)
1695 [titleString appendAttributedString:labelAttribString];
1696 [labelAttribString release];
1698 [entryString addAttributes:entryDict range:NSMakeRange(0,[entryString length])];
1699 [titleString appendAttributedString:entryString];
1702 return [titleString autorelease];
1705 - (NSAttributedString *)_tooltipBodyForObject:(AIListObject *)object
1707 NSMutableAttributedString *tipString = [[NSMutableAttributedString alloc] init];
1709 //Configure fonts and attributes
1710 NSFontManager *fontManager = [NSFontManager sharedFontManager];
1711 NSFont *toolTipsFont = [NSFont toolTipsFontOfSize:10];
1712 NSMutableDictionary *labelDict = [NSMutableDictionary dictionaryWithObject:[fontManager convertFont:[NSFont toolTipsFontOfSize:9] toHaveTrait:NSBoldFontMask]
1713 forKey:NSFontAttributeName];
1714 NSMutableDictionary *labelEndLineDict = [NSMutableDictionary dictionaryWithObject:[NSFont toolTipsFontOfSize:1]
1715 forKey:NSFontAttributeName];
1716 NSMutableDictionary *entryDict = [NSMutableDictionary dictionaryWithObject:toolTipsFont
1717 forKey:NSFontAttributeName];
1719 //Entries from plugins
1720 NSEnumerator *labelEnumerator;
1721 NSMutableArray *labelArray = [NSMutableArray array]; //Array of NSStrings
1722 NSMutableArray *entryArray = [NSMutableArray array]; //Array of NSMutableStrings
1724 BOOL firstEntry = YES;
1726 //Calculate the widest label while loading the arrays
1727 for (id <AIContactListTooltipEntry>tooltipEntry in contactListTooltipSecondaryEntryArray) {
1728 NSMutableAttributedString *entryString = [[tooltipEntry entryForObject:object] mutableCopy];
1729 if (entryString && entryString.length) {
1730 NSString *labelString = [tooltipEntry labelForObject:object];
1732 if (labelString && labelString.length) {
1733 [entryArray addObject:entryString];
1734 [labelArray addObject:labelString];
1736 NSAttributedString *labelAttribString = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@:",labelString]
1737 attributes:labelDict];
1739 //The largest size should be the label's size plus the distance to the next tab at least a space past its end
1740 labelWidth = labelAttribString.size.width;
1741 [labelAttribString release];
1743 if (labelWidth > maxLabelWidth)
1744 maxLabelWidth = labelWidth;
1747 [entryString release];
1750 //Add labels plus entires to the toolTip
1751 labelEnumerator = [labelArray objectEnumerator];
1752 for (NSMutableAttributedString *entryString in entryArray) {
1753 NSMutableAttributedString *labelString = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"\t%@:\t",[labelEnumerator nextObject]]
1754 attributes:labelDict];
1759 //Add a carriage return and skip a line
1760 [tipString appendString:@"\n\n" withAttributes:labelEndLineDict];
1763 //Add the label (with its spacing)
1764 [tipString appendAttributedString:labelString];
1765 [labelString release];
1767 NSRange fullLength = NSMakeRange(0, [entryString length]);
1769 //remove any background coloration
1770 [entryString removeAttribute:NSBackgroundColorAttributeName range:fullLength];
1772 //adjust foreground colors for the tooltip background
1773 [entryString adjustColorsToShowOnBackground:[NSColor colorWithCalibratedRed:1.000f green:1.000f blue:0.800f alpha:1.0f]];
1775 //headIndent doesn't apply to the first line of a paragraph... so when new lines are in the entry, we need to tab over to the proper location
1776 if ([entryString replaceOccurrencesOfString:@"\r" withString:@"\r\t\t" options:NSLiteralSearch range:fullLength])
1777 fullLength = NSMakeRange(0, [entryString length]);
1778 if ([entryString replaceOccurrencesOfString:@"\n" withString:@"\n\t\t" options:NSLiteralSearch range:fullLength])
1779 fullLength = NSMakeRange(0, [entryString length]);
1781 //Run the entry through the filters and add it to tipString
1782 entryString = [[adium.contentController filterAttributedString:entryString
1783 usingFilterType:AIFilterTooltips
1784 direction:AIFilterIncoming
1785 context:object] mutableCopy];
1787 [entryString addAttributes:entryDict range:NSMakeRange(0,[entryString length])];
1788 [tipString appendAttributedString:entryString];
1789 [entryString release];
1792 return [tipString autorelease];
1795 //Custom pasting ----------------------------------------------------------------------------------------------------
1796 #pragma mark Custom Pasting
1797 //Paste, stripping formatting
1798 - (IBAction)paste:(id)sender
1800 [self _pasteWithPreferredSelector:@selector(pasteAsPlainTextWithTraits:) sender:sender];
1803 //Paste with formatting
1804 - (IBAction)pasteAndMatchStyle:(id)sender
1806 [self _pasteWithPreferredSelector:@selector(pasteAsPlainText:) sender:sender];
1809 - (IBAction)pasteWithImagesAndColors:(id)sender
1811 [self _pasteWithPreferredSelector:@selector(pasteAsRichText:) sender:sender];
1815 * @brief Send a paste message, using preferredSelector if possible and paste: if not
1817 * Walks the responder chain looking for a responder which can handle pasting, skipping instances of
1818 * WebHTMLView. These are skipped because we can control what paste does to WebView (by using a custom subclass) but
1819 * have no control over what the WebHTMLView would do.
1821 * If no responder is found, repeats the process looking for the simpler paste: selector.
1823 - (void)_pasteWithPreferredSelector:(SEL)selector sender:(id)sender
1825 NSWindow *keyWin = [[NSApplication sharedApplication] keyWindow];
1826 NSResponder *responder;
1828 //First, look for a responder which can handle the preferred selector
1829 if (!(responder = [keyWin earliestResponderWhichRespondsToSelector:selector
1830 andIsNotOfClass:NSClassFromString(@"WebHTMLView")])) {
1831 //No responder found. Try again, looking for one which will respond to paste:
1832 selector = @selector(paste:);
1833 responder = [keyWin earliestResponderWhichRespondsToSelector:selector
1834 andIsNotOfClass:NSClassFromString(@"WebHTMLView")];
1837 //Sending pasteAsRichText: to a non rich text NSTextView won't do anything; change it to a generic paste:
1838 if ([responder isKindOfClass:[NSTextView class]] && ![(NSTextView *)responder isRichText]) {
1839 selector = @selector(paste:);
1843 [keyWin makeFirstResponder:responder];
1844 [responder performSelector:selector
1849 //Custom Printing ------------------------------------------------------------------------------------------------------
1850 #pragma mark Custom Printing
1851 - (IBAction)adiumPrint:(id)sender
1853 //Pass the print command to the window, which is responsible for routing it to the correct place or
1854 //creating a view and printing. Adium will not print from a window that does not respond to adiumPrint:
1855 NSWindow *keyWindowController = [[[NSApplication sharedApplication] keyWindow] windowController];
1856 if ([keyWindowController respondsToSelector:@selector(adiumPrint:)]) {
1857 [keyWindowController performSelector:@selector(adiumPrint:)
1862 #pragma mark Preferences Display
1863 - (IBAction)showPreferenceWindow:(id)sender
1865 [adium.preferenceController showPreferenceWindow:sender];
1868 #pragma mark Font Panel
1869 - (IBAction)toggleFontPanel:(id)sender
1871 if ([NSFontPanel sharedFontPanelExists] &&
1872 [[NSFontPanel sharedFontPanel] isVisible]) {
1873 [[NSFontPanel sharedFontPanel] close];
1876 NSFontPanel *fontPanel = [NSFontPanel sharedFontPanel];
1878 if (!fontPanelAccessoryView) {
1879 [NSBundle loadNibNamed:@"FontPanelAccessoryView" owner:self];
1880 [fontPanel setAccessoryView:fontPanelAccessoryView];
1882 [button_fontPanelSetAsDefault setLocalizedString:AILocalizedString(@"Save This Setting As My Default Font", "Appears in the Format > Show Fonts window. You are limited for horizontal space, so try to keep it at most the length of the English string.")];
1885 [fontPanel orderFront:self];
1889 - (IBAction)setFontPanelSettingsAsDefaultFont:(id)sender
1891 NSFont *selectedFont = [[NSFontManager sharedFontManager] selectedFont];
1893 [adium.preferenceController setPreference:[selectedFont stringRepresentation]
1894 forKey:KEY_FORMATTING_FONT
1895 group:PREF_GROUP_FORMATTING];
1897 //We can't get foreground/background color from the font panel so far as I can tell... so we do the best we can.
1898 NSWindow *keyWin = [[NSApplication sharedApplication] keyWindow];
1899 NSResponder *responder = [keyWin firstResponder];
1900 if ([responder isKindOfClass:[NSTextView class]]) {
1901 NSDictionary *typingAttributes = [(NSTextView *)responder typingAttributes];
1902 NSColor *foregroundColor, *backgroundColor;
1904 if ((foregroundColor = [typingAttributes objectForKey:NSForegroundColorAttributeName])) {
1905 [adium.preferenceController setPreference:[foregroundColor stringRepresentation]
1906 forKey:KEY_FORMATTING_TEXT_COLOR
1907 group:PREF_GROUP_FORMATTING];
1910 if ((backgroundColor = [typingAttributes objectForKey:AIBodyColorAttributeName])) {
1911 [adium.preferenceController setPreference:[backgroundColor stringRepresentation]
1912 forKey:KEY_FORMATTING_BACKGROUND_COLOR
1913 group:PREF_GROUP_FORMATTING];
1918 //Custom Dimming menu items --------------------------------------------------------------------------------------------
1919 #pragma mark Custom Dimming menu items
1920 //The standard ones do not dim correctly when unavailable
1921 - (IBAction)toggleFontTrait:(id)sender
1923 NSFontManager *fontManager = [NSFontManager sharedFontManager];
1925 if ([fontManager traitsOfFont:[fontManager selectedFont]] & [sender tag]) {
1926 [fontManager removeFontTrait:sender];
1928 [fontManager addFontTrait:sender];
1932 - (void)toggleToolbarShown:(id)sender
1934 NSWindow *window = [[NSApplication sharedApplication] keyWindow];
1935 [window toggleToolbarShown:sender];
1938 - (void)runToolbarCustomizationPalette:(id)sender
1940 NSWindow *window = [[NSApplication sharedApplication] keyWindow];
1941 [window runToolbarCustomizationPalette:sender];
1944 //Menu item validation
1945 - (BOOL)validateMenuItem:(NSMenuItem *)menuItem
1948 NSWindow *keyWin = [[NSApplication sharedApplication] keyWindow];
1949 NSResponder *responder = [keyWin firstResponder];
1951 if (menuItem == menuItem_bold || menuItem == menuItem_italic) {
1952 NSFont *selectedFont = [[NSFontManager sharedFontManager] selectedFont];
1954 //We must be in a text view, have text on the pasteboard, and have a font that supports bold or italic
1955 if ([responder isKindOfClass:[NSTextView class]]) {
1956 return (menuItem == menuItem_bold ? [selectedFont supportsBold] : [selectedFont supportsItalics]);
1960 } else if (menuItem == menuItem_paste || menuItem == menuItem_pasteAndMatchStyle || menuItem == menuItem_pasteWithImagesAndColors) {
1962 //The user can paste if the pasteboard contains an image, some text, one or more files, or one or more URLs.
1963 NSPasteboard *pboard = [NSPasteboard generalPasteboard];
1964 NSArray *nonImageTypes = [NSArray arrayWithObjects:
1968 NSFilenamesPboardType,
1969 NSFilesPromisePboardType,
1972 return ([pboard availableTypeFromArray:nonImageTypes] != nil) || [NSImage canInitWithPasteboard:pboard];
1974 } else if (menuItem == menuItem_showToolbar) {
1975 [menuItem_showToolbar setTitle:([[keyWin toolbar] isVisible] ?
1976 AILocalizedString(@"Hide Toolbar",nil) :
1977 AILocalizedString(@"Show Toolbar",nil))];
1978 return [keyWin toolbar] != nil;
1980 } else if (menuItem == menuItem_customizeToolbar) {
1981 return ([keyWin toolbar] != nil && [[keyWin toolbar] isVisible] && [[keyWin windowController] canCustomizeToolbar]);
1983 } else if (menuItem == menuItem_close) {
1984 return (keyWin && ([[keyWin standardWindowButton:NSWindowCloseButton] isEnabled] ||
1985 ([[keyWin windowController] respondsToSelector:@selector(windowPermitsClose)] &&
1986 [[keyWin windowController] windowPermitsClose])));
1988 } else if (menuItem == menuItem_closeChat || menuItem == menuItem_clearDisplay) {
1989 return activeChat != nil;
1991 } else if( menuItem == menuItem_closeAllChats) {
1992 return [[self openChats] count] > 0;
1994 } else if (menuItem == menuItem_print) {
1995 NSWindowController *windowController = [keyWin windowController];
1997 return ([windowController respondsToSelector:@selector(adiumPrint:)] &&
1998 (![windowController respondsToSelector:@selector(validatePrintMenuItem:)] ||
1999 [windowController validatePrintMenuItem:menuItem]));
2001 } else if (menuItem == menuItem_showFonts) {
2002 [menuItem_showFonts setTitle:(([NSFontPanel sharedFontPanelExists] && [[NSFontPanel sharedFontPanel] isVisible]) ?
2003 AILocalizedString(@"Hide Fonts",nil) :
2004 AILocalizedString(@"Show Fonts",nil))];
2006 } else if (menuItem == menuItem_toggleUserlist || menuItem == menuItem_toggleUserlistSide) {
2007 return self.activeChat.isGroupChat;
2008 } else if (menuItem == menuItem_reopenTab) {
2009 return recentlyClosedChats.count > 0;
2015 #pragma mark Window levels
2016 - (NSMenu *)menuForWindowLevelsNotifyingTarget:(id)target
2018 NSMenu *windowPositionMenu = [[NSMenu allocWithZone:[NSMenu zone]] init];
2019 NSMenuItem *menuItem;
2021 menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Above other windows",nil)
2023 action:@selector(selectedWindowLevel:)
2025 [menuItem setEnabled:YES];
2026 [menuItem setTag:AIFloatingWindowLevel];
2027 [windowPositionMenu addItem:menuItem];
2030 menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Normally",nil)
2032 action:@selector(selectedWindowLevel:)
2034 [menuItem setEnabled:YES];
2035 [menuItem setTag:AINormalWindowLevel];
2036 [windowPositionMenu addItem:menuItem];
2039 menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Below other windows",nil)
2041 action:@selector(selectedWindowLevel:)
2043 [menuItem setEnabled:YES];
2044 [menuItem setTag:AIDesktopWindowLevel];
2045 [windowPositionMenu addItem:menuItem];
2048 [windowPositionMenu setAutoenablesItems:NO];
2050 return [windowPositionMenu autorelease];
2053 -(void)toggleUserlist:(id)sender
2055 [self.activeChat.chatContainer.chatViewController toggleUserList];
2058 -(void)toggleUserlistSide:(id)sender
2060 [self.activeChat.chatContainer.chatViewController toggleUserListSide];
2063 -(void)clearDisplay:(id)sender
2065 [self.activeChat.chatContainer.messageViewController.messageDisplayController clearView];