Added the Add Bookmark toolbar item to the default toolbar. Refs #8772
[adiumx.git] / Source / AIInterfaceController.m
blob0c7db082263c81dbf1e2bef4a53f2ccedd81cf9b
1 /* 
2  * Adium is the legal property of its developers, whose names are listed in the copyright file included
3  * with this source distribution.
4  * 
5  * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
6  * General Public License as published by the Free Software Foundation; either version 2 of the License,
7  * or (at your option) any later version.
8  * 
9  * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
10  * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
11  * Public License for more details.
12  * 
13  * You should have received a copy of the GNU General Public License along with this program; if not,
14  * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
15  */
17 // $Id$
19 #import "AIInterfaceController.h"
21 #import <Adium/AIContactControllerProtocol.h>
22 #import <Adium/AIChatControllerProtocol.h>
23 #import <Adium/AIContentControllerProtocol.h>
24 #import <Adium/AIMenuControllerProtocol.h>
25 #import <Adium/AIPreferenceControllerProtocol.h>
26 #import "AdiumDisconnectionErrorController.h"
27 #import <AIUtilities/AIAttributedStringAdditions.h>
28 #import <AIUtilities/AIColorAdditions.h>
29 #import <AIUtilities/AIFontAdditions.h>
30 #import <AIUtilities/AIImageAdditions.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 "KFTypeSelectTableView.h"
46 #import <KNShelfSplitview.h>
48 #define ERROR_MESSAGE_WINDOW_TITLE              AILocalizedString(@"Adium : Error","Error message window title")
49 #define LABEL_ENTRY_SPACING                             4.0
50 #define DISPLAY_IMAGE_ON_RIGHT                  NO
52 #define PREF_GROUP_FORMATTING                   @"Formatting"
53 #define KEY_FORMATTING_FONT                             @"Default Font"
55 #define MESSAGES_WINDOW_MENU_TITLE              AILocalizedString(@"Chats","Title for the messages window menu item")
57 //#define       LOG_RESPONDER_CHAIN
59 @interface AIInterfaceController (PRIVATE)
60 - (void)_resetOpenChatsCache;
61 - (void)_addItemToMainMenuAndDock:(NSMenuItem *)item;
62 - (NSAttributedString *)_tooltipTitleForObject:(AIListObject *)object;
63 - (NSAttributedString *)_tooltipBodyForObject:(AIListObject *)object;
64 - (void)_pasteWithPreferredSelector:(SEL)preferredSelector sender:(id)sender;
65 - (void)updateCloseMenuKeys;
67 //Window Menu
68 - (void)updateActiveWindowMenuItem;
69 - (void)buildWindowMenu;
71 - (AIChat *)mostRecentActiveChat;
72 @end
74 /*!
75  * @class AIInterfaceController
76  * @brief Interface controller
77  *
78  * Chat window related requests, such as opening and closing chats, are routed through the interface controller
79  * to the appropriate component. The interface controller keeps track of the most recently active chat, handles chat
80  * cycling (switching between chats), chat sorting, and so on.  The interface controller also handles switching to
81  * an appropriate window or chat when the dock icon is clicked for a 'reopen' event.
82  *
83  * Contact list window requests, such as toggling window visibilty are routed to the contact list controller component.
84  *
85  * Error messages are routed through the interface controller.
86  *
87  * Tooltips, such as seen on hover in the contact list are generated and displayed here.  Tooltip display components and
88  * plugins register with the interface controller to be queried for contact information when a tooltip is displayed.
89  *
90  * When displays in Adium flash, such as in the dock or the contact list for unviewed content, the interface controller
91  * manages keeping the flashing synchronized.
92  *
93  * Finally, the interface controller manages many menu items, providing better menu item validation and target routing
94  * than the responder chain alone would do.
95  */
96 @implementation AIInterfaceController
98 - (id)init
100         if ((self = [super init])) {
101                 /* Use KFTypeSelectTableView as our NSTableView base class to allow type-select searching of all
102                 * table and outline views throughout Adium.
103                 */
104                 [[KFTypeSelectTableView class] poseAsClass:[NSTableView class]];
105                 
106                 contactListViewArray = [[NSMutableArray alloc] init];
107                 messageViewArray = [[NSMutableArray alloc] init];
108                 contactListTooltipEntryArray = [[NSMutableArray alloc] init];
109                 contactListTooltipSecondaryEntryArray = [[NSMutableArray alloc] init];
110                 closeMenuConfiguredForChat = NO;
111                 _cachedOpenChats = nil;
112                 mostRecentActiveChat = nil;
113                 activeChat = nil;
114                 
115                 tooltipListObject = nil;
116                 tooltipTitle = nil;
117                 tooltipBody = nil;
118                 tooltipImage = nil;
119                 flashObserverArray = nil;
120                 flashTimer = nil;
121                 flashState = 0;
122                 
123                 windowMenuArray = nil;
124                 
125 #ifdef LOG_RESPONDER_CHAIN
126                 [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(reportResponderChain:) userInfo:nil repeats:YES];
127 #endif
128         }
129         
130         return self;
133 #ifdef LOG_RESPONDER_CHAIN
134 //Can be called by a timer to periodically log the responder chain
135 //[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(reportResponderChain:) userInfo:nil repeats:YES];
136 - (void)reportResponderChain:(NSTimer *)inTimer
138         NSMutableString *responderChain = [NSMutableString string];
139         
140         NSWindow        *keyWindow = [[NSApplication sharedApplication] keyWindow];
141         [responderChain appendFormat:@"%@ (%i): ",keyWindow,[keyWindow respondsToSelector:@selector(print:)]];
142         
143         NSResponder     *responder = [keyWindow firstResponder];
144         
145         //First, walk down the responder chain looking for a responder which can handle the preferred selector
146         while (responder) {
147                 [responderChain appendFormat:@"%@ (%i)",responder,[responder respondsToSelector:@selector(print:)]];
148                 responder = [responder nextResponder];
149                 if (responder) [responderChain appendString:@" -> "];
150         }
152         NSLog(responderChain);
154 #endif
156 - (void)controllerDidLoad
158     //Load the interface
159     [interfacePlugin openInterface];
161         //Open the contact list window
162     [self showContactList:nil];
164         //Userlist show/hide item
165         menuItem_toggleUserlist = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Toggle User List", nil)
166                                                                                                                                                                                          target:self
167                                                                                                                                                                                          action:@selector(toggleUserlist:)
168                                                                                                                                                                           keyEquivalent:@""];
169         [[adium menuController] addMenuItem:menuItem_toggleUserlist toLocation:LOC_View_General];
170                                                                                                                                                           
171         //Contact list menu item
172         NSMenuItem* menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Contact List","Name of the window which lists contacts")
173                                                                                                                                                                 target:self
174                                                                                                                                                                 action:@selector(toggleContactList:)
175                                                                                                                                                  keyEquivalent:@"/"];
176         [[adium menuController] addMenuItem:menuItem toLocation:LOC_Window_Fixed];
177         [[adium menuController] addMenuItem:[[menuItem copy] autorelease] toLocation:LOC_Dock_Status];
178         [menuItem release];
179         
180         menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Close Chat","Title for the close chat menu item")
181                                                                                                                                         target:self
182                                                                                                                                         action:@selector(closeContextualChat:)
183                                                                                                                          keyEquivalent:@""];
184         [[adium menuController] addContextualMenuItem:menuItem toLocation:Context_Tab_Action];
185         [menuItem release];
186         
187         //Observe preference changes
188         [[adium preferenceController] registerPreferenceObserver:self forGroup:PREF_GROUP_INTERFACE];
190     //Observe content so we can open chats as necessary
191     [[adium notificationCenter] addObserver:self selector:@selector(didReceiveContent:) 
192                                                                            name:CONTENT_MESSAGE_RECEIVED object:nil];
193     [[adium notificationCenter] addObserver:self selector:@selector(didReceiveContent:) 
194                                                                            name:CONTENT_MESSAGE_RECEIVED_GROUP object:nil];     
197 - (void)controllerWillClose
199     [contactListPlugin closeContactList];
200     [interfacePlugin closeInterface];
203 // Dealloc
204 - (void)dealloc
206     [contactListViewArray release]; contactListViewArray = nil;
207     [messageViewArray release]; messageViewArray = nil;
208     [interfaceArray release]; interfaceArray = nil;
209         
210     [tooltipListObject release]; tooltipListObject = nil;
211         [tooltipTitle release]; tooltipTitle = nil;
212         [tooltipBody release]; tooltipBody = nil;
213         [tooltipImage release]; tooltipImage = nil;
214         
215         [[adium notificationCenter] removeObserver:self];
216         [[adium preferenceController] unregisterPreferenceObserver:self];
217         
218     [super dealloc];
221 //Registers code to handle the interface
222 - (void)registerInterfaceController:(id <AIInterfaceComponent>)inController
224         if (!interfacePlugin) interfacePlugin = [inController retain];
227 //Register code to handle the contact list
228 - (void)registerContactListController:(id <AIContactListComponent>)inController
230         if (!contactListPlugin) contactListPlugin = [inController retain];
233 //Preferences changed
234 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key
235                                                         object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
237         //
238         [[adium notificationCenter] removeObserver:self name:Contact_OrderChanged object:nil];
239         
240         //Update prefs
241         tabbedChatting = [[prefDict objectForKey:KEY_TABBED_CHATTING] boolValue];
242         groupChatsByContactGroup = [[prefDict objectForKey:KEY_GROUP_CHATS_BY_GROUP] boolValue];
245 //Handle a reopen/dock icon click
246 - (BOOL)handleReopenWithVisibleWindows:(BOOL)visibleWindows
248         if (![self contactListIsVisibleAndMain] && [[interfacePlugin openContainers] count] == 0) {
249                 //The contact list is not visible, and there are no chat windows. Make the contact list visible.
250                 [self showContactList:nil];
252         } else {
253                 AIChat  *mostRecentUnviewedChat;
255                 //If windows are open, try switching to a chat with unviewed content
256                 if ((mostRecentUnviewedChat = [[adium chatController] mostRecentUnviewedChat])) {
257                         if ([mostRecentActiveChat unviewedContentCount]) {
258                                 //If the most recently active chat has unviewed content, ensure it is in the front
259                                 [self setActiveChat:mostRecentActiveChat];
260                         } else {
261                                 //Otherwise, switch to the chat which most recently received content
262                                 [self setActiveChat:mostRecentUnviewedChat];
263                         }
265                 } else {
266                         NSEnumerator    *enumerator;
267                         NSWindow            *window, *targetWindow = nil;
268                         BOOL            unMinimizedWindows = 0;
269                         
270                         //If there was no unviewed content, ensure that atleast one of Adium's windows is unminimized
271                         enumerator = [[NSApp windows] objectEnumerator];
272                         while ((window = [enumerator nextObject])) {
273                                 //Check stylemask to rule out the system menu's window (Which reports itself as visible like a real window)
274                                 if (([window styleMask] & (NSTitledWindowMask | NSClosableWindowMask | NSMiniaturizableWindowMask))) {
275                                         if (!targetWindow) targetWindow = window;
276                                         if (![window isMiniaturized]) unMinimizedWindows++;
277                                 }
278                         }
279                         
280                         //If there are no unminimized windows, unminimize the last one
281                         if (unMinimizedWindows == 0 && targetWindow) {
282                                 [targetWindow deminiaturize:nil];
283                         }
284                 }
285         }
287         return YES; 
290 //Contact List ---------------------------------------------------------------------------------------------------------
291 #pragma mark Contact list
293  * @brief Toggles contact list between visible and hiden
294  */
295 - (IBAction)toggleContactList:(id)sender
297     if ([self contactListIsVisibleAndMain]) {
298                 [self closeContactList:nil];
299     } else {
300                 [[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
301                 [self showContactList:nil];
302     } 
306  * @brief Brings contact list to the front
307  */
308 - (IBAction)showContactList:(id)sender
310         [contactListPlugin showContactListAndBringToFront:YES];
314  * @brief Show the contact list window and bring Adium to the front
315  */
316 - (IBAction)showContactListAndBringToFront:(id)sender
318         [contactListPlugin showContactListAndBringToFront:YES];
319         [[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
323  * @brief Close the contact list window
324  */
325 - (IBAction)closeContactList:(id)sender
327         [contactListPlugin closeContactList];
331  * @returns YES if contact list is visible and selected, otherwise NO
332  */
333 - (BOOL)contactListIsVisibleAndMain
335         return [contactListPlugin contactListIsVisibleAndMain];
339 * @returns YES if contact list is visible, otherwise NO
340  */
341 - (BOOL)contactListIsVisible
343         return [contactListPlugin contactListIsVisible];
346 //Detachable Contact List ----------------------------------------------------------------------------------------------
347 #pragma mark Detachable Contact List
350  * @returns YES if detachable groups are allowed, otherwise NO
351  */
352 - (BOOL)allowDetachableGroups {
353         return [contactListPlugin allowDetachableContactList];
357  * @returns Created contact list controller for detached contact list
358  */
359 - (AIListWindowController *)detachContactList:(AIListGroup *)aContactList {
360         return [contactListPlugin detachContactList:aContactList];
364 //Messaging ------------------------------------------------------------------------------------------------------------
365 //Methods for instructing the interface to provide a representation of chats, and to determine which chat has user focus
366 #pragma mark Messaging
369  * @brief Opens window for chat
370  */
371 - (void)openChat:(AIChat *)inChat
373         NSArray         *containers = [interfacePlugin openContainersAndChats];
374         NSString        *containerID = nil;
375         NSString        *containerName = nil;
376         
377         //Determine the correct container for this chat
378         
379         if (!tabbedChatting) {
380                 //We're not using tabs; each chat starts in its own container, based on the destination object or the chat name
381                 if ([inChat listObject]) {
382                         containerID = [[inChat listObject] internalObjectID];
383                 } else {
384                         containerID = [inChat name];
385                 }
386                 
387         } else if (groupChatsByContactGroup) {
388                 if ([inChat isGroupChat]) {
389                         containerID = AILocalizedString(@"Group Chat",nil);
390                         
391                 } else {
392                         AIListObject    *group = [[[inChat listObject] parentContact] containingObject];
393                         
394                         //If the contact is in the contact list root, we don't have a group
395                         if (group && (group != [[adium contactController] contactList])) {
396                                 containerID = [group displayName];
397                         }
398                 }
399                 
400                 containerName = containerID;
401         }
402         
403         if (!containerID) {
404                 //Open new chats into the first container (if not available, create a new one)
405                 if ([containers count] > 0) {
406                         containerID = [[containers objectAtIndex:0] objectForKey:@"ID"];
407                 } else {
408                         containerID = nil;
409                 }
410         }
412         //Determine the correct placement for this chat within the container
413         [interfacePlugin openChat:inChat inContainerWithID:containerID withName:containerName atIndex:-1];
414         if (![inChat isOpen]) {
415                 [inChat setIsOpen:YES];
416                 
417                 //Post the notification last, so observers receive a chat whose isOpen flag is yes.
418                 [[adium notificationCenter] postNotificationName:Chat_DidOpen object:inChat userInfo:nil];
419         }
422 - (id)openChat:(AIChat *)inChat inContainerWithID:(NSString *)containerID atIndex:(int)index
423 {       
424         NSArray         *containers = [interfacePlugin openContainersAndChats];
426         if (!containerID) {
427                 //Open new chats into the first container (if not available, create a new one)
428                 if ([containers count] > 0) {
429                         containerID = [[containers objectAtIndex:0] objectForKey:@"ID"];
430                 } else {
431                         containerID = AILocalizedString(@"Chat",nil);
432                 }
433         }
435         //Determine the correct placement for this chat within the container
436         id tabViewItem = [interfacePlugin openChat:inChat inContainerWithID:containerID withName:nil atIndex:index];
437         if (![inChat isOpen]) {
438                 [inChat setIsOpen:YES];
439                 
440                 //Post the notification last, so observers receive a chat whose isOpen flag is yes.
441                 [[adium notificationCenter] postNotificationName:Chat_DidOpen object:inChat userInfo:nil];
442         }
443         return tabViewItem;
447  * @brief Opens a container with a specific ID
449  * Asks the interfacePlugin to openContainerWithID:
450  */
451 - (id)openContainerWithID:(NSString *)containerID name:(NSString *)containerName
453         return [interfacePlugin openContainerWithID:containerID name:containerName];
457  * @brief Close the interface for a chat
459  * Tell the interface plugin to close the chat.
460  */
461 - (void)closeChat:(AIChat *)inChat
463         if (inChat) {
464                 if ([[adium chatController] closeChat:inChat]) {
465                         [interfacePlugin closeChat:inChat];
466                 }
467         }
471  * @brief Consolidate chats into a single container
472  */
473 - (void)consolidateChats
475         //We work with copies of these arrays, since moving chats may change their contents
476         NSArray                 *openContainers = [[interfacePlugin openContainers] copy];
477         NSEnumerator    *containerEnumerator = [openContainers objectEnumerator];
478         NSString                *firstContainerID = [containerEnumerator nextObject];
479         NSString                *containerID;
480         
481         //For all containers but the first, move the chats they contain to the first container
482         while ((containerID = [containerEnumerator nextObject])) {
483                 NSArray                 *openChats = [[interfacePlugin openChatsInContainerWithID:containerID] copy];
484                 NSEnumerator    *chatEnumerator = [openChats objectEnumerator];
485                 AIChat                  *chat;
487                 //Move all the chats, providing a target index if chat sorting is enabled
488                 while ((chat = [chatEnumerator nextObject])) {
489                         [interfacePlugin moveChat:chat
490                                         toContainerWithID:firstContainerID
491                                                                 index:-1];
492                 }
493                 
494                 [openChats release];
495         }
496         
497         [self chatOrderDidChange];
498         
499         [openContainers release];
503  * @returns Active chat
504  */
505 - (AIChat *)activeChat
507         return activeChat;
511  * @brief Set the active chat window
512  */
513 - (void)setActiveChat:(AIChat *)inChat
515         [interfacePlugin setActiveChat:inChat];
519  * @returns Last chat to be active, nil if not chat is open
520  */
521 - (AIChat *)mostRecentActiveChat
523         return mostRecentActiveChat;
527  * @brief Sets active chat window based on chat
528  */
529 - (void)setMostRecentActiveChat:(AIChat *)inChat
531         [self setActiveChat:inChat];
535  * @returns Array of open chats (cached, so call as frequently as desired)
536  */
537 - (NSArray *)openChats
539         if (!_cachedOpenChats) {
540                 _cachedOpenChats = [[interfacePlugin openChats] retain];
541         }
542         
543         return _cachedOpenChats;
547  * @param containerID ID for chat window
549  * @returns Array of all chats in chat window
550  */
551 - (NSArray *)openChatsInContainerWithID:(NSString *)containerID
553         return [interfacePlugin openChatsInContainerWithID:containerID];
557  * @brief Resets the cache of open chats
558  */
559 - (void)_resetOpenChatsCache
561         [_cachedOpenChats release]; _cachedOpenChats = nil;
566 //Interface plugin callbacks -------------------------------------------------------------------------------------------
567 //These methods are called by the interface to let us know what's going on.  We're informed of chats opening, closing,
568 //changing order, etc.
569 #pragma mark Interface plugin callbacks
571  * @breif A chat window did open: rebuild our window menu to show the new chat
573  * @param inChat Newly created chat 
574  */
575 - (void)chatDidOpen:(AIChat *)inChat
577         [self _resetOpenChatsCache];
578         [self buildWindowMenu];
582  * @brief A chat has become active: update our chat closing keys and flag this chat as selected in the window menu
584  * @param inChat Chat which has become active
585  */
586 - (void)chatDidBecomeActive:(AIChat *)inChat
588         AIChat  *previouslyActiveChat = activeChat;
589         
590         activeChat = [inChat retain];
591         
592         [self updateCloseMenuKeys];
593         [self updateActiveWindowMenuItem];
594         
595         if (inChat && (inChat != mostRecentActiveChat)) {
596                 [mostRecentActiveChat release]; mostRecentActiveChat = nil;
597                 mostRecentActiveChat = [inChat retain];
598         }
599         
600         [[adium notificationCenter] postNotificationName:Chat_BecameActive
601                                                                                           object:inChat 
602                                                                                         userInfo:(previouslyActiveChat ?
603                                                                                                           [NSDictionary dictionaryWithObject:previouslyActiveChat
604                                                                                                                                                                   forKey:@"PreviouslyActiveChat"] :
605                                                                                                           nil)];
606         
607         if (inChat) {
608                 /* Clear the unviewed content on the next event loop so other methods have a chance to react to the chat becoming
609                 * active. Specifically, this lets the handleReopenWithVisibleWindows: method have a chance to know that this chat
610                 * had unviewed content.
611                 */
612                 [inChat performSelector:@selector(clearUnviewedContentCount)
613                                          withObject:nil
614                                          afterDelay:0];
615         }
616         
617         [previouslyActiveChat release]; 
621  * @brief A chat has become visible: send out a notification for components and plugins to take action
623  * @param inChat Chat that has become active
624  * @param nWindow Containing chat window
625  */
626 - (void)chatDidBecomeVisible:(AIChat *)inChat inWindow:(NSWindow *)inWindow
628         [[adium notificationCenter] postNotificationName:@"AIChatDidBecomeVisible"
629                                                                                           object:inChat
630                                                                                         userInfo:[NSDictionary dictionaryWithObject:inWindow
631                                                                                                                                                                  forKey:@"NSWindow"]];
635  * @brief Find the window currently displaying a chat
637  * @returns Window for chat otherwise if the chat is not in any window, or is not visible in any window, returns nil
638  */
639 - (NSWindow *)windowForChat:(AIChat *)inChat
641         return [interfacePlugin windowForChat:inChat];
645  * @brief Find the chat active in a window
647  * If the window does not have an active chat, nil is returned
648  */
649 - (AIChat *)activeChatInWindow:(NSWindow *)window
651         return [interfacePlugin activeChatInWindow:window];
655  * @brief A chat window did close: rebuild our window menu to remove the chat
656  * 
657  * @param inChat Chat that closed
658  */
659 - (void)chatDidClose:(AIChat *)inChat
661         [self _resetOpenChatsCache];
662         [inChat clearUnviewedContentCount];
663         [self buildWindowMenu];
664         
665         if (inChat == activeChat) {
666                 [activeChat release]; activeChat = nil;
667         }
668         
669         if (inChat == mostRecentActiveChat) {
670                 [mostRecentActiveChat release]; mostRecentActiveChat = nil;
671         }
675  * @brief The order of chats has changed: rebuild our window menu to reflect the new order
676  */
677 - (void)chatOrderDidChange
679         [self _resetOpenChatsCache];
680         [self buildWindowMenu];
681         [[adium notificationCenter] postNotificationName:Chat_OrderDidChange object:nil userInfo:nil];
682         
685 #pragma mark Unviewed content
688  * @breif Content was received, increase the unviewed content count of the chat (if it's not currently active)
689  */
690 - (void)didReceiveContent:(NSNotification *)notification
692         AIChat          *chat = [[notification userInfo] objectForKey:@"AIChat"];
693         
694         if (chat != activeChat) {
695                 [chat incrementUnviewedContentCount];
696         }
700 //Chat close menus -----------------------------------------------------------------------------------------------------
701 #pragma mark Chat close menus
704  * @brief Closes currently active window
705  */
706 - (IBAction)closeMenu:(id)sender
708     [[[NSApplication sharedApplication] keyWindow] performClose:nil];
712  * @brief Closes currently active chat (if there is an active chat)
713  */
714 - (IBAction)closeChatMenu:(id)sender
716         if (activeChat) [self closeChat:activeChat];
720  * @brief Closes currently selected chat based on current chat contextual menu
721  */
722 - (IBAction)closeContextualChat:(id)sender
724         [self closeChat:[[adium menuController] currentContextMenuChat]];
728  * @brief Loop through open chats and close them
729  */
730 - (IBAction)closeAllChats:(id)sender
732         NSEnumerator    *containerEnumerator = [[[[interfacePlugin openChats] copy] autorelease] objectEnumerator];
733         AIChat                  *chatToClose;
735         while ((chatToClose = [containerEnumerator nextObject])) {
736                 [self closeChat:chatToClose];
737         }
741  * @brief Updates the key equivalents on 'close' and 'close chat' (dynamically changed to make cmd-w less destructive)
742  */
743 - (void)updateCloseMenuKeys
745         if (activeChat && !closeMenuConfiguredForChat) {
746         [menuItem_close setKeyEquivalent:@"W"];
747         [menuItem_closeChat setKeyEquivalent:@"w"];
748                 closeMenuConfiguredForChat = YES;
749         } else if (!activeChat && closeMenuConfiguredForChat) {
750         [menuItem_close setKeyEquivalent:@"w"];
751                 [menuItem_closeChat removeKeyEquivalent];               
752                 closeMenuConfiguredForChat = NO;
753         }
757 //Window Menu ----------------------------------------------------------------------------------------------------------
758 #pragma mark Window Menu
761  * @brief Make a chat window active
762  * 
763  * Invoked by a selection in the window menu
764  */
765 - (IBAction)showChatWindow:(id)sender
767         [self setActiveChat:[sender representedObject]];
768     [[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
772  * @brief Updates the 'check' icon so it's next to the active window
773  */
774 - (void)updateActiveWindowMenuItem
776     NSEnumerator        *enumerator = [windowMenuArray objectEnumerator];
777     NSMenuItem          *item;
779     while ((item = [enumerator nextObject])) {
780                 if ([item representedObject]) [item setState:([item representedObject] == activeChat ? NSOnState : NSOffState)];
781     }
785  * @brief Builds the window menu
786  * 
787  * This function gets called whenever chats are opened, closed, or re-ordered - so improvements and optimizations here
788  * would probably be helpful
789  */
790 - (void)buildWindowMenu
791 {       
792     NSMenuItem                          *item;
793     NSEnumerator                        *enumerator;
794     int                                         windowKey = 1;
795         
796     //Remove any existing menus
797     enumerator = [windowMenuArray objectEnumerator];
798     while ((item = [enumerator nextObject])) {
799         [[adium menuController] removeMenuItem:item];
800     }
801     [windowMenuArray release]; windowMenuArray = [[NSMutableArray alloc] init];
802         
803     //Messages window and any open messasges
804         NSEnumerator    *containerEnumerator = [[interfacePlugin openContainersAndChats] objectEnumerator];
805         NSDictionary    *containerDict;
806         
807         while ((containerDict = [containerEnumerator nextObject])) {
808                 NSString                *containerName = [containerDict objectForKey:@"Name"];
809                 NSArray                 *contentArray = [containerDict objectForKey:@"Content"];
810                 NSEnumerator    *contentEnumerator = [contentArray objectEnumerator];
811                 AIChat                  *chat;
812                 
813                 //Add a menu item for the container
814                 if ([contentArray count] > 1) {
815                         item = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:containerName
816                                                                                                                                                 target:nil
817                                                                                                                                                 action:nil
818                                                                                                                                  keyEquivalent:@""];
819                         [self _addItemToMainMenuAndDock:item];
820                         [item release];
821                 }
822                 
823                 //Add items for the chats it contains
824                 while ((chat = [contentEnumerator nextObject])) {
825                         NSString                *windowKeyString;
826                         
827                         //Prepare a key equivalent for the controller
828                         if (windowKey < 10) {
829                                 windowKeyString = [NSString stringWithFormat:@"%i",(windowKey)];
830                         } else if (windowKey == 10) {
831                                 windowKeyString = [NSString stringWithString:@"0"];
832                         } else {
833                                 windowKeyString = [NSString stringWithString:@""];
834                         }
835                         
836                         item = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:[chat displayName]
837                                                                                                                                                 target:self
838                                                                                                                                                 action:@selector(showChatWindow:)
839                                                                                                                                  keyEquivalent:windowKeyString];
840                         if ([contentArray count] > 1) [item setIndentationLevel:1];
841                         [item setRepresentedObject:chat];
842                         [item setImage:[chat chatMenuImage]];
843                         [self _addItemToMainMenuAndDock:item];
844                         [item release];
846                         windowKey++;
847                 }
848         }
850         [self updateActiveWindowMenuItem];
854  * brief Adds a menu item to the internal array, dock menu, and main menu
856  * Should be used for adding a new window to the window menu (and dock menu)
857  */
858 - (void)_addItemToMainMenuAndDock:(NSMenuItem *)item
860         //Add to main menu first
861         [[adium menuController] addMenuItem:item toLocation:LOC_Window_Fixed];
862         [windowMenuArray addObject:item];
863         
864         //Make a copy, and add to the dock
865         item = [item copy];
866         [item setKeyEquivalent:@""];
867         [[adium menuController] addMenuItem:item toLocation:LOC_Dock_Status];
868         [windowMenuArray addObject:item];
869         [item release];
873 //Chat Cycling ---------------------------------------------------------------------------------------------------------
874 #pragma mark Chat Cycling
877  * @brief Cycles to the next active chat
878  */
879 - (void)nextChat:(id)sender
881         NSArray *openChats = [self openChats];
883         if ([openChats count]) {
884                 if (activeChat) {
885                         int chatIndex = [openChats indexOfObject:activeChat]+1;
886                         [self setActiveChat:[openChats objectAtIndex:(chatIndex < [openChats count] ? chatIndex : 0)]];
887                 } else {
888                         [self setActiveChat:[openChats objectAtIndex:0]];
889                 }
890         }
894  * @brief Cycles to the previus active chat
895  */
896 - (void)previousChat:(id)sender
898         NSArray *openChats = [self openChats];
899         
900         if ([openChats count]) {
901                 if (activeChat) {
902                         int chatIndex = [openChats indexOfObject:activeChat]-1;
903                         [self setActiveChat:[openChats objectAtIndex:(chatIndex >= 0 ? chatIndex : [openChats count]-1)]];
904                 } else {
905                         [self setActiveChat:[openChats lastObject]];
906                 }
907         }
910 //Selected contact ------------------------------------------------
911 #pragma mark Selected contact
912 - (id)_performSelectorOnFirstAvailableResponder:(SEL)selector
914     NSResponder *responder = [[[NSApplication sharedApplication] mainWindow] firstResponder];
915     //Check the first responder
916     if ([responder respondsToSelector:selector]) {
917         return [responder performSelector:selector];
918     }
919         
920     //Search the responder chain
921     do{
922         responder = [responder nextResponder];
923         if ([responder respondsToSelector:selector]) {
924             return [responder performSelector:selector];
925         }
926                 
927     } while (responder != nil);
928         
929     //None found, return nil
930     return nil;
932 - (id)_performSelectorOnFirstAvailableResponder:(SEL)selector conformingToProtocol:(Protocol *)protocol
934         NSResponder *responder = [[[NSApplication sharedApplication] mainWindow] firstResponder];
935         //Check the first responder
936         if ([responder conformsToProtocol:protocol] && [responder respondsToSelector:selector]) {
937                 return [responder performSelector:selector];
938         }
939         
940     //Search the responder chain
941     do{
942         responder = [responder nextResponder];
943         if ([responder conformsToProtocol:protocol] && [responder respondsToSelector:selector]) {
944             return [responder performSelector:selector];
945         }
946                 
947     } while (responder != nil);
948         
949     //None found, return nil
950     return nil;
954  * @returns The "selected"(represented) contact (By finding the first responder that returns a contact)
955  * If no listObject is found, try to find a list object selected in a group chat
956  */
957 - (AIListObject *)selectedListObject
959         AIListObject *listObject = [self _performSelectorOnFirstAvailableResponder:@selector(listObject)];
960         if ( !listObject) {
961                 listObject = [self _performSelectorOnFirstAvailableResponder:@selector(preferredListObject)];
962         }
963         return listObject;
966 - (AIListObject *)selectedListObjectInContactList
968         return [self _performSelectorOnFirstAvailableResponder:@selector(listObject) conformingToProtocol:@protocol(ContactListOutlineView)];
970 - (NSArray *)arrayOfSelectedListObjectsInContactList
972         return [self _performSelectorOnFirstAvailableResponder:@selector(arrayOfListObjects) conformingToProtocol:@protocol(ContactListOutlineView)];
975 //Message View ---------------------------------------------------------------------------------------------------------
976 //Message view is abstracted from the containing interface, since they're not directly related to eachother
977 #pragma mark Message View
978 //Registers a view to handle the contact list
979 - (void)registerMessageDisplayPlugin:(id <AIMessageDisplayPlugin>)inPlugin
981     [messageViewArray addObject:inPlugin];
983 - (id <AIMessageDisplayController>)messageDisplayControllerForChat:(AIChat *)inChat
985         //Sometimes our users find it amusing to disable plugins that are located within the Adium bundle.  This error
986         //trap prevents us from crashing if they happen to disable all the available message view plugins.
987         //PUT THAT PLUGIN BACK IT WAS IMPORTANT!
988         if ([messageViewArray count] == 0) {
989                 NSRunCriticalAlertPanel(@"No Message View Plugin Installed",
990                                                                 @"Adium cannot find its message view plugin. Please re-install.  If you've manually disabled Adium's message view plugin, please re-enable it.",
991                                                                 @"Quit",
992                                                                 nil,
993                                                                 nil);
994                 [NSApp terminate:nil];
995         }
996         
997         return [[messageViewArray objectAtIndex:0] messageDisplayControllerForChat:inChat];
1001 //Error Display --------------------------------------------------------------------------------------------------------
1002 #pragma mark Error Display
1003 - (void)handleErrorMessage:(NSString *)inTitle withDescription:(NSString *)inDesc
1005     [self handleMessage:inTitle withDescription:inDesc withWindowTitle:ERROR_MESSAGE_WINDOW_TITLE];
1008 - (void)handleMessage:(NSString *)inTitle withDescription:(NSString *)inDesc withWindowTitle:(NSString *)inWindowTitle;
1010     NSDictionary        *errorDict;
1011     
1012     //Post a notification that an error was recieved
1013     errorDict = [NSDictionary dictionaryWithObjectsAndKeys:inTitle,@"Title",inDesc,@"Description",inWindowTitle,@"Window Title",nil];
1014     [[adium notificationCenter] postNotificationName:Interface_ShouldDisplayErrorMessage object:nil userInfo:errorDict];
1017 //Display then clear the last disconnection error
1018 - (void)account:(AIAccount *)inAccount disconnectedWithError:(NSString *)disconnectionError
1020 //      [AdiumDisconnectionErrorController account:inAccount disconnectedWithError:disconnectionError];
1023 //Question Display -----------------------------------------------------------------------------------------------------
1024 #pragma mark Question Display
1025 - (void)displayQuestion:(NSString *)inTitle withAttributedDescription:(NSAttributedString *)inDesc withWindowTitle:(NSString *)inWindowTitle
1026                   defaultButton:(NSString *)inDefaultButton alternateButton:(NSString *)inAlternateButton otherButton:(NSString *)inOtherButton
1027                                  target:(id)inTarget selector:(SEL)inSelector userInfo:(id)inUserInfo
1029         NSMutableDictionary *questionDict = [NSMutableDictionary dictionary];
1030         
1031         if(inTitle != nil)
1032                 [questionDict setObject:inTitle forKey:@"Title"];
1033         if(inDesc != nil)
1034                 [questionDict setObject:inDesc forKey:@"Description"];
1035         if(inWindowTitle != nil)
1036                 [questionDict setObject:inWindowTitle forKey:@"Window Title"];
1037         if(inDefaultButton != nil)
1038                 [questionDict setObject:inDefaultButton forKey:@"Default Button"];
1039         if(inAlternateButton != nil)
1040                 [questionDict setObject:inAlternateButton forKey:@"Alternate Button"];
1041         if(inOtherButton != nil)
1042                 [questionDict setObject:inOtherButton forKey:@"Other Button"];
1043         if(inTarget != nil)
1044                 [questionDict setObject:inTarget forKey:@"Target"];
1045         if(inSelector != NULL)
1046                 [questionDict setObject:NSStringFromSelector(inSelector) forKey:@"Selector"];
1047         if(inUserInfo != nil)
1048                 [questionDict setObject:inUserInfo forKey:@"Userinfo"];
1049         
1050         [[adium notificationCenter] postNotificationName:Interface_ShouldDisplayQuestion object:nil userInfo:questionDict];
1053 - (void)displayQuestion:(NSString *)inTitle withDescription:(NSString *)inDesc withWindowTitle:(NSString *)inWindowTitle
1054                   defaultButton:(NSString *)inDefaultButton alternateButton:(NSString *)inAlternateButton otherButton:(NSString *)inOtherButton
1055                                  target:(id)inTarget selector:(SEL)inSelector userInfo:(id)inUserInfo
1057         [self displayQuestion:inTitle
1058 withAttributedDescription:[[[NSAttributedString alloc] initWithString:inDesc
1059                                                                                                                    attributes:[NSDictionary dictionaryWithObject:[NSFont systemFontOfSize:0]
1060                                                                                                                                                                                                   forKey:NSFontAttributeName]] autorelease]
1061                   withWindowTitle:inWindowTitle
1062                         defaultButton:inDefaultButton
1063                   alternateButton:inAlternateButton
1064                           otherButton:inOtherButton
1065                                    target:inTarget
1066                                  selector:inSelector
1067                                  userInfo:inUserInfo];
1069 //Synchronized Flashing ------------------------------------------------------------------------------------------------
1070 #pragma mark Synchronized Flashing
1071 //Register to observe the synchronized flashing
1072 - (void)registerFlashObserver:(id <AIFlashObserver>)inObserver
1074     //Setup the timer if we don't have one yet
1075     if (!flashObserverArray) {
1076         flashObserverArray = [[NSMutableArray alloc] init];
1077         flashTimer = [[NSTimer scheduledTimerWithTimeInterval:(1.0/2.0) 
1078                                                        target:self 
1079                                                      selector:@selector(flashTimer:) 
1080                                                      userInfo:nil
1081                                                       repeats:YES] retain];
1082     }
1083     
1084     //Add the new observer to the array
1085     [flashObserverArray addObject:inObserver];
1088 //Unregister from observing flashing
1089 - (void)unregisterFlashObserver:(id <AIFlashObserver>)inObserver
1091     //Remove the observer from our array
1092     [flashObserverArray removeObject:inObserver];
1093     
1094     //Release the observer array and uninstall the timer
1095     if ([flashObserverArray count] == 0) {
1096         [flashObserverArray release]; flashObserverArray = nil;
1097         [flashTimer invalidate];
1098         [flashTimer release]; flashTimer = nil;
1099     }
1102 //Timer, invoke a flash
1103 - (void)flashTimer:(NSTimer *)inTimer
1105     NSEnumerator        *enumerator;
1106     id<AIFlashObserver> observer;
1107     
1108     flashState++;
1109     
1110     enumerator = [flashObserverArray objectEnumerator];
1111     while ((observer = [enumerator nextObject])) {
1112         [observer flash:flashState];
1113     }
1116 //Current state of flashing.  This is an integer the increases by 1 with every flash.  Mod to whatever range is desired
1117 - (int)flashState
1119     return flashState;
1123 //Tooltips -------------------------------------------------------------------------------------------------------------
1124 #pragma mark Tooltips
1125 //Registers code to display tooltip info about a contact
1126 - (void)registerContactListTooltipEntry:(id <AIContactListTooltipEntry>)inEntry secondaryEntry:(BOOL)isSecondary
1128     if (isSecondary)
1129         [contactListTooltipSecondaryEntryArray addObject:inEntry];
1130     else
1131         [contactListTooltipEntryArray addObject:inEntry];
1134 //Unregisters code to display tooltip info about a contact
1135 - (void)unregisterContactListTooltipEntry:(id <AIContactListTooltipEntry>)inEntry secondaryEntry:(BOOL)isSecondary
1137     if (isSecondary)
1138         [contactListTooltipSecondaryEntryArray removeObject:inEntry];
1139     else
1140         [contactListTooltipEntryArray removeObject:inEntry];
1143 //list object tooltips
1144 - (void)showTooltipForListObject:(AIListObject *)object atScreenPoint:(NSPoint)point onWindow:(NSWindow *)inWindow 
1146     if (object) {
1147         if (object == tooltipListObject) { //If we already have this tooltip open
1148                                          //Move the existing tooltip
1149             [AITooltipUtilities showTooltipWithTitle:tooltipTitle
1150                                                                                                 body:tooltipBody
1151                                                                                            image:tooltipImage 
1152                                                                                 imageOnRight:DISPLAY_IMAGE_ON_RIGHT 
1153                                                                                         onWindow:inWindow
1154                                                                                          atPoint:point 
1155                                                                                  orientation:TooltipBelow];
1156             
1157         } else { //This is a new tooltip
1158             NSArray                     *tabArray;
1159             NSMutableParagraphStyle     *paragraphStyleTitle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
1160             NSMutableParagraphStyle     *paragraphStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
1161             
1162             //Hold onto the new object
1163             [tooltipListObject release]; tooltipListObject = [object retain];
1164             
1165             //Buddy Icon
1166             [tooltipImage release];
1167                         tooltipImage = [[tooltipListObject userIcon] retain];
1168                         if (!tooltipImage) tooltipImage = [[AIServiceIcons serviceIconForObject:tooltipListObject
1169                                                                                                                                                          type:AIServiceIconLarge
1170                                                                                                                                                 direction:AIIconNormal] retain];
1171             
1172             //Reset the maxLabelWidth for the tooltip generation
1173             maxLabelWidth = 0;
1174             
1175             //Build a tooltip string for the primary information
1176             [tooltipTitle release]; tooltipTitle = [[self _tooltipTitleForObject:object] retain];
1177             
1178             //If there is an image, set the title tab and indentation settings independently
1179             if (tooltipImage) {
1180                 //Set a right-align tab at the maximum label width and a left-align just past it
1181                 tabArray = [[NSArray alloc] initWithObjects:[[[NSTextTab alloc] initWithType:NSRightTabStopType 
1182                                                                                                                                                                         location:maxLabelWidth] autorelease]
1183                                                             ,[[[NSTextTab alloc] initWithType:NSLeftTabStopType 
1184                                                                                    location:maxLabelWidth + LABEL_ENTRY_SPACING] autorelease]
1185                                                             ,nil];
1186                 
1187                 [paragraphStyleTitle setTabStops:tabArray];
1188                 [tabArray release];
1189                 tabArray = nil;
1190                 [paragraphStyleTitle setHeadIndent:(maxLabelWidth + LABEL_ENTRY_SPACING)];
1191                 
1192                 [tooltipTitle addAttribute:NSParagraphStyleAttributeName 
1193                                      value:paragraphStyleTitle
1194                                      range:NSMakeRange(0,[tooltipTitle length])];
1195                 
1196                 //Reset the max label width since the body will be independent
1197                 maxLabelWidth = 0;
1198             }
1199             
1200             //Build a tooltip string for the secondary information
1201             [tooltipBody release]; tooltipBody = nil;
1202             tooltipBody = [[self _tooltipBodyForObject:object] retain];
1203             
1204             //Set a right-align tab at the maximum label width for the body and a left-align just past it
1205             tabArray = [[NSArray alloc] initWithObjects:[[[NSTextTab alloc] initWithType:NSRightTabStopType 
1206                                                                                  location:maxLabelWidth] autorelease]
1207                                                         ,[[[NSTextTab alloc] initWithType:NSLeftTabStopType 
1208                                                                                 location:maxLabelWidth + LABEL_ENTRY_SPACING] autorelease]
1209                                                         ,nil];
1210             [paragraphStyle setTabStops:tabArray];
1211             [tabArray release];
1212             [paragraphStyle setHeadIndent:(maxLabelWidth + LABEL_ENTRY_SPACING)];
1213             
1214             [tooltipBody addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:NSMakeRange(0,[tooltipBody length])];
1215             //If there is no image, also use these settings for the top part
1216             if (!tooltipImage) {
1217                 [tooltipTitle addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:NSMakeRange(0,[tooltipTitle length])];
1218             }
1219             
1220             //Display the new tooltip
1221             [AITooltipUtilities showTooltipWithTitle:tooltipTitle
1222                                                 body:tooltipBody 
1223                                                image:tooltipImage
1224                                         imageOnRight:DISPLAY_IMAGE_ON_RIGHT
1225                                             onWindow:inWindow
1226                                              atPoint:point 
1227                                          orientation:TooltipBelow];
1228                         
1229                         [paragraphStyleTitle release];
1230                         [paragraphStyle release];
1231         }
1232         
1233     } else {
1234         //Hide the existing tooltip
1235         if (tooltipListObject) {
1236             [AITooltipUtilities showTooltipWithTitle:nil 
1237                                                 body:nil
1238                                                image:nil 
1239                                             onWindow:nil
1240                                              atPoint:point
1241                                          orientation:TooltipBelow];
1242             [tooltipListObject release]; tooltipListObject = nil;
1243                         
1244                         [tooltipTitle release]; tooltipTitle = nil;
1245                         [tooltipBody release]; tooltipBody = nil;
1246                         [tooltipImage release]; tooltipImage = nil;
1247         }
1248     }
1251 - (NSAttributedString *)_tooltipTitleForObject:(AIListObject *)object
1253     NSMutableAttributedString           *titleString = [[NSMutableAttributedString alloc] init];
1254     
1255     id <AIContactListTooltipEntry>              tooltipEntry;
1256     NSEnumerator                                                *enumerator;
1257     NSEnumerator                        *labelEnumerator;
1258     NSMutableArray                      *labelArray = [NSMutableArray array];
1259     NSMutableArray                      *entryArray = [NSMutableArray array];
1260     NSMutableAttributedString           *entryString;
1261     float                               labelWidth;
1262     BOOL                                isFirst = YES;
1263     
1264     NSString                            *formattedUID = [object formattedUID];
1265     
1266     //Configure fonts and attributes
1267     NSFontManager                       *fontManager = [NSFontManager sharedFontManager];
1268     NSFont                              *toolTipsFont = [NSFont toolTipsFontOfSize:10];
1269     NSMutableDictionary                 *titleDict = [NSMutableDictionary dictionaryWithObject:[fontManager convertFont:[NSFont toolTipsFontOfSize:12] toHaveTrait:NSBoldFontMask]
1270                                                                                             forKey:NSFontAttributeName];
1271     NSMutableDictionary                 *labelDict = [NSMutableDictionary dictionaryWithObject:[fontManager convertFont:[NSFont toolTipsFontOfSize:9] toHaveTrait:NSBoldFontMask]
1272                                                                                             forKey:NSFontAttributeName];
1273     NSMutableDictionary                 *labelEndLineDict = [NSMutableDictionary dictionaryWithObject:[NSFont toolTipsFontOfSize:2]
1274                                                                                                    forKey:NSFontAttributeName];
1275     NSMutableDictionary                 *entryDict = [NSMutableDictionary dictionaryWithObject:toolTipsFont
1276                                                                                             forKey:NSFontAttributeName];
1277         
1278         //Get the user's display name as an attributed string
1279     NSAttributedString                  *displayName = [[NSAttributedString alloc] initWithString:[object displayName]
1280                                                                                                                                                                            attributes:titleDict];
1281         NSAttributedString                                      *filteredDisplayName = [[adium contentController] filterAttributedString:displayName
1282                                                                                                                                                                                                  usingFilterType:AIFilterTooltips
1283                                                                                                                                                                                                            direction:AIFilterIncoming
1284                                                                                                                                                                                                                  context:nil];
1285         
1286         //Append the user's display name
1287         if (filteredDisplayName) {
1288                 [titleString appendAttributedString:filteredDisplayName];
1289         }
1290         
1291         //Append the user's formatted UID if there is one that's different to the display name
1292         if (formattedUID && (!([[[displayName string] compactedString] isEqualToString:[formattedUID compactedString]]))) {
1293                 [titleString appendString:[NSString stringWithFormat:@" (%@)", formattedUID] withAttributes:titleDict];
1294         }
1295         [displayName release];
1296     
1297     if ([object isKindOfClass:[AIListContact class]]) {
1298                 if ((![object isKindOfClass:[AIMetaContact class]] || [(AIMetaContact *)object containsOnlyOneService]) &&
1299                         [object userIcon]) {
1300                         NSImage *serviceIcon = [[AIServiceIcons serviceIconForObject:object type:AIServiceIconSmall direction:AIIconNormal]
1301                                                                         imageByScalingToSize:NSMakeSize(14,14)];
1302                         if (serviceIcon) {
1303                                 NSTextAttachment                *attachment;
1304                                 NSTextAttachmentCell    *cell;
1305                                 
1306                                 cell = [[NSTextAttachmentCell alloc] init];
1307                                 [cell setImage:serviceIcon];
1308                                 
1309                                 attachment = [[NSTextAttachment alloc] init];
1310                                 [attachment setAttachmentCell:cell];
1311                                 [cell release];
1312         
1313                                 [titleString appendString:@" " withAttributes:nil];
1314                                 [titleString appendAttributedString:[NSAttributedString attributedStringWithAttachment:attachment]];
1315                                 [attachment release];
1316                         }
1317                 }
1318         }
1319                 
1320     if ([object isKindOfClass:[AIListGroup class]]) {
1321         [titleString appendString:[NSString stringWithFormat:@" (%i/%i)",[(AIListGroup *)object visibleCount],[(AIListGroup *)object containedObjectsCount]] 
1322                    withAttributes:titleDict];
1323     }
1324     
1325     //Entries from plugins
1326     
1327     //Calculate the widest label while loading the arrays
1328     enumerator = [contactListTooltipEntryArray objectEnumerator];
1329     
1330     while ((tooltipEntry = [enumerator nextObject])) {
1331         
1332         entryString = [[tooltipEntry entryForObject:object] mutableCopy];
1333         if (entryString && [entryString length]) {
1334             
1335             NSString        *labelString = [tooltipEntry labelForObject:object];
1336             if (labelString && [labelString length]) {
1337                 
1338                 [entryArray addObject:entryString];
1339                 [labelArray addObject:labelString];
1340                 
1341                 NSAttributedString * labelAttribString = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@:",labelString] 
1342                                                                                                                                                                                  attributes:labelDict];
1343                 
1344                 //The largest size should be the label's size plus the distance to the next tab at least a space past its end
1345                 labelWidth = [labelAttribString size].width;
1346                 [labelAttribString release];
1347                 
1348                 if (labelWidth > maxLabelWidth)
1349                     maxLabelWidth = labelWidth;
1350             }
1351         }
1352         [entryString release];
1353     }
1354     
1355     //Add labels plus entires to the toolTip
1356     enumerator = [entryArray objectEnumerator];
1357     labelEnumerator = [labelArray objectEnumerator];
1358     
1359     while ((entryString = [enumerator nextObject])) {        
1360         NSAttributedString * labelAttribString = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"\t%@:\t",[labelEnumerator nextObject]]
1361                                                                                                                                                                  attributes:labelDict];
1362         
1363         //Add a carriage return
1364         [titleString appendString:@"\n" withAttributes:labelEndLineDict];
1365         
1366         if (isFirst) {
1367             //skip a line
1368             [titleString appendString:@"\n" withAttributes:labelEndLineDict];
1369             isFirst = NO;
1370         }
1371         
1372         //Add the label (with its spacing)
1373         [titleString appendAttributedString:labelAttribString];
1374                 [labelAttribString release];
1376                 [entryString addAttributes:entryDict range:NSMakeRange(0,[entryString length])];
1377         [titleString appendAttributedString:entryString];
1378     }
1380     return [titleString autorelease];
1383 - (NSAttributedString *)_tooltipBodyForObject:(AIListObject *)object
1385     NSMutableAttributedString       *tipString = [[NSMutableAttributedString alloc] init];
1386     
1387     //Configure fonts and attributes
1388     NSFontManager                   *fontManager = [NSFontManager sharedFontManager];
1389     NSFont                          *toolTipsFont = [NSFont toolTipsFontOfSize:10];
1390     NSMutableDictionary             *labelDict = [NSMutableDictionary dictionaryWithObject:[fontManager convertFont:[NSFont toolTipsFontOfSize:9] toHaveTrait:NSBoldFontMask]
1391                                                                                         forKey:NSFontAttributeName];
1392     NSMutableDictionary             *labelEndLineDict = [NSMutableDictionary dictionaryWithObject:[NSFont toolTipsFontOfSize:1]
1393                                                                                                forKey:NSFontAttributeName];
1394     NSMutableDictionary             *entryDict = [NSMutableDictionary dictionaryWithObject:toolTipsFont
1395                                                                                         forKey:NSFontAttributeName];
1396     
1397     //Entries from plugins
1398     id <AIContactListTooltipEntry>  tooltipEntry;
1399     NSEnumerator                    *enumerator;
1400     NSEnumerator                    *labelEnumerator; 
1401     NSMutableArray                  *labelArray = [NSMutableArray array];
1402     NSMutableArray                  *entryArray = [NSMutableArray array];    
1403     NSMutableAttributedString       *entryString;
1404     float                           labelWidth;
1405     BOOL                            firstEntry = YES;
1406     
1407     //Calculate the widest label while loading the arrays
1408         enumerator = [contactListTooltipSecondaryEntryArray objectEnumerator];
1409         
1410         while ((tooltipEntry = [enumerator nextObject])) {
1411                 
1412                 entryString = [[tooltipEntry entryForObject:object] mutableCopy];
1413                 if (entryString && [entryString length]) {
1414                         
1415                         NSString        *labelString = [tooltipEntry labelForObject:object];
1416                         if (labelString && [labelString length]) {
1417                                 
1418                                 [entryArray addObject:entryString];
1419                                 [labelArray addObject:labelString];
1420                                 
1421                                 NSAttributedString * labelAttribString = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@:",labelString] 
1422                                                                                                                                                                                  attributes:labelDict];
1423                                 
1424                                 //The largest size should be the label's size plus the distance to the next tab at least a space past its end
1425                                 labelWidth = [labelAttribString size].width;
1426                                 [labelAttribString release];
1427                                 
1428                                 if (labelWidth > maxLabelWidth)
1429                                         maxLabelWidth = labelWidth;
1430                         }
1431                 }
1432                 [entryString release];
1433         }
1434                 
1435     //Add labels plus entires to the toolTip
1436     enumerator = [entryArray objectEnumerator];
1437     labelEnumerator = [labelArray objectEnumerator];
1438     while ((entryString = [enumerator nextObject])) {
1439         NSMutableAttributedString *labelString = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"\t%@:\t",[labelEnumerator nextObject]]
1440                                                                                                                                                                                 attributes:labelDict];
1441         
1442         if (firstEntry) {
1443             firstEntry = NO;
1444         } else {
1445             //Add a carriage return and skip a line
1446             [tipString appendString:@"\n\n" withAttributes:labelEndLineDict];
1447         }
1448         
1449         //Add the label (with its spacing)
1450         [tipString appendAttributedString:labelString];
1451         [labelString release];
1453         NSRange fullLength = NSMakeRange(0, [entryString length]);
1454         
1455         //remove any background coloration
1456         [entryString removeAttribute:NSBackgroundColorAttributeName range:fullLength];
1457         
1458         //adjust foreground colors for the tooltip background
1459         [entryString adjustColorsToShowOnBackground:[NSColor colorWithCalibratedRed:1.000 green:1.000 blue:0.800 alpha:1.0]];
1461         //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
1462                 if ([entryString replaceOccurrencesOfString:@"\r" withString:@"\r\t\t" options:NSLiteralSearch range:fullLength])
1463             fullLength = NSMakeRange(0, [entryString length]);
1464         if ([entryString replaceOccurrencesOfString:@"\n" withString:@"\n\t\t" options:NSLiteralSearch range:fullLength])
1465             fullLength = NSMakeRange(0, [entryString length]);
1466                 
1467         //Run the entry through the filters and add it to tipString
1468                 entryString = [[[adium contentController] filterAttributedString:entryString
1469                                                                                                                  usingFilterType:AIFilterTooltips
1470                                                                                                                            direction:AIFilterIncoming
1471                                                                                                                                  context:object] mutableCopy];
1472                 
1473                 [entryString addAttributes:entryDict range:NSMakeRange(0,[entryString length])];
1474         [tipString appendAttributedString:entryString];
1475                 [entryString release];
1476     }
1478     return [tipString autorelease];
1481 //Custom pasting ----------------------------------------------------------------------------------------------------
1482 #pragma mark Custom Pasting
1483 //Paste, stripping formatting
1484 - (IBAction)paste:(id)sender
1486         [self _pasteWithPreferredSelector:@selector(pasteAsPlainTextWithTraits:) sender:sender];
1489 //Paste with formatting
1490 - (IBAction)pasteAndMatchStyle:(id)sender
1492         [self _pasteWithPreferredSelector:@selector(pasteAsPlainText:) sender:sender];
1495 - (IBAction)pasteWithImagesAndColors:(id)sender
1497         [self _pasteWithPreferredSelector:@selector(pasteAsRichText:) sender:sender];   
1501  * @brief Send a paste message, using preferredSelector if possible and paste: if not
1503  * Walks the responder chain looking for a responder which can handle pasting, skipping instances of
1504  * WebHTMLView.  These are skipped because we can control what paste does to WebView (by using a custom subclass) but
1505  * have no control over what the WebHTMLView would do.
1507  * If no responder is found, repeats the process looking for the simpler paste: selector.
1508  */
1509 - (void)_pasteWithPreferredSelector:(SEL)selector sender:(id)sender
1511         NSWindow        *keyWindow = [[NSApplication sharedApplication] keyWindow];
1512         NSResponder     *responder;
1514         //First, look for a responder which can handle the preferred selector
1515         if (!(responder = [keyWindow earliestResponderWhichRespondsToSelector:selector
1516                                                                                                                   andIsNotOfClass:NSClassFromString(@"WebHTMLView")])) {                
1517                 //No responder found.  Try again, looking for one which will respond to paste:
1518                 selector = @selector(paste:);
1519                 responder = [keyWindow earliestResponderWhichRespondsToSelector:selector
1520                                                                                                                 andIsNotOfClass:NSClassFromString(@"WebHTMLView")];
1521         }
1523         //Sending pasteAsRichText: to a non rich text NSTextView won't do anything; change it to a generic paste:
1524         if ([responder isKindOfClass:[NSTextView class]] && ![(NSTextView *)responder isRichText]) {
1525                 selector = @selector(paste:);
1526         }
1528         if (selector) {
1529                 [keyWindow makeFirstResponder:responder];
1530                 [responder performSelector:selector
1531                                                 withObject:sender];
1532         }
1535 //Custom Printing ------------------------------------------------------------------------------------------------------
1536 #pragma mark Custom Printing
1537 - (IBAction)adiumPrint:(id)sender
1539         //Pass the print command to the window, which is responsible for routing it to the correct place or
1540         //creating a view and printing.  Adium will not print from a window that does not respond to adiumPrint:
1541         NSWindow        *keyWindowController = [[[NSApplication sharedApplication] keyWindow] windowController];
1542         if ([keyWindowController respondsToSelector:@selector(adiumPrint:)]) {
1543                 [keyWindowController performSelector:@selector(adiumPrint:)
1544                                                                   withObject:sender];
1545         }
1548 #pragma mark Preferences Display
1549 - (IBAction)showPreferenceWindow:(id)sender
1551         [[adium preferenceController] showPreferenceWindow:sender];
1554 #pragma mark Font Panel
1555 - (IBAction)toggleFontPanel:(id)sender
1557         if ([NSFontPanel sharedFontPanelExists] &&
1558                 [[NSFontPanel sharedFontPanel] isVisible]) {
1559                 [[NSFontPanel sharedFontPanel] close];
1561         } else {
1562                 NSFontPanel     *fontPanel = [NSFontPanel sharedFontPanel];
1563                 
1564                 if (!fontPanelAccessoryView) {
1565                         [NSBundle loadNibNamed:@"FontPanelAccessoryView" owner:self];
1566                         [fontPanel setAccessoryView:fontPanelAccessoryView];
1567                 }
1568                 
1569                 [fontPanel orderFront:self]; 
1570         }
1573 - (IBAction)setFontPanelSettingsAsDefaultFont:(id)sender
1575         NSFont  *selectedFont = [[NSFontManager sharedFontManager] selectedFont];
1577         [[adium preferenceController] setPreference:[selectedFont stringRepresentation]
1578                                                                                  forKey:KEY_FORMATTING_FONT
1579                                                                                   group:PREF_GROUP_FORMATTING];
1580         
1581         //We can't get foreground/background color from the font panel so far as I can tell... so we do the best we can.
1582         NSWindow        *keyWindow = [[NSApplication sharedApplication] keyWindow];
1583         NSResponder *responder = [keyWindow firstResponder]; 
1584         if ([responder isKindOfClass:[NSTextView class]]) {
1585                 NSDictionary    *typingAttributes = [(NSTextView *)responder typingAttributes];
1586                 NSColor                 *foregroundColor, *backgroundColor;
1588                 if ((foregroundColor = [typingAttributes objectForKey:NSForegroundColorAttributeName])) {
1589                         [[adium preferenceController] setPreference:[foregroundColor stringRepresentation]
1590                                                                                                  forKey:KEY_FORMATTING_TEXT_COLOR
1591                                                                                                   group:PREF_GROUP_FORMATTING];
1592                 }
1594                 if ((backgroundColor = [typingAttributes objectForKey:AIBodyColorAttributeName])) {
1595                         [[adium preferenceController] setPreference:[backgroundColor stringRepresentation]
1596                                                                                                  forKey:KEY_FORMATTING_BACKGROUND_COLOR
1597                                                                                                   group:PREF_GROUP_FORMATTING];
1598                 }
1599         }
1602 //Custom Dimming menu items --------------------------------------------------------------------------------------------
1603 #pragma mark Custom Dimming menu items
1604 //The standard ones do not dim correctly when unavailable
1605 - (IBAction)toggleFontTrait:(id)sender
1607     NSFontManager       *fontManager = [NSFontManager sharedFontManager];
1608     
1609     if ([fontManager traitsOfFont:[fontManager selectedFont]] & [sender tag]) {
1610         [fontManager removeFontTrait:sender];
1611     } else {
1612         [fontManager addFontTrait:sender];
1613     }
1616 - (void)toggleToolbarShown:(id)sender
1618         NSWindow        *window = [[NSApplication sharedApplication] keyWindow];        
1619         [window toggleToolbarShown:sender];
1622 - (void)runToolbarCustomizationPalette:(id)sender
1624         NSWindow        *window = [[NSApplication sharedApplication] keyWindow];        
1625         [window runToolbarCustomizationPalette:sender];
1628 //Menu item validation
1629 - (BOOL)validateMenuItem:(NSMenuItem *)menuItem
1631         
1632         NSWindow        *keyWindow = [[NSApplication sharedApplication] keyWindow];
1633         NSResponder *responder = [keyWindow firstResponder]; 
1634         
1635     if (menuItem == menuItem_bold || menuItem == menuItem_italic) {
1636                 NSFont                  *selectedFont = [[NSFontManager sharedFontManager] selectedFont];
1637                 
1638                 //We must be in a text view, have text on the pasteboard, and have a font that supports bold or italic
1639                 if ([responder isKindOfClass:[NSTextView class]]) {
1640                         return (menuItem == menuItem_bold ? [selectedFont supportsBold] : [selectedFont supportsItalics]);
1641                 }
1642                 return NO;
1643                 
1644         } else if (menuItem == menuItem_paste || menuItem == menuItem_pasteAndMatchStyle || menuItem == menuItem_pasteWithImagesAndColors) {
1646                 //The user can paste if the pasteboard contains an image, some text, one or more files, or one or more URLs.
1647                 NSPasteboard *pboard = [NSPasteboard generalPasteboard];
1648                 NSArray *nonImageTypes = [NSArray arrayWithObjects:
1649                         NSStringPboardType,
1650                         NSRTFPboardType,
1651                         NSURLPboardType,
1652                         NSFilenamesPboardType,
1653                         NSFilesPromisePboardType,
1654                         NSRTFDPboardType,
1655                         nil];
1656                 return ([pboard availableTypeFromArray:nonImageTypes] != nil) || [NSImage canInitWithPasteboard:pboard];
1657         
1658         } else if (menuItem == menuItem_showToolbar) {
1659                 [menuItem_showToolbar setTitle:([[keyWindow toolbar] isVisible] ? 
1660                                                                                 AILocalizedString(@"Hide Toolbar",nil) : 
1661                                                                                 AILocalizedString(@"Show Toolbar",nil))];
1662                 return [keyWindow toolbar] != nil;
1663         
1664         } else if (menuItem == menuItem_customizeToolbar) {
1665                 return ([keyWindow toolbar] != nil && [[keyWindow toolbar] isVisible] && [[keyWindow windowController] canCustomizeToolbar]);
1667         } else if (menuItem == menuItem_close) {
1668                 return (keyWindow && ([[keyWindow standardWindowButton:NSWindowCloseButton] isEnabled] ||
1669                                                           ([[keyWindow windowController] respondsToSelector:@selector(windowPermitsClose)] &&
1670                                                            [[keyWindow windowController] windowPermitsClose])));
1671                 
1672         } else if (menuItem == menuItem_closeChat) {
1673                 return activeChat != nil;
1674                 
1675         } else if( menuItem == menuItem_closeAllChats) {
1676                 return [[self openChats] count] > 0;
1678         } else if (menuItem == menuItem_print) {
1679                 NSWindowController *windowController = [keyWindow windowController];
1681                 return ([windowController respondsToSelector:@selector(adiumPrint:)] &&
1682                                 (![windowController respondsToSelector:@selector(validatePrintMenuItem:)] ||
1683                                  [windowController validatePrintMenuItem:menuItem]));
1684                 
1685         } else if (menuItem == menuItem_showFonts) {
1686                 [menuItem_showFonts setTitle:(([NSFontPanel sharedFontPanelExists] && [[NSFontPanel sharedFontPanel] isVisible]) ?
1687                                                                           AILocalizedString(@"Hide Fonts",nil) :
1688                                                                           AILocalizedString(@"Show Fonts",nil))];
1689                 return YES;
1690         } else if (menuItem == menuItem_toggleUserlist) {
1691                         return [[self activeChat] isGroupChat];
1692         } else {
1693                 return YES;
1694         }
1697 #pragma mark Window levels
1698 - (NSMenu *)menuForWindowLevelsNotifyingTarget:(id)target
1700         NSMenu          *windowPositionMenu = [[NSMenu allocWithZone:[NSMenu zone]] init];
1701         NSMenuItem      *menuItem;
1702         
1703         menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Above other windows",nil)
1704                                                                                                                                         target:target
1705                                                                                                                                         action:@selector(selectedWindowLevel:)
1706                                                                                                                          keyEquivalent:@""];
1707         [menuItem setEnabled:YES];
1708         [menuItem setTag:AIFloatingWindowLevel];
1709         [windowPositionMenu addItem:menuItem];
1710         [menuItem release];
1711         
1712         menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Normally",nil)
1713                                                                                                                                         target:target
1714                                                                                                                                         action:@selector(selectedWindowLevel:)
1715                                                                                                                          keyEquivalent:@""];
1716         [menuItem setEnabled:YES];
1717         [menuItem setTag:AINormalWindowLevel];
1718         [windowPositionMenu addItem:menuItem];
1719         [menuItem release];
1720         
1721         menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Below other windows",nil)
1722                                                                                                                                         target:target
1723                                                                                                                                         action:@selector(selectedWindowLevel:)
1724                                                                                                                          keyEquivalent:@""];
1725         [menuItem setEnabled:YES];
1726         [menuItem setTag:AIDesktopWindowLevel];
1727         [windowPositionMenu addItem:menuItem];
1728         [menuItem release];
1729         
1730         [windowPositionMenu setAutoenablesItems:NO];
1732         return [windowPositionMenu autorelease];
1735 -(void)toggleUserlist:(id)sender
1737         NSLog(@"toggle userlist in interfaceController");
1738         [[adium notificationCenter] postNotificationName:@"toggleUserlist" object:nil];
1739 }       
1742 @end