Grab the "Authorization Requests" localized string from the right bundle. Refs #13275.
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>
49 #import "AIMessageViewController.h"
51 #define ERROR_MESSAGE_WINDOW_TITLE AILocalizedString(@"Adium : Error","Error message window title")
52 #define LABEL_ENTRY_SPACING 4.0
53 #define DISPLAY_IMAGE_ON_RIGHT NO
55 #define PREF_GROUP_FORMATTING @"Formatting"
56 #define KEY_FORMATTING_FONT @"Default Font"
58 #define MESSAGES_WINDOW_MENU_TITLE AILocalizedString(@"Chats","Title for the messages window menu item")
60 //#define LOG_RESPONDER_CHAIN
62 @interface AIInterfaceController ()
63 - (void)_resetOpenChatsCache;
64 - (void)_addItemToMainMenuAndDock:(NSMenuItem *)item;
65 - (NSAttributedString *)_tooltipTitleForObject:(AIListObject *)object;
66 - (NSAttributedString *)_tooltipBodyForObject:(AIListObject *)object;
67 - (void)_pasteWithPreferredSelector:(SEL)preferredSelector sender:(id)sender;
68 - (void)updateCloseMenuKeys;
70 - (void)saveContainers;
71 - (void)restoreSavedContainers;
74 - (void)updateActiveWindowMenuItem;
75 - (void)buildWindowMenu;
77 - (AIChat *)mostRecentActiveChat;
81 * @class AIInterfaceController
82 * @brief Interface controller
84 * Chat window related requests, such as opening and closing chats, are routed through the interface controller
85 * to the appropriate component. The interface controller keeps track of the most recently active chat, handles chat
86 * cycling (switching between chats), chat sorting, and so on. The interface controller also handles switching to
87 * an appropriate window or chat when the dock icon is clicked for a 'reopen' event.
89 * Contact list window requests, such as toggling window visibilty are routed to the contact list controller component.
91 * Error messages are routed through the interface controller.
93 * Tooltips, such as seen on hover in the contact list are generated and displayed here. Tooltip display components and
94 * plugins register with the interface controller to be queried for contact information when a tooltip is displayed.
96 * When displays in Adium flash, such as in the dock or the contact list for unviewed content, the interface controller
97 * manages keeping the flashing synchronized.
99 * Finally, the interface controller manages many menu items, providing better menu item validation and target routing
100 * than the responder chain alone would do.
102 @implementation AIInterfaceController
106 if ((self = [super init])) {
107 contactListViewArray = [[NSMutableArray alloc] init];
108 messageViewArray = [[NSMutableArray alloc] init];
109 contactListTooltipEntryArray = [[NSMutableArray alloc] init];
110 contactListTooltipSecondaryEntryArray = [[NSMutableArray alloc] init];
111 closeMenuConfiguredForChat = NO;
112 _cachedOpenChats = nil;
113 mostRecentActiveChat = nil;
116 tooltipListObject = nil;
120 flashObserverArray = nil;
124 windowMenuArray = nil;
126 #ifdef LOG_RESPONDER_CHAIN
127 [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(reportResponderChain:) userInfo:nil repeats:YES];
134 #ifdef LOG_RESPONDER_CHAIN
135 //Can be called by a timer to periodically log the responder chain
136 //[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(reportResponderChain:) userInfo:nil repeats:YES];
137 - (void)reportResponderChain:(NSTimer *)inTimer
139 NSMutableString *responderChain = [NSMutableString string];
141 NSWindow *keyWindow = [[NSApplication sharedApplication] keyWindow];
142 #warning 64BIT: Check formatting arguments
143 [responderChain appendFormat:@"%@ (%i): ",keyWindow,[keyWindow respondsToSelector:@selector(print:)]];
145 NSResponder *responder = [keyWindow firstResponder];
147 //First, walk down the responder chain looking for a responder which can handle the preferred selector
149 #warning 64BIT: Check formatting arguments
150 [responderChain appendFormat:@"%@ (%i)",responder,[responder respondsToSelector:@selector(print:)]];
151 responder = [responder nextResponder];
152 if (responder) [responderChain appendString:@" -> "];
155 NSLog(responderChain);
159 - (void)controllerDidLoad
162 [interfacePlugin openInterface];
164 //Open the contact list window
165 [self showContactList:nil];
167 //Userlist show/hide item
168 menuItem_toggleUserlist = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Toggle User List", nil)
170 action:@selector(toggleUserlist:)
172 [menuItem_toggleUserlist setKeyEquivalentModifierMask:(NSCommandKeyMask | NSAlternateKeyMask)];
174 [adium.menuController addMenuItem:menuItem_toggleUserlist toLocation:LOC_Display_General];
176 menuItem_toggleUserlistSide = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Toggle User List Side", nil)
178 action:@selector(toggleUserlistSide:)
181 [adium.menuController addMenuItem:menuItem_toggleUserlistSide toLocation:LOC_Display_General];
183 NSMenuItem *menuItem = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Toggle User List", nil)
185 action:@selector(toggleUserlist:)
186 keyEquivalent:@""] autorelease];
188 [adium.menuController addContextualMenuItem:menuItem toLocation:Context_GroupChat_Action];
191 menuItem_clearDisplay = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Clear Display", nil)
193 action:@selector(clearDisplay:)
195 [adium.menuController addMenuItem:menuItem_clearDisplay toLocation:LOC_Display_MessageControl];
197 //Contact list menu item
198 menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Contact List","Name of the window which lists contacts")
200 action:@selector(toggleContactList:)
202 [adium.menuController addMenuItem:menuItem toLocation:LOC_Window_Fixed];
203 [adium.menuController addMenuItem:[[menuItem copy] autorelease] toLocation:LOC_Dock_Status];
206 menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Close Chat","Title for the close chat menu item")
208 action:@selector(closeContextualChat:)
210 [adium.menuController addContextualMenuItem:menuItem toLocation:Context_Tab_Action];
213 // Authorization requests menu item
214 menuItem = [[NSMenuItem alloc] initWithTitle:AILocalizedStringFromTableInBundle(@"Authorization Requests",nil, [NSBundle bundleForClass:[AIAuthorizationRequestsWindowController class]], nil)
216 action:@selector(openAuthorizationWindow:)
219 [adium.menuController addMenuItem:menuItem toLocation:LOC_Window_Auxiliary];
222 //Observe content so we can open chats as necessary
223 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveContent:)
224 name:CONTENT_MESSAGE_RECEIVED object:nil];
225 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveContent:)
226 name:CONTENT_MESSAGE_RECEIVED_GROUP object:nil];
228 //Observe Adium finishing loading so we can do things which may require other components or plugins
229 [[NSNotificationCenter defaultCenter] addObserver:self
230 selector:@selector(adiumDidFinishLoading:)
231 name:AIApplicationDidFinishLoadingNotification
234 //Observe quits so we can save containers.
235 [[NSNotificationCenter defaultCenter] addObserver:self
236 selector:@selector(saveContainersOnQuit:)
237 name:AIAppWillTerminateNotification
241 - (void)controllerWillClose
243 [contactListPlugin closeContactList];
244 [interfacePlugin closeInterface];
250 [contactListViewArray release]; contactListViewArray = nil;
251 [messageViewArray release]; messageViewArray = nil;
252 [interfaceArray release]; interfaceArray = nil;
254 [tooltipListObject release]; tooltipListObject = nil;
255 [tooltipTitle release]; tooltipTitle = nil;
256 [tooltipBody release]; tooltipBody = nil;
257 [tooltipImage release]; tooltipImage = nil;
259 [[NSNotificationCenter defaultCenter] removeObserver:self];
260 [adium.preferenceController unregisterPreferenceObserver:self];
265 - (void)adiumDidFinishLoading:(NSNotification *)inNotification
267 //Observe preference changes. This will also restore saved containers if appropriate.
268 [adium.preferenceController registerPreferenceObserver:self forGroup:PREF_GROUP_INTERFACE];
270 [[NSNotificationCenter defaultCenter] removeObserver:self
271 name:AIApplicationDidFinishLoadingNotification
275 //Registers code to handle the interface
276 - (void)registerInterfaceController:(id <AIInterfaceComponent>)inController
278 if (!interfacePlugin) interfacePlugin = [inController retain];
281 //Register code to handle the contact list
282 - (void)registerContactListController:(id <AIContactListComponent>)inController
284 if (!contactListPlugin) contactListPlugin = [inController retain];
287 //Preferences changed
288 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key
289 object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
293 tabbedChatting = [[prefDict objectForKey:KEY_TABBED_CHATTING] boolValue];
294 groupChatsByContactGroup = [[prefDict objectForKey:KEY_GROUP_CHATS_BY_GROUP] boolValue];
295 saveContainers = [[prefDict objectForKey:KEY_SAVE_CONTAINERS] boolValue];
298 if (saveContainers) {
299 //Restore saved containers
300 [self restoreSavedContainers];
301 } else if ([prefDict objectForKey:KEY_CONTAINERS]) {
302 /* We've loaded without wanting to save containers; clear any saved
303 * from a previous session.
305 [adium.preferenceController setPreference:nil
306 forKey:KEY_CONTAINERS
307 group:PREF_GROUP_INTERFACE];
313 //Handle a reopen/dock icon click
314 - (BOOL)handleReopenWithVisibleWindows:(BOOL)visibleWindows
316 if (![self contactListIsVisibleAndMain] && [[interfacePlugin openContainerIDs] count] == 0) {
317 //The contact list is not visible, and there are no chat windows. Make the contact list visible.
318 [self showContactList:nil];
321 AIChat *mostRecentUnviewedChat;
323 //If windows are open, try switching to a chat with unviewed content
324 if ((mostRecentUnviewedChat = [adium.chatController mostRecentUnviewedChat])) {
325 if ([mostRecentActiveChat unviewedContentCount]) {
326 //If the most recently active chat has unviewed content, ensure it is in the front
327 [self setActiveChat:mostRecentActiveChat];
329 //Otherwise, switch to the chat which most recently received content
330 [self setActiveChat:mostRecentUnviewedChat];
334 NSWindow *targetWindow = nil;
335 BOOL unMinimizedWindows = 0;
337 //If there was no unviewed content, ensure that atleast one of Adium's windows is unminimized
338 for (NSWindow *window in [NSApp windows]) {
339 //Check stylemask to rule out the system menu's window (Which reports itself as visible like a real window)
340 if (([window styleMask] & (NSTitledWindowMask | NSClosableWindowMask | NSMiniaturizableWindowMask))) {
341 if (!targetWindow) targetWindow = window;
342 if (![window isMiniaturized]) unMinimizedWindows++;
346 //If there are no unminimized windows, unminimize the last one
347 if (unMinimizedWindows == 0 && targetWindow) {
348 [targetWindow deminiaturize:nil];
356 //Contact List ---------------------------------------------------------------------------------------------------------
357 #pragma mark Contact list
359 * @brief Toggles contact list between visible and hiden
361 - (IBAction)toggleContactList:(id)sender
363 if ([self contactListIsVisibleAndMain]) {
364 [self closeContactList:nil];
366 [[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
367 [self showContactList:nil];
372 * @brief Brings contact list to the front
374 - (IBAction)showContactList:(id)sender
376 [contactListPlugin showContactListAndBringToFront:YES];
380 * @brief Show the contact list window and bring Adium to the front
382 - (IBAction)showContactListAndBringToFront:(id)sender
384 [contactListPlugin showContactListAndBringToFront:YES];
385 [[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
389 * @brief Close the contact list window
391 - (IBAction)closeContactList:(id)sender
393 [contactListPlugin closeContactList];
397 * @returns YES if contact list is visible and selected, otherwise NO
399 - (BOOL)contactListIsVisibleAndMain
401 return [contactListPlugin contactListIsVisibleAndMain];
405 * @returns YES if contact list is visible, otherwise NO
407 - (BOOL)contactListIsVisible
409 return [contactListPlugin contactListIsVisible];
412 //Detachable Contact List ----------------------------------------------------------------------------------------------
413 #pragma mark Detachable Contact List
416 * @returns Created contact list controller for detached contact list
418 - (AIListWindowController *)detachContactList:(AIContactList *)aContactList
420 return [contactListPlugin detachContactList:aContactList];
424 #pragma mark Container Saving
426 * @brief Restores containers saved from a previous session
428 - (void)restoreSavedContainers
430 NSData *savedData = [adium.preferenceController preferenceForKey:KEY_CONTAINERS
431 group:PREF_GROUP_INTERFACE];
433 // If there's no data, we can't restore anything.
437 for (NSDictionary *dict in [NSKeyedUnarchiver unarchiveObjectWithData:savedData]) {
438 AIMessageWindowController *windowController = [self openContainerWithID:[dict objectForKey:@"ID"]
439 name:[dict objectForKey:@"Name"]];
440 AIChat *containerActiveChat = nil;
442 // Position the container where it was last saved (using -savedFrameFromString: to prevent going offscreen)
443 [[windowController window] setFrame:[windowController savedFrameFromString:[dict objectForKey:@"Frame"]] display:YES];
445 for (NSDictionary *chatDict in [dict objectForKey:@"Content"]) {
447 AIService *service = [adium.accountController firstServiceWithServiceID:[chatDict objectForKey:@"serviceID"]];
448 AIAccount *account = [adium.accountController accountWithInternalObjectID:[chatDict objectForKey:@"AccountID"]];
450 if ([[chatDict objectForKey:@"IsGroupChat"] boolValue]) {
451 chat = [adium.chatController chatWithName:[chatDict objectForKey:@"Name"]
454 chatCreationInfo:[chatDict objectForKey:@"ChatCreationInfo"]];
456 AIListContact *contact = [adium.contactController contactWithService:service
458 UID:[chatDict objectForKey:@"UID"]];
460 chat = [adium.chatController chatWithContact:contact];
463 // Tag the chat as restored.
464 [chat setValue:[NSNumber numberWithBool:YES]
465 forProperty:@"Restored Chat"
468 if ([[chatDict objectForKey:@"ActiveChat"] boolValue]) {
469 containerActiveChat = chat;
472 // Open the chat into the container we've created above.
473 [self openChat:chat inContainerWithID:[dict objectForKey:@"ID"] atIndex:-1];
476 if (containerActiveChat)
477 [self setActiveChat:containerActiveChat];
482 * @brief Saves open container information with their content when Adium quits
484 - (void)saveContainersOnQuit:(NSNotification *)notification
486 [self saveContainers];
490 * @brief Save opened containers and windows
492 * @param withContent Save the current buffer of the window to restore at a later point
494 - (void)saveContainers
496 if (!saveContainers) {
497 // Don't save anything if we're not set to.
501 // Save active containers.
502 NSMutableArray *savedContainers = [NSMutableArray array];
504 for (NSDictionary *dict in [interfacePlugin openContainersAndChats]) {
505 NSMutableArray *containerContents = [NSMutableArray array];
507 for (AIChat *chat in [dict objectForKey:@"Content"]) {
508 NSMutableDictionary *newContainerDict = [NSMutableDictionary dictionary];
510 [newContainerDict setObject:chat.account.internalObjectID forKey:@"AccountID"];
512 // Save chat-specific information.
513 if (chat.isGroupChat) {
514 // -chatCreationDictionary may be nil, so put it last.
515 [newContainerDict addEntriesFromDictionary:[NSDictionary dictionaryWithObjectsAndKeys:
516 [NSNumber numberWithBool:YES], @"IsGroupChat",
517 [NSNumber numberWithBool:([dict objectForKey:@"ActiveChat"] == chat)], @"ActiveChat",
519 [chat chatCreationDictionary], @"ChatCreationInfo",nil]];
521 [newContainerDict addEntriesFromDictionary:[NSDictionary dictionaryWithObjectsAndKeys:
522 [NSNumber numberWithBool:([dict objectForKey:@"ActiveChat"] == chat)], @"ActiveChat",
523 chat.listObject.UID, @"UID",
524 chat.account.service.serviceID, @"serviceID",
525 chat.account.internalObjectID, @"AccountID",nil]];
528 [containerContents addObject:newContainerDict];
531 // Replace the "Content" key in -openContainersAndChats with our version of the content.
532 // Remove the ActiveChat reference
533 // We use the same keys otherwise that -openContainersAndChats provides (Name, ID, Frame)
534 NSMutableDictionary *saveDict = [[dict mutableCopy] autorelease];
536 [saveDict removeObjectForKey:@"ActiveChat"];
538 [saveDict setObject:containerContents
541 [savedContainers addObject:saveDict];
544 [adium.preferenceController setPreference:[NSKeyedArchiver archivedDataWithRootObject:savedContainers]
545 forKey:KEY_CONTAINERS
546 group:PREF_GROUP_INTERFACE];
549 //Messaging ------------------------------------------------------------------------------------------------------------
550 //Methods for instructing the interface to provide a representation of chats, and to determine which chat has user focus
551 #pragma mark Messaging
554 * @brief Opens window for chat
556 - (void)openChat:(AIChat *)inChat
558 NSArray *containerIDs = [interfacePlugin openContainerIDs];
559 NSString *containerID = nil;
560 NSString *containerName = nil;
562 //Determine the correct container for this chat
564 if (!tabbedChatting) {
565 //We're not using tabs; each chat starts in its own container, based on the destination object or the chat name
566 if ([inChat listObject]) {
567 containerID = inChat.listObject.internalObjectID;
569 containerID = inChat.name;
572 } else if (groupChatsByContactGroup) {
573 if (inChat.isGroupChat) {
574 containerID = AILocalizedString(@"Group Chats",nil);
577 //XXX multiple containers: this is "correct" but maybe not desirable, as it is non-deterministic
578 AIListGroup *group = inChat.listObject.parentContact.groups.anyObject;
580 //If the contact is in the contact list root, we don't have a group
581 if (group && ![group isKindOfClass:[AIContactList class]]) {
582 containerID = group.displayName;
586 containerName = containerID;
590 //Open new chats into the first container (if not available, create a new one)
591 if ([containerIDs count] > 0) {
592 containerID = [containerIDs objectAtIndex:0];
598 //Determine the correct placement for this chat within the container
599 [interfacePlugin openChat:inChat inContainerWithID:containerID withName:containerName atIndex:-1];
600 if (![inChat isOpen]) {
601 [inChat setIsOpen:YES];
603 //Post the notification last, so observers receive a chat whose isOpen flag is yes.
604 [[NSNotificationCenter defaultCenter] postNotificationName:Chat_DidOpen object:inChat userInfo:nil];
608 - (id)openChat:(AIChat *)inChat inContainerWithID:(NSString *)containerID atIndex:(NSUInteger)index
610 NSArray *openContainerIDs = [interfacePlugin openContainerIDs];
613 //Open new chats into the first container (if not available, create a new one)
614 if ([openContainerIDs count] > 0) {
615 containerID = [openContainerIDs objectAtIndex:0];
617 containerID = AILocalizedString(@"Chats",nil);
621 //Determine the correct placement for this chat within the container
622 id tabViewItem = [interfacePlugin openChat:inChat inContainerWithID:containerID withName:nil atIndex:index];
623 if (![inChat isOpen]) {
624 [inChat setIsOpen:YES];
626 //Post the notification last, so observers receive a chat whose isOpen flag is yes.
627 [[NSNotificationCenter defaultCenter] postNotificationName:Chat_DidOpen object:inChat userInfo:nil];
633 * @brief Opens a container with a specific ID
635 * Asks the interfacePlugin to openContainerWithID:
637 - (AIMessageWindowController *)openContainerWithID:(NSString *)containerID name:(NSString *)containerName
639 return [interfacePlugin openContainerWithID:containerID name:containerName];
643 * @brief Close the interface for a chat
645 * Tell the interface plugin to close the chat.
647 - (void)closeChat:(AIChat *)inChat
650 if ([adium.chatController closeChat:inChat]) {
651 [interfacePlugin closeChat:inChat];
657 * @brief Consolidate chats into a single container
659 - (void)consolidateChats
661 //We work with copies of these arrays, since moving chats may change their contents
662 NSArray *openContainerIDs = [[interfacePlugin openContainerIDs] copy];
663 NSEnumerator *containerEnumerator = [openContainerIDs objectEnumerator];
664 NSString *firstContainerID = [containerEnumerator nextObject];
665 NSString *containerID;
667 //For all containers but the first, move the chats they contain to the first container
668 while ((containerID = [containerEnumerator nextObject])) {
669 NSArray *openChats = [[interfacePlugin openChatsInContainerWithID:containerID] copy];
670 NSEnumerator *chatEnumerator = [openChats objectEnumerator];
673 //Move all the chats, providing a target index if chat sorting is enabled
674 while ((chat = [chatEnumerator nextObject])) {
675 [interfacePlugin moveChat:chat
676 toContainerWithID:firstContainerID
683 [self chatOrderDidChange];
685 [openContainerIDs release];
688 - (void)moveChatToNewContainer:(AIChat *)inChat
690 [interfacePlugin moveChatToNewContainer:inChat];
694 * @returns Active chat
696 - (AIChat *)activeChat
702 * @brief Set the active chat window
704 - (void)setActiveChat:(AIChat *)inChat
706 [interfacePlugin setActiveChat:inChat];
710 * @returns Last chat to be active, nil if not chat is open
712 - (AIChat *)mostRecentActiveChat
714 return mostRecentActiveChat;
718 * @brief Sets active chat window based on chat
720 - (void)setMostRecentActiveChat:(AIChat *)inChat
722 [self setActiveChat:inChat];
726 * @returns Array of open chats (cached, so call as frequently as desired)
728 - (NSArray *)openChats
730 if (!_cachedOpenChats) {
731 _cachedOpenChats = [[interfacePlugin openChats] retain];
734 return _cachedOpenChats;
737 - (NSArray *)openContainerIDs
739 return [interfacePlugin openContainerIDs];
743 * @param containerID ID for chat window
745 * @returns Array of all chats in chat window
747 - (NSArray *)openChatsInContainerWithID:(NSString *)containerID
749 return [interfacePlugin openChatsInContainerWithID:containerID];
753 * @brief Resets the cache of open chats
755 - (void)_resetOpenChatsCache
757 [_cachedOpenChats release]; _cachedOpenChats = nil;
762 //Interface plugin callbacks -------------------------------------------------------------------------------------------
763 //These methods are called by the interface to let us know what's going on. We're informed of chats opening, closing,
764 //changing order, etc.
765 #pragma mark Interface plugin callbacks
767 * @brief A chat window did open: rebuild our window menu to show the new chat
769 * This should be called by the interface plugin (e.g. AIDualWindowInterfacePlugin) after a chat opens
771 * @param inChat Newly created chat
773 - (void)chatDidOpen:(AIChat *)inChat
775 [self _resetOpenChatsCache];
776 [self buildWindowMenu];
777 [self saveContainers];
781 * @brief A chat has become active: update our chat closing keys and flag this chat as selected in the window menu
783 * @param inChat Chat which has become active
785 - (void)chatDidBecomeActive:(AIChat *)inChat
787 AIChat *previouslyActiveChat = activeChat;
789 activeChat = [inChat retain];
791 [self updateCloseMenuKeys];
792 [self updateActiveWindowMenuItem];
794 if (inChat && (inChat != mostRecentActiveChat)) {
795 [mostRecentActiveChat release]; mostRecentActiveChat = nil;
796 mostRecentActiveChat = [inChat retain];
799 [[NSNotificationCenter defaultCenter] postNotificationName:Chat_BecameActive
801 userInfo:(previouslyActiveChat ?
802 [NSDictionary dictionaryWithObject:previouslyActiveChat
803 forKey:@"PreviouslyActiveChat"] :
807 /* Clear the unviewed content on the next event loop so other methods have a chance to react to the chat becoming
808 * active. Specifically, this lets the handleReopenWithVisibleWindows: method have a chance to know that this chat
809 * had unviewed content.
811 [inChat performSelector:@selector(clearUnviewedContentCount)
816 [previouslyActiveChat release];
820 * @brief A chat has become visible: send out a notification for components and plugins to take action
822 * @param inChat Chat that has become active
823 * @param nWindow Containing chat window
825 - (void)chatDidBecomeVisible:(AIChat *)inChat inWindow:(NSWindow *)inWindow
827 [[NSNotificationCenter defaultCenter] postNotificationName:@"AIChatDidBecomeVisible"
829 userInfo:[NSDictionary dictionaryWithObject:inWindow
830 forKey:@"NSWindow"]];
834 * @brief Find the window currently displaying a chat
836 * @returns Window for chat otherwise if the chat is not in any window, or is not visible in any window, returns nil
838 - (NSWindow *)windowForChat:(AIChat *)inChat
840 return [interfacePlugin windowForChat:inChat];
844 * @brief Find the chat active in a window
846 * If the window does not have an active chat, nil is returned
848 - (AIChat *)activeChatInWindow:(NSWindow *)window
850 return [interfacePlugin activeChatInWindow:window];
854 * @brief A chat window did close: rebuild our window menu to remove the chat
856 * @param inChat Chat that closed
858 - (void)chatDidClose:(AIChat *)inChat
860 [self _resetOpenChatsCache];
861 [inChat clearUnviewedContentCount];
862 [self buildWindowMenu];
864 if (!adium.isQuitting) {
865 // Don't save containers when the chats are closed while quitting
866 [self saveContainers];
869 if (inChat == activeChat) {
870 [activeChat release]; activeChat = nil;
873 if (inChat == mostRecentActiveChat) {
874 [mostRecentActiveChat release]; mostRecentActiveChat = nil;
879 * @brief The order of chats has changed: rebuild our window menu to reflect the new order
881 - (void)chatOrderDidChange
883 [self _resetOpenChatsCache];
884 [self buildWindowMenu];
886 if (!adium.isQuitting) {
887 // Don't save containers when the chats are closed while quitting
888 [self saveContainers];
891 [[NSNotificationCenter defaultCenter] postNotificationName:Chat_OrderDidChange object:nil userInfo:nil];
895 #pragma mark Unviewed content
898 * @breif Content was received, increase the unviewed content count of the chat (if it's not currently active)
900 - (void)didReceiveContent:(NSNotification *)notification
902 AIChat *chat = [[notification userInfo] objectForKey:@"AIChat"];
904 if (chat != activeChat) {
905 [chat incrementUnviewedContentCount];
910 //Chat close menus -----------------------------------------------------------------------------------------------------
911 #pragma mark Chat close menus
914 * @brief Closes currently active window
916 - (IBAction)closeMenu:(id)sender
918 [[[NSApplication sharedApplication] keyWindow] performClose:nil];
922 * @brief Closes currently active chat (if there is an active chat)
924 - (IBAction)closeChatMenu:(id)sender
926 if (activeChat) [self closeChat:activeChat];
930 * @brief Closes currently selected chat based on current chat contextual menu
932 - (IBAction)closeContextualChat:(id)sender
934 [self closeChat:[adium.menuController currentContextMenuChat]];
938 * @brief Loop through open chats and close them
940 - (IBAction)closeAllChats:(id)sender
942 for (AIChat *chatToClose in [[interfacePlugin.openChats copy] autorelease]) {
943 [self closeChat:chatToClose];
948 * @brief Updates the key equivalents on 'close' and 'close chat' (dynamically changed to make cmd-w less destructive)
950 - (void)updateCloseMenuKeys
952 if (activeChat && !closeMenuConfiguredForChat) {
953 [menuItem_close setKeyEquivalent:@"W"];
954 [menuItem_closeChat setKeyEquivalent:@"w"];
955 closeMenuConfiguredForChat = YES;
956 } else if (!activeChat && closeMenuConfiguredForChat) {
957 [menuItem_close setKeyEquivalent:@"w"];
958 [menuItem_closeChat removeKeyEquivalent];
959 closeMenuConfiguredForChat = NO;
964 //Window Menu ----------------------------------------------------------------------------------------------------------
965 #pragma mark Window Menu
968 * @brief Open the authorization requests window.
970 - (void)openAuthorizationWindow:(id)sender
972 [[AIAuthorizationRequestsWindowController sharedController] showWindow:nil];
976 * @brief Make a chat window active
978 * Invoked by a selection in the window menu
980 - (IBAction)showChatWindow:(id)sender
982 [self setActiveChat:[sender representedObject]];
983 [[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
987 * @brief Updates the 'check' icon so it's next to the active window
989 - (void)updateActiveWindowMenuItem
993 for (item in windowMenuArray) {
994 if ([item representedObject]) [item setState:([item representedObject] == activeChat ? NSOnState : NSOffState)];
999 * @brief Builds the window menu
1001 * This function gets called whenever chats are opened, closed, or re-ordered - so improvements and optimizations here
1002 * would probably be helpful
1004 - (void)buildWindowMenu
1007 NSInteger windowKey = 1;
1009 //Remove any existing menus
1010 for (item in windowMenuArray) {
1011 [adium.menuController removeMenuItem:item];
1013 [windowMenuArray release]; windowMenuArray = [[NSMutableArray alloc] init];
1015 //Messages window and any open messasges
1016 for (NSDictionary *containerDict in [interfacePlugin openContainersAndChats]) {
1017 NSString *containerName = [containerDict objectForKey:@"Name"];
1018 NSArray *contentArray = [containerDict objectForKey:@"Content"];
1020 //Add a menu item for the container
1021 if (contentArray.count > 1) {
1022 item = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:([containerName length] ? containerName : AILocalizedString(@"Chats", nil))
1026 [self _addItemToMainMenuAndDock:item];
1030 //Add items for the chats it contains
1031 for (AIChat *chat in [contentArray objectEnumerator]) {
1032 NSString *windowKeyString;
1034 //Prepare a key equivalent for the controller
1035 if (windowKey < 10) {
1036 windowKeyString = [NSString stringWithFormat:@"%ld", (windowKey)];
1037 } else if (windowKey == 10) {
1038 windowKeyString = [NSString stringWithString:@"0"];
1040 windowKeyString = [NSString stringWithString:@""];
1043 item = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:chat.displayName
1045 action:@selector(showChatWindow:)
1046 keyEquivalent:windowKeyString];
1047 if ([contentArray count] > 1) [item setIndentationLevel:1];
1048 [item setRepresentedObject:chat];
1049 [item setImage:chat.chatMenuImage];
1050 [self _addItemToMainMenuAndDock:item];
1057 [self updateActiveWindowMenuItem];
1061 * brief Adds a menu item to the internal array, dock menu, and main menu
1063 * Should be used for adding a new window to the window menu (and dock menu)
1065 - (void)_addItemToMainMenuAndDock:(NSMenuItem *)item
1067 //Add to main menu first
1068 [adium.menuController addMenuItem:item toLocation:LOC_Window_Fixed];
1069 [windowMenuArray addObject:item];
1071 //Make a copy, and add to the dock
1073 [item setKeyEquivalent:@""];
1074 [adium.menuController addMenuItem:item toLocation:LOC_Dock_Status];
1075 [windowMenuArray addObject:item];
1080 //Chat Cycling ---------------------------------------------------------------------------------------------------------
1081 #pragma mark Chat Cycling
1084 * @brief Cycles to the next active chat
1086 - (void)nextChat:(id)sender
1088 NSArray *openChats = [self openChats];
1090 if ([openChats count]) {
1092 NSInteger chatIndex = [openChats indexOfObject:activeChat]+1;
1093 [self setActiveChat:[openChats objectAtIndex:(chatIndex < [openChats count] ? chatIndex : 0)]];
1095 [self setActiveChat:[openChats objectAtIndex:0]];
1101 * @brief Cycles to the previus active chat
1103 - (void)previousChat:(id)sender
1105 NSArray *openChats = [self openChats];
1107 if ([openChats count]) {
1109 NSInteger chatIndex = [openChats indexOfObject:activeChat]-1;
1110 [self setActiveChat:[openChats objectAtIndex:(chatIndex >= 0 ? chatIndex : [openChats count]-1)]];
1112 [self setActiveChat:[openChats lastObject]];
1117 //Selected contact ------------------------------------------------
1118 #pragma mark Selected contact
1119 - (id)_performSelectorOnFirstAvailableResponder:(SEL)selector
1121 NSResponder *responder = [[[NSApplication sharedApplication] mainWindow] firstResponder];
1122 //Check the first responder
1123 if ([responder respondsToSelector:selector]) {
1124 return [responder performSelector:selector];
1127 //Search the responder chain
1129 responder = [responder nextResponder];
1130 if ([responder respondsToSelector:selector]) {
1131 return [responder performSelector:selector];
1134 } while (responder != nil);
1136 //None found, return nil
1139 - (id)_performSelectorOnFirstAvailableResponder:(SEL)selector conformingToProtocol:(Protocol *)protocol
1141 NSResponder *responder = [[[NSApplication sharedApplication] mainWindow] firstResponder];
1142 //Check the first responder
1143 if ([responder conformsToProtocol:protocol] && [responder respondsToSelector:selector]) {
1144 return [responder performSelector:selector];
1147 //Search the responder chain
1149 responder = [responder nextResponder];
1150 if ([responder conformsToProtocol:protocol] && [responder respondsToSelector:selector]) {
1151 return [responder performSelector:selector];
1154 } while (responder != nil);
1156 //None found, return nil
1161 * @returns The "selected"(represented) contact (By finding the first responder that returns a contact)
1162 * If no listObject is found, try to find a list object selected in a group chat
1164 - (AIListObject *)selectedListObject
1166 AIListObject *listObject = [self _performSelectorOnFirstAvailableResponder:@selector(listObject)];
1168 listObject = [self _performSelectorOnFirstAvailableResponder:@selector(preferredListObject)];
1173 - (AIListObject *)selectedListObjectInContactList
1175 return [self _performSelectorOnFirstAvailableResponder:@selector(listObject) conformingToProtocol:@protocol(ContactListOutlineView)];
1177 - (NSArray *)arrayOfSelectedListObjectsInContactList
1179 return [self _performSelectorOnFirstAvailableResponder:@selector(arrayOfListObjects) conformingToProtocol:@protocol(ContactListOutlineView)];
1181 - (NSArray *)arrayOfSelectedListObjectsWithGroupsInContactList
1183 return [self _performSelectorOnFirstAvailableResponder:@selector(arrayOfListObjectsWithGroups) conformingToProtocol:@protocol(ContactListOutlineView)];
1186 //Message View ---------------------------------------------------------------------------------------------------------
1187 //Message view is abstracted from the containing interface, since they're not directly related to eachother
1188 #pragma mark Message View
1189 //Registers a view to handle the contact list
1190 - (void)registerMessageDisplayPlugin:(id <AIMessageDisplayPlugin>)inPlugin
1192 [messageViewArray addObject:inPlugin];
1194 - (void)unregisterMessageDisplayPlugin:(id <AIMessageDisplayPlugin>)inPlugin
1196 [messageViewArray removeObject:inPlugin];
1198 - (id <AIMessageDisplayController>)messageDisplayControllerForChat:(AIChat *)inChat
1200 //Sometimes our users find it amusing to disable plugins that are located within the Adium bundle. This error
1201 //trap prevents us from crashing if they happen to disable all the available message view plugins.
1202 //PUT THAT PLUGIN BACK IT WAS IMPORTANT!
1203 if ([messageViewArray count] == 0) {
1204 NSRunCriticalAlertPanel(@"No Message View Plugin Installed",
1205 @"Adium cannot find its message view plugin. Please re-install. If you've manually disabled Adium's message view plugin, please re-enable it.",
1209 [NSApp terminate:nil];
1212 return [[messageViewArray objectAtIndex:0] messageDisplayControllerForChat:inChat];
1216 //Error Display --------------------------------------------------------------------------------------------------------
1217 #pragma mark Error Display
1218 - (void)handleErrorMessage:(NSString *)inTitle withDescription:(NSString *)inDesc
1220 [self handleMessage:inTitle withDescription:inDesc withWindowTitle:ERROR_MESSAGE_WINDOW_TITLE];
1223 - (void)handleMessage:(NSString *)inTitle withDescription:(NSString *)inDesc withWindowTitle:(NSString *)inWindowTitle;
1225 NSDictionary *errorDict;
1227 //Post a notification that an error was recieved
1228 errorDict = [NSDictionary dictionaryWithObjectsAndKeys:inTitle,@"Title",inDesc,@"Description",inWindowTitle,@"Window Title",nil];
1229 [[NSNotificationCenter defaultCenter] postNotificationName:Interface_ShouldDisplayErrorMessage object:nil userInfo:errorDict];
1232 //Display then clear the last disconnection error
1233 - (void)account:(AIAccount *)inAccount disconnectedWithError:(NSString *)disconnectionError
1238 //Question Display -----------------------------------------------------------------------------------------------------
1239 #pragma mark Question Display
1240 - (void)displayQuestion:(NSString *)inTitle withAttributedDescription:(NSAttributedString *)inDesc withWindowTitle:(NSString *)inWindowTitle
1241 defaultButton:(NSString *)inDefaultButton alternateButton:(NSString *)inAlternateButton otherButton:(NSString *)inOtherButton suppression:(NSString *)inSuppression
1242 target:(id)inTarget selector:(SEL)inSelector userInfo:(id)inUserInfo
1244 NSMutableDictionary *questionDict = [NSMutableDictionary dictionary];
1247 [questionDict setObject:inTitle forKey:@"Title"];
1249 [questionDict setObject:inDesc forKey:@"Description"];
1250 if(inWindowTitle != nil)
1251 [questionDict setObject:inWindowTitle forKey:@"Window Title"];
1252 if(inDefaultButton != nil)
1253 [questionDict setObject:inDefaultButton forKey:@"Default Button"];
1254 if(inAlternateButton != nil)
1255 [questionDict setObject:inAlternateButton forKey:@"Alternate Button"];
1256 if(inOtherButton != nil)
1257 [questionDict setObject:inOtherButton forKey:@"Other Button"];
1258 if(inSuppression != nil)
1259 [questionDict setObject:inSuppression forKey:@"Suppression Checkbox"];
1261 [questionDict setObject:inTarget forKey:@"Target"];
1262 if(inSelector != NULL)
1263 [questionDict setObject:NSStringFromSelector(inSelector) forKey:@"Selector"];
1264 if(inUserInfo != nil)
1265 [questionDict setObject:inUserInfo forKey:@"Userinfo"];
1267 [[NSNotificationCenter defaultCenter] postNotificationName:Interface_ShouldDisplayQuestion object:nil userInfo:questionDict];
1270 - (void)displayQuestion:(NSString *)inTitle withDescription:(NSString *)inDesc withWindowTitle:(NSString *)inWindowTitle
1271 defaultButton:(NSString *)inDefaultButton alternateButton:(NSString *)inAlternateButton otherButton:(NSString *)inOtherButton suppression:(NSString *)inSuppression
1272 target:(id)inTarget selector:(SEL)inSelector userInfo:(id)inUserInfo
1274 [self displayQuestion:inTitle
1275 withAttributedDescription:[[[NSAttributedString alloc] initWithString:inDesc
1276 attributes:[NSDictionary dictionaryWithObject:[NSFont systemFontOfSize:0]
1277 forKey:NSFontAttributeName]] autorelease]
1278 withWindowTitle:inWindowTitle
1279 defaultButton:inDefaultButton
1280 alternateButton:inAlternateButton
1281 otherButton:inOtherButton
1282 suppression:inSuppression
1285 userInfo:inUserInfo];
1287 //Synchronized Flashing ------------------------------------------------------------------------------------------------
1288 #pragma mark Synchronized Flashing
1289 //Register to observe the synchronized flashing
1290 - (void)registerFlashObserver:(id <AIFlashObserver>)inObserver
1292 //Setup the timer if we don't have one yet
1293 if (!flashObserverArray) {
1294 flashObserverArray = [[NSMutableArray alloc] init];
1295 flashTimer = [[NSTimer scheduledTimerWithTimeInterval:(1.0/2.0)
1297 selector:@selector(flashTimer:)
1299 repeats:YES] retain];
1302 //Add the new observer to the array
1303 [flashObserverArray addObject:inObserver];
1306 //Unregister from observing flashing
1307 - (void)unregisterFlashObserver:(id <AIFlashObserver>)inObserver
1309 //Remove the observer from our array
1310 [flashObserverArray removeObject:inObserver];
1312 //Release the observer array and uninstall the timer
1313 if ([flashObserverArray count] == 0) {
1314 [flashObserverArray release]; flashObserverArray = nil;
1315 [flashTimer invalidate];
1316 [flashTimer release]; flashTimer = nil;
1320 //Timer, invoke a flash
1321 - (void)flashTimer:(NSTimer *)inTimer
1325 for (id<AIFlashObserver>observer in [[flashObserverArray copy] autorelease]) {
1326 [observer flash:flashState];
1330 //Current state of flashing. This is an integer the increases by 1 with every flash. Mod to whatever range is desired
1337 //Tooltips -------------------------------------------------------------------------------------------------------------
1338 #pragma mark Tooltips
1339 //Registers code to display tooltip info about a contact
1340 - (void)registerContactListTooltipEntry:(id <AIContactListTooltipEntry>)inEntry secondaryEntry:(BOOL)isSecondary
1343 [contactListTooltipSecondaryEntryArray addObject:inEntry];
1345 [contactListTooltipEntryArray addObject:inEntry];
1348 //Unregisters code to display tooltip info about a contact
1349 - (void)unregisterContactListTooltipEntry:(id <AIContactListTooltipEntry>)inEntry secondaryEntry:(BOOL)isSecondary
1352 [contactListTooltipSecondaryEntryArray removeObject:inEntry];
1354 [contactListTooltipEntryArray removeObject:inEntry];
1357 - (NSArray *)contactListTooltipPrimaryEntries
1359 return contactListTooltipEntryArray;
1362 - (NSArray *)contactListTooltipSecondaryEntries
1364 return contactListTooltipSecondaryEntryArray;
1367 //list object tooltips
1368 - (void)showTooltipForListObject:(AIListObject *)object atScreenPoint:(NSPoint)point onWindow:(NSWindow *)inWindow
1371 if (object == tooltipListObject) { //If we already have this tooltip open
1372 //Move the existing tooltip
1373 [AITooltipUtilities showTooltipWithTitle:tooltipTitle
1376 imageOnRight:DISPLAY_IMAGE_ON_RIGHT
1379 orientation:TooltipBelow];
1381 } else { //This is a new tooltip
1383 NSMutableParagraphStyle *paragraphStyleTitle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
1384 NSMutableParagraphStyle *paragraphStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
1386 //Hold onto the new object
1387 [tooltipListObject release]; tooltipListObject = [object retain];
1390 [tooltipImage release];
1391 tooltipImage = [[tooltipListObject userIcon] retain];
1392 if (!tooltipImage) tooltipImage = [[AIServiceIcons serviceIconForObject:tooltipListObject
1393 type:AIServiceIconLarge
1394 direction:AIIconNormal] retain];
1396 //Reset the maxLabelWidth for the tooltip generation
1399 //Build a tooltip string for the primary information
1400 [tooltipTitle release]; tooltipTitle = [[self _tooltipTitleForObject:object] retain];
1402 //If there is an image, set the title tab and indentation settings independently
1404 //Set a right-align tab at the maximum label width and a left-align just past it
1405 tabArray = [[NSArray alloc] initWithObjects:[[[NSTextTab alloc] initWithType:NSRightTabStopType
1406 location:maxLabelWidth] autorelease]
1407 ,[[[NSTextTab alloc] initWithType:NSLeftTabStopType
1408 location:maxLabelWidth + LABEL_ENTRY_SPACING] autorelease]
1411 [paragraphStyleTitle setTabStops:tabArray];
1414 [paragraphStyleTitle setHeadIndent:(maxLabelWidth + LABEL_ENTRY_SPACING)];
1416 [tooltipTitle addAttribute:NSParagraphStyleAttributeName
1417 value:paragraphStyleTitle
1418 range:NSMakeRange(0,[tooltipTitle length])];
1420 //Reset the max label width since the body will be independent
1424 //Build a tooltip string for the secondary information
1425 [tooltipBody release]; tooltipBody = nil;
1426 tooltipBody = [[self _tooltipBodyForObject:object] retain];
1428 //Set a right-align tab at the maximum label width for the body and a left-align just past it
1429 tabArray = [[NSArray alloc] initWithObjects:[[[NSTextTab alloc] initWithType:NSRightTabStopType
1430 location:maxLabelWidth] autorelease]
1431 ,[[[NSTextTab alloc] initWithType:NSLeftTabStopType
1432 location:maxLabelWidth + LABEL_ENTRY_SPACING] autorelease]
1434 [paragraphStyle setTabStops:tabArray];
1436 [paragraphStyle setHeadIndent:(maxLabelWidth + LABEL_ENTRY_SPACING)];
1438 [tooltipBody addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:NSMakeRange(0,[tooltipBody length])];
1439 //If there is no image, also use these settings for the top part
1440 if (!tooltipImage) {
1441 [tooltipTitle addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:NSMakeRange(0,[tooltipTitle length])];
1444 //Display the new tooltip
1445 [AITooltipUtilities showTooltipWithTitle:tooltipTitle
1448 imageOnRight:DISPLAY_IMAGE_ON_RIGHT
1451 orientation:TooltipBelow];
1453 [paragraphStyleTitle release];
1454 [paragraphStyle release];
1458 //Hide the existing tooltip
1459 if (tooltipListObject) {
1460 [AITooltipUtilities showTooltipWithTitle:nil
1465 orientation:TooltipBelow];
1466 [tooltipListObject release]; tooltipListObject = nil;
1468 [tooltipTitle release]; tooltipTitle = nil;
1469 [tooltipBody release]; tooltipBody = nil;
1470 [tooltipImage release]; tooltipImage = nil;
1475 - (NSAttributedString *)_tooltipTitleForObject:(AIListObject *)object
1477 NSMutableAttributedString *titleString = [[NSMutableAttributedString alloc] init];
1479 id <AIContactListTooltipEntry> tooltipEntry;
1480 NSEnumerator *labelEnumerator;
1481 NSMutableArray *labelArray = [NSMutableArray array];
1482 NSMutableArray *entryArray = [NSMutableArray array];
1483 NSMutableAttributedString *entryString;
1487 NSString *formattedUID = object.formattedUID;
1489 //Configure fonts and attributes
1490 NSFontManager *fontManager = [NSFontManager sharedFontManager];
1491 NSFont *toolTipsFont = [NSFont toolTipsFontOfSize:10];
1492 NSMutableDictionary *titleDict = [NSMutableDictionary dictionaryWithObject:[fontManager convertFont:[NSFont toolTipsFontOfSize:12] toHaveTrait:NSBoldFontMask]
1493 forKey:NSFontAttributeName];
1494 NSMutableDictionary *labelDict = [NSMutableDictionary dictionaryWithObject:[fontManager convertFont:[NSFont toolTipsFontOfSize:9] toHaveTrait:NSBoldFontMask]
1495 forKey:NSFontAttributeName];
1496 NSMutableDictionary *labelEndLineDict = [NSMutableDictionary dictionaryWithObject:[NSFont toolTipsFontOfSize:2]
1497 forKey:NSFontAttributeName];
1498 NSMutableDictionary *entryDict = [NSMutableDictionary dictionaryWithObject:toolTipsFont
1499 forKey:NSFontAttributeName];
1501 //Get the user's display name as an attributed string
1502 NSAttributedString *displayName = [[NSAttributedString alloc] initWithString:object.displayName
1503 attributes:titleDict];
1504 NSAttributedString *filteredDisplayName = [adium.contentController filterAttributedString:displayName
1505 usingFilterType:AIFilterTooltips
1506 direction:AIFilterIncoming
1509 //Append the user's display name
1510 if (filteredDisplayName) {
1511 [titleString appendAttributedString:filteredDisplayName];
1514 //Append the user's formatted UID if there is one that's different to the display name
1515 if (formattedUID && (!([[[displayName string] compactedString] isEqualToString:[formattedUID compactedString]]))) {
1516 [titleString appendString:[NSString stringWithFormat:@" (%@)", formattedUID] withAttributes:titleDict];
1518 [displayName release];
1520 if ([object isKindOfClass:[AIListContact class]]) {
1521 if ((![object isKindOfClass:[AIMetaContact class]] || [(AIMetaContact *)object containsOnlyOneService]) &&
1522 [object userIcon]) {
1523 NSImage *serviceIcon = [[AIServiceIcons serviceIconForObject:object type:AIServiceIconSmall direction:AIIconNormal]
1524 imageByScalingToSize:NSMakeSize(14,14)];
1526 NSTextAttachment *attachment;
1527 NSTextAttachmentCell *cell;
1529 cell = [[NSTextAttachmentCell alloc] init];
1530 [cell setImage:serviceIcon];
1532 attachment = [[NSTextAttachment alloc] init];
1533 [attachment setAttachmentCell:cell];
1536 [titleString appendString:@" " withAttributes:nil];
1537 [titleString appendAttributedString:[NSAttributedString attributedStringWithAttachment:attachment]];
1538 [attachment release];
1543 if ([object isKindOfClass:[AIListGroup class]]) {
1544 [titleString appendString:[NSString stringWithFormat:@" (%ld/%ld)",[(AIListGroup *)object visibleCount],[(AIListGroup *)object countOfContainedObjects]]
1545 withAttributes:titleDict];
1548 //Entries from plugins
1550 //Calculate the widest label while loading the arrays
1552 for (tooltipEntry in contactListTooltipEntryArray) {
1554 entryString = [[tooltipEntry entryForObject:object] mutableCopy];
1555 if (entryString && [entryString length]) {
1557 NSString *labelString = [tooltipEntry labelForObject:object];
1558 if (labelString && [labelString length]) {
1560 [entryArray addObject:entryString];
1561 [labelArray addObject:labelString];
1563 NSAttributedString * labelAttribString = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@:",labelString]
1564 attributes:labelDict];
1566 //The largest size should be the label's size plus the distance to the next tab at least a space past its end
1567 labelWidth = [labelAttribString size].width;
1568 [labelAttribString release];
1570 if (labelWidth > maxLabelWidth)
1571 maxLabelWidth = labelWidth;
1574 [entryString release];
1577 //Add labels plus entires to the toolTip
1578 labelEnumerator = [labelArray objectEnumerator];
1580 for (entryString in entryArray) {
1581 NSAttributedString * labelAttribString = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"\t%@:\t",[labelEnumerator nextObject]]
1582 attributes:labelDict];
1584 //Add a carriage return
1585 [titleString appendString:@"\n" withAttributes:labelEndLineDict];
1589 [titleString appendString:@"\n" withAttributes:labelEndLineDict];
1593 //Add the label (with its spacing)
1594 [titleString appendAttributedString:labelAttribString];
1595 [labelAttribString release];
1597 [entryString addAttributes:entryDict range:NSMakeRange(0,[entryString length])];
1598 [titleString appendAttributedString:entryString];
1601 return [titleString autorelease];
1604 - (NSAttributedString *)_tooltipBodyForObject:(AIListObject *)object
1606 NSMutableAttributedString *tipString = [[NSMutableAttributedString alloc] init];
1608 //Configure fonts and attributes
1609 NSFontManager *fontManager = [NSFontManager sharedFontManager];
1610 NSFont *toolTipsFont = [NSFont toolTipsFontOfSize:10];
1611 NSMutableDictionary *labelDict = [NSMutableDictionary dictionaryWithObject:[fontManager convertFont:[NSFont toolTipsFontOfSize:9] toHaveTrait:NSBoldFontMask]
1612 forKey:NSFontAttributeName];
1613 NSMutableDictionary *labelEndLineDict = [NSMutableDictionary dictionaryWithObject:[NSFont toolTipsFontOfSize:1]
1614 forKey:NSFontAttributeName];
1615 NSMutableDictionary *entryDict = [NSMutableDictionary dictionaryWithObject:toolTipsFont
1616 forKey:NSFontAttributeName];
1618 //Entries from plugins
1619 NSEnumerator *labelEnumerator;
1620 NSMutableArray *labelArray = [NSMutableArray array]; //Array of NSStrings
1621 NSMutableArray *entryArray = [NSMutableArray array]; //Array of NSMutableStrings
1623 BOOL firstEntry = YES;
1625 //Calculate the widest label while loading the arrays
1626 for (id <AIContactListTooltipEntry>tooltipEntry in contactListTooltipSecondaryEntryArray) {
1627 NSMutableAttributedString *entryString = [[tooltipEntry entryForObject:object] mutableCopy];
1628 if (entryString && entryString.length) {
1629 NSString *labelString = [tooltipEntry labelForObject:object];
1631 if (labelString && labelString.length) {
1632 [entryArray addObject:entryString];
1633 [labelArray addObject:labelString];
1635 NSAttributedString *labelAttribString = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@:",labelString]
1636 attributes:labelDict];
1638 //The largest size should be the label's size plus the distance to the next tab at least a space past its end
1639 labelWidth = labelAttribString.size.width;
1640 [labelAttribString release];
1642 if (labelWidth > maxLabelWidth)
1643 maxLabelWidth = labelWidth;
1646 [entryString release];
1649 //Add labels plus entires to the toolTip
1650 labelEnumerator = [labelArray objectEnumerator];
1651 for (NSMutableAttributedString *entryString in entryArray) {
1652 NSMutableAttributedString *labelString = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"\t%@:\t",[labelEnumerator nextObject]]
1653 attributes:labelDict];
1658 //Add a carriage return and skip a line
1659 [tipString appendString:@"\n\n" withAttributes:labelEndLineDict];
1662 //Add the label (with its spacing)
1663 [tipString appendAttributedString:labelString];
1664 [labelString release];
1666 NSRange fullLength = NSMakeRange(0, [entryString length]);
1668 //remove any background coloration
1669 [entryString removeAttribute:NSBackgroundColorAttributeName range:fullLength];
1671 //adjust foreground colors for the tooltip background
1672 [entryString adjustColorsToShowOnBackground:[NSColor colorWithCalibratedRed:1.000 green:1.000 blue:0.800 alpha:1.0]];
1674 //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
1675 if ([entryString replaceOccurrencesOfString:@"\r" withString:@"\r\t\t" options:NSLiteralSearch range:fullLength])
1676 fullLength = NSMakeRange(0, [entryString length]);
1677 if ([entryString replaceOccurrencesOfString:@"\n" withString:@"\n\t\t" options:NSLiteralSearch range:fullLength])
1678 fullLength = NSMakeRange(0, [entryString length]);
1680 //Run the entry through the filters and add it to tipString
1681 entryString = [[adium.contentController filterAttributedString:entryString
1682 usingFilterType:AIFilterTooltips
1683 direction:AIFilterIncoming
1684 context:object] mutableCopy];
1686 [entryString addAttributes:entryDict range:NSMakeRange(0,[entryString length])];
1687 [tipString appendAttributedString:entryString];
1688 [entryString release];
1691 return [tipString autorelease];
1694 //Custom pasting ----------------------------------------------------------------------------------------------------
1695 #pragma mark Custom Pasting
1696 //Paste, stripping formatting
1697 - (IBAction)paste:(id)sender
1699 [self _pasteWithPreferredSelector:@selector(pasteAsPlainTextWithTraits:) sender:sender];
1702 //Paste with formatting
1703 - (IBAction)pasteAndMatchStyle:(id)sender
1705 [self _pasteWithPreferredSelector:@selector(pasteAsPlainText:) sender:sender];
1708 - (IBAction)pasteWithImagesAndColors:(id)sender
1710 [self _pasteWithPreferredSelector:@selector(pasteAsRichText:) sender:sender];
1714 * @brief Send a paste message, using preferredSelector if possible and paste: if not
1716 * Walks the responder chain looking for a responder which can handle pasting, skipping instances of
1717 * WebHTMLView. These are skipped because we can control what paste does to WebView (by using a custom subclass) but
1718 * have no control over what the WebHTMLView would do.
1720 * If no responder is found, repeats the process looking for the simpler paste: selector.
1722 - (void)_pasteWithPreferredSelector:(SEL)selector sender:(id)sender
1724 NSWindow *keyWindow = [[NSApplication sharedApplication] keyWindow];
1725 NSResponder *responder;
1727 //First, look for a responder which can handle the preferred selector
1728 if (!(responder = [keyWindow earliestResponderWhichRespondsToSelector:selector
1729 andIsNotOfClass:NSClassFromString(@"WebHTMLView")])) {
1730 //No responder found. Try again, looking for one which will respond to paste:
1731 selector = @selector(paste:);
1732 responder = [keyWindow earliestResponderWhichRespondsToSelector:selector
1733 andIsNotOfClass:NSClassFromString(@"WebHTMLView")];
1736 //Sending pasteAsRichText: to a non rich text NSTextView won't do anything; change it to a generic paste:
1737 if ([responder isKindOfClass:[NSTextView class]] && ![(NSTextView *)responder isRichText]) {
1738 selector = @selector(paste:);
1742 [keyWindow makeFirstResponder:responder];
1743 [responder performSelector:selector
1748 //Custom Printing ------------------------------------------------------------------------------------------------------
1749 #pragma mark Custom Printing
1750 - (IBAction)adiumPrint:(id)sender
1752 //Pass the print command to the window, which is responsible for routing it to the correct place or
1753 //creating a view and printing. Adium will not print from a window that does not respond to adiumPrint:
1754 NSWindow *keyWindowController = [[[NSApplication sharedApplication] keyWindow] windowController];
1755 if ([keyWindowController respondsToSelector:@selector(adiumPrint:)]) {
1756 [keyWindowController performSelector:@selector(adiumPrint:)
1761 #pragma mark Preferences Display
1762 - (IBAction)showPreferenceWindow:(id)sender
1764 [adium.preferenceController showPreferenceWindow:sender];
1767 #pragma mark Font Panel
1768 - (IBAction)toggleFontPanel:(id)sender
1770 if ([NSFontPanel sharedFontPanelExists] &&
1771 [[NSFontPanel sharedFontPanel] isVisible]) {
1772 [[NSFontPanel sharedFontPanel] close];
1775 NSFontPanel *fontPanel = [NSFontPanel sharedFontPanel];
1777 if (!fontPanelAccessoryView) {
1778 [NSBundle loadNibNamed:@"FontPanelAccessoryView" owner:self];
1779 [fontPanel setAccessoryView:fontPanelAccessoryView];
1782 [fontPanel orderFront:self];
1786 - (IBAction)setFontPanelSettingsAsDefaultFont:(id)sender
1788 NSFont *selectedFont = [[NSFontManager sharedFontManager] selectedFont];
1790 [adium.preferenceController setPreference:[selectedFont stringRepresentation]
1791 forKey:KEY_FORMATTING_FONT
1792 group:PREF_GROUP_FORMATTING];
1794 //We can't get foreground/background color from the font panel so far as I can tell... so we do the best we can.
1795 NSWindow *keyWindow = [[NSApplication sharedApplication] keyWindow];
1796 NSResponder *responder = [keyWindow firstResponder];
1797 if ([responder isKindOfClass:[NSTextView class]]) {
1798 NSDictionary *typingAttributes = [(NSTextView *)responder typingAttributes];
1799 NSColor *foregroundColor, *backgroundColor;
1801 if ((foregroundColor = [typingAttributes objectForKey:NSForegroundColorAttributeName])) {
1802 [adium.preferenceController setPreference:[foregroundColor stringRepresentation]
1803 forKey:KEY_FORMATTING_TEXT_COLOR
1804 group:PREF_GROUP_FORMATTING];
1807 if ((backgroundColor = [typingAttributes objectForKey:AIBodyColorAttributeName])) {
1808 [adium.preferenceController setPreference:[backgroundColor stringRepresentation]
1809 forKey:KEY_FORMATTING_BACKGROUND_COLOR
1810 group:PREF_GROUP_FORMATTING];
1815 //Custom Dimming menu items --------------------------------------------------------------------------------------------
1816 #pragma mark Custom Dimming menu items
1817 //The standard ones do not dim correctly when unavailable
1818 - (IBAction)toggleFontTrait:(id)sender
1820 NSFontManager *fontManager = [NSFontManager sharedFontManager];
1822 if ([fontManager traitsOfFont:[fontManager selectedFont]] & [sender tag]) {
1823 [fontManager removeFontTrait:sender];
1825 [fontManager addFontTrait:sender];
1829 - (void)toggleToolbarShown:(id)sender
1831 NSWindow *window = [[NSApplication sharedApplication] keyWindow];
1832 [window toggleToolbarShown:sender];
1835 - (void)runToolbarCustomizationPalette:(id)sender
1837 NSWindow *window = [[NSApplication sharedApplication] keyWindow];
1838 [window runToolbarCustomizationPalette:sender];
1841 //Menu item validation
1842 - (BOOL)validateMenuItem:(NSMenuItem *)menuItem
1845 NSWindow *keyWindow = [[NSApplication sharedApplication] keyWindow];
1846 NSResponder *responder = [keyWindow firstResponder];
1848 if (menuItem == menuItem_bold || menuItem == menuItem_italic) {
1849 NSFont *selectedFont = [[NSFontManager sharedFontManager] selectedFont];
1851 //We must be in a text view, have text on the pasteboard, and have a font that supports bold or italic
1852 if ([responder isKindOfClass:[NSTextView class]]) {
1853 return (menuItem == menuItem_bold ? [selectedFont supportsBold] : [selectedFont supportsItalics]);
1857 } else if (menuItem == menuItem_paste || menuItem == menuItem_pasteAndMatchStyle || menuItem == menuItem_pasteWithImagesAndColors) {
1859 //The user can paste if the pasteboard contains an image, some text, one or more files, or one or more URLs.
1860 NSPasteboard *pboard = [NSPasteboard generalPasteboard];
1861 NSArray *nonImageTypes = [NSArray arrayWithObjects:
1865 NSFilenamesPboardType,
1866 NSFilesPromisePboardType,
1869 return ([pboard availableTypeFromArray:nonImageTypes] != nil) || [NSImage canInitWithPasteboard:pboard];
1871 } else if (menuItem == menuItem_showToolbar) {
1872 [menuItem_showToolbar setTitle:([[keyWindow toolbar] isVisible] ?
1873 AILocalizedString(@"Hide Toolbar",nil) :
1874 AILocalizedString(@"Show Toolbar",nil))];
1875 return [keyWindow toolbar] != nil;
1877 } else if (menuItem == menuItem_customizeToolbar) {
1878 return ([keyWindow toolbar] != nil && [[keyWindow toolbar] isVisible] && [[keyWindow windowController] canCustomizeToolbar]);
1880 } else if (menuItem == menuItem_close) {
1881 return (keyWindow && ([[keyWindow standardWindowButton:NSWindowCloseButton] isEnabled] ||
1882 ([[keyWindow windowController] respondsToSelector:@selector(windowPermitsClose)] &&
1883 [[keyWindow windowController] windowPermitsClose])));
1885 } else if (menuItem == menuItem_closeChat || menuItem == menuItem_clearDisplay) {
1886 return activeChat != nil;
1888 } else if( menuItem == menuItem_closeAllChats) {
1889 return [[self openChats] count] > 0;
1891 } else if (menuItem == menuItem_print) {
1892 NSWindowController *windowController = [keyWindow windowController];
1894 return ([windowController respondsToSelector:@selector(adiumPrint:)] &&
1895 (![windowController respondsToSelector:@selector(validatePrintMenuItem:)] ||
1896 [windowController validatePrintMenuItem:menuItem]));
1898 } else if (menuItem == menuItem_showFonts) {
1899 [menuItem_showFonts setTitle:(([NSFontPanel sharedFontPanelExists] && [[NSFontPanel sharedFontPanel] isVisible]) ?
1900 AILocalizedString(@"Hide Fonts",nil) :
1901 AILocalizedString(@"Show Fonts",nil))];
1903 } else if (menuItem == menuItem_toggleUserlist || menuItem == menuItem_toggleUserlistSide) {
1904 return self.activeChat.isGroupChat;
1910 #pragma mark Window levels
1911 - (NSMenu *)menuForWindowLevelsNotifyingTarget:(id)target
1913 NSMenu *windowPositionMenu = [[NSMenu allocWithZone:[NSMenu zone]] init];
1914 NSMenuItem *menuItem;
1916 menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Above other windows",nil)
1918 action:@selector(selectedWindowLevel:)
1920 [menuItem setEnabled:YES];
1921 [menuItem setTag:AIFloatingWindowLevel];
1922 [windowPositionMenu addItem:menuItem];
1925 menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Normally",nil)
1927 action:@selector(selectedWindowLevel:)
1929 [menuItem setEnabled:YES];
1930 [menuItem setTag:AINormalWindowLevel];
1931 [windowPositionMenu addItem:menuItem];
1934 menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Below other windows",nil)
1936 action:@selector(selectedWindowLevel:)
1938 [menuItem setEnabled:YES];
1939 [menuItem setTag:AIDesktopWindowLevel];
1940 [windowPositionMenu addItem:menuItem];
1943 [windowPositionMenu setAutoenablesItems:NO];
1945 return [windowPositionMenu autorelease];
1948 -(void)toggleUserlist:(id)sender
1950 [self.activeChat.chatContainer.chatViewController toggleUserList];
1953 -(void)toggleUserlistSide:(id)sender
1955 [self.activeChat.chatContainer.chatViewController toggleUserListSide];
1958 -(void)clearDisplay:(id)sender
1960 [self.activeChat.chatContainer.messageViewController.messageDisplayController clearView];