libgaim.framework [386] which has extensive debugging in the upnp module to try to...
[adiumx.git] / Source / AIInterfaceController.m
blobc1a9e7789bda6e0a27f3a9189155a9e536af3b01
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"
47 #define ERROR_MESSAGE_WINDOW_TITLE              AILocalizedString(@"Adium : Error","Error message window title")
48 #define LABEL_ENTRY_SPACING                             4.0
49 #define DISPLAY_IMAGE_ON_RIGHT                  NO
51 #define PREF_GROUP_FORMATTING                   @"Formatting"
52 #define KEY_FORMATTING_FONT                             @"Default Font"
54 #define MESSAGES_WINDOW_MENU_TITLE              AILocalizedString(@"Chats","Title for the messages window menu item")
56 @interface AIInterfaceController (PRIVATE)
57 - (void)_resetOpenChatsCache;
58 - (void)_addItemToMainMenuAndDock:(NSMenuItem *)item;
59 - (NSAttributedString *)_tooltipTitleForObject:(AIListObject *)object;
60 - (NSAttributedString *)_tooltipBodyForObject:(AIListObject *)object;
61 - (void)_pasteWithPreferredSelector:(SEL)preferredSelector sender:(id)sender;
62 - (void)updateCloseMenuKeys;
64 //Window Menu
65 - (void)updateActiveWindowMenuItem;
66 - (void)buildWindowMenu;
68 - (AIChat *)mostRecentActiveChat;
69 @end
71 /*!
72  * @class AIInterfaceController
73  * @brief Interface controller
74  *
75  * Chat window related requests, such as opening and closing chats, are routed through the interface controller
76  * to the appropriate component. The interface controller keeps track of the most recently active chat, handles chat
77  * cycling (switching between chats), chat sorting, and so on.  The interface controller also handles switching to
78  * an appropriate window or chat when the dock icon is clicked for a 'reopen' event.
79  *
80  * Contact list window requests, such as toggling window visibilty are routed to the contact list controller component.
81  *
82  * Error messages are routed through the interface controller.
83  *
84  * Tooltips, such as seen on hover in the contact list are generated and displayed here.  Tooltip display components and
85  * plugins register with the interface controller to be queried for contact information when a tooltip is displayed.
86  *
87  * When displays in Adium flash, such as in the dock or the contact list for unviewed content, the interface controller
88  * manages keeping the flashing synchronized.
89  *
90  * Finally, the interface controller manages many menu items, providing better menu item validation and target routing
91  * than the responder chain alone would do.
92  */
93 @implementation AIInterfaceController
95 - (id)init
97         if ((self = [super init])) {
98                 /* Use KFTypeSelectTableView as our NSTableView base class to allow type-select searching of all
99                 * table and outline views throughout Adium.
100                 */
101                 [[KFTypeSelectTableView class] poseAsClass:[NSTableView class]];
102                 
103                 contactListViewArray = [[NSMutableArray alloc] init];
104                 messageViewArray = [[NSMutableArray alloc] init];
105                 contactListTooltipEntryArray = [[NSMutableArray alloc] init];
106                 contactListTooltipSecondaryEntryArray = [[NSMutableArray alloc] init];
107                 closeMenuConfiguredForChat = NO;
108                 _cachedOpenChats = nil;
109                 mostRecentActiveChat = nil;
110                 activeChat = nil;
111                 
112                 tooltipListObject = nil;
113                 tooltipTitle = nil;
114                 tooltipBody = nil;
115                 tooltipImage = nil;
116                 flashObserverArray = nil;
117                 flashTimer = nil;
118                 flashState = 0;
119                 
120                 windowMenuArray = nil;
121         }
122         
123         return self;
126 #if 0
127 //Can be called by a timer to periodically log the responder chain
128 //[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(reportResponderChain:) userInfo:nil repeats:YES];
129 - (void)reportResponderChain:(NSTimer *)inTimer
131         NSMutableString *responderChain = [NSMutableString string];
132         
133         NSWindow        *keyWindow = [[NSApplication sharedApplication] keyWindow];
134         [responderChain appendFormat:@"%@ (%i): ",keyWindow,[keyWindow respondsToSelector:@selector(print:)]];
135         
136         NSResponder     *responder = [keyWindow firstResponder];
137         
138         //First, walk down the responder chain looking for a responder which can handle the preferred selector
139         while (responder) {
140                 [responderChain appendFormat:@"%@ (%i)",responder,[responder respondsToSelector:@selector(print:)]];
141                 responder = [responder nextResponder];
142                 if (responder) [responderChain appendString:@" -> "];
143         }
145         NSLog(responderChain);
147 #endif
149 - (void)controllerDidLoad
151     //Load the interface
152     [interfacePlugin openInterface];
154         //Open the contact list window
155     [self showContactList:nil];
157         //Contact list menu tem
158     NSMenuItem *menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Contact List","Name of the window which lists contacts")
159                                                                                                                                                                 target:self
160                                                                                                                                                                 action:@selector(toggleContactList:)
161                                                                                                                                                  keyEquivalent:@"/"];
162         [[adium menuController] addMenuItem:menuItem toLocation:LOC_Window_Fixed];
163         [[adium menuController] addMenuItem:[[menuItem copy] autorelease] toLocation:LOC_Dock_Status];
164         [menuItem release];
166         menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Close Chat","Title for the close chat menu item")
167                                                                                                                                         target:self
168                                                                                                                                         action:@selector(closeContextualChat:)
169                                                                                                                          keyEquivalent:@""];
170         [[adium menuController] addContextualMenuItem:menuItem toLocation:Context_Tab_Action];
171         [menuItem release];
172         
173         //Observe preference changes
174         [[adium preferenceController] registerPreferenceObserver:self forGroup:PREF_GROUP_INTERFACE];
176     //Observe content so we can open chats as necessary
177     [[adium notificationCenter] addObserver:self selector:@selector(didReceiveContent:) 
178                                                                            name:CONTENT_MESSAGE_RECEIVED object:nil];
181 - (void)controllerWillClose
183     [contactListPlugin closeContactList];
184     [interfacePlugin closeInterface];
187 // Dealloc
188 - (void)dealloc
190     [contactListViewArray release]; contactListViewArray = nil;
191     [messageViewArray release]; messageViewArray = nil;
192     [interfaceArray release]; interfaceArray = nil;
193         
194     [tooltipListObject release]; tooltipListObject = nil;
195         [tooltipTitle release]; tooltipTitle = nil;
196         [tooltipBody release]; tooltipBody = nil;
197         [tooltipImage release]; tooltipImage = nil;
198         
199         [[adium notificationCenter] removeObserver:self];
200         [[adium preferenceController] unregisterPreferenceObserver:self];
201         
202     [super dealloc];
205 //Registers code to handle the interface
206 - (void)registerInterfaceController:(id <AIInterfaceComponent>)inController
208         if (!interfacePlugin) interfacePlugin = [inController retain];
211 //Register code to handle the contact list
212 - (void)registerContactListController:(id <AIContactListComponent>)inController
214         if (!contactListPlugin) contactListPlugin = [inController retain];
217 //Preferences changed
218 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key
219                                                         object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
221         //
222         [[adium notificationCenter] removeObserver:self name:Contact_OrderChanged object:nil];
223         
224         //Update prefs
225         tabbedChatting = [[prefDict objectForKey:KEY_TABBED_CHATTING] boolValue];
226         groupChatsByContactGroup = [[prefDict objectForKey:KEY_GROUP_CHATS_BY_GROUP] boolValue];
229 //Handle a reopen/dock icon click
230 - (BOOL)handleReopenWithVisibleWindows:(BOOL)visibleWindows
232     //The 'visibleWindows' variable passed by the system is unreliable, since the presence
233     //of the Adium system menu will cause it to always be YES.  We won't use it below.
235         //If no windows are visible, show the contact list
236         if (![contactListPlugin contactListIsVisibleAndMain] && [[interfacePlugin openContainers] count] == 0) {
237                 [self showContactList:nil];
238         } else {
239                 AIChat  *mostRecentUnviewedChat;
241                 //If windows are open, try switching to a chat with unviewed content
242                 if ((mostRecentUnviewedChat = [[adium chatController] mostRecentUnviewedChat])) {
243                         if ([mostRecentActiveChat unviewedContentCount]) {
244                                 //If the most recently active chat has unviewed content, ensure it is in the front
245                                 [self setActiveChat:mostRecentActiveChat];
247                         } else {
248                                 //Otherwise, switch to the chat which most recently received content
249                                 [self setActiveChat:mostRecentUnviewedChat];
250                         }
252                 } else {
253                         NSEnumerator    *enumerator;
254                         NSWindow            *window, *targetWindow = nil;
255                         BOOL            unMinimizedWindows = 0;
256                         
257                         //If there was no unviewed content, ensure that atleast one of Adium's windows is unminimized
258                         enumerator = [[NSApp windows] objectEnumerator];
259                         while ((window = [enumerator nextObject])) {
260                                 //Check stylemask to rule out the system menu's window (Which reports itself as visible like a real window)
261                                 if (([window styleMask] & (NSTitledWindowMask | NSClosableWindowMask | NSMiniaturizableWindowMask))) {
262                                         if (!targetWindow) targetWindow = window;
263                                         if (![window isMiniaturized]) unMinimizedWindows++;
264                                 }
265                         }
266                         
267                         //If there are no unminimized windows, unminimize the last one
268                         if (unMinimizedWindows == 0 && targetWindow) {
269                                 [targetWindow deminiaturize:nil];
270                         }
271                 }
272         }
273         
274         //We handled the reopen; return NO so NSApp does nothing.
275     return NO; 
278 //Contact List ---------------------------------------------------------------------------------------------------------
279 #pragma mark Contact list
280 //Toggle the contact list
281 - (IBAction)toggleContactList:(id)sender
283     if ([contactListPlugin contactListIsVisibleAndMain]) {
284                 [self closeContactList:nil];
285     } else {
286                 [self showContactList:nil];
287     } 
290 //Show the contact list window
291 - (IBAction)showContactList:(id)sender
293         [contactListPlugin showContactListAndBringToFront:YES];
296 //Show the contact list window and bring Adium to the front
297 - (IBAction)showContactListAndBringToFront:(id)sender
299         [contactListPlugin showContactListAndBringToFront:YES];
300         [[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
303 //Close the contact list window
304 - (IBAction)closeContactList:(id)sender
306         [contactListPlugin closeContactList];
310 //Messaging ------------------------------------------------------------------------------------------------------------
311 //Methods for instructing the interface to provide a representation of chats, and to determine which chat has user focus
312 #pragma mark Messaging
313 //Open a window for the chat
314 - (void)openChat:(AIChat *)inChat
316         NSArray         *containers = [interfacePlugin openContainersAndChats];
317         NSString        *containerID = nil;
318         
319         //Determine the correct container for this chat
320         
321         if (!tabbedChatting) {
322                 //We're not using tabs; each chat starts in its own container, based on the destination object or the chat name
323                 if ([inChat listObject]) {
324                         containerID = [[inChat listObject] internalObjectID];
325                 } else {
326                         containerID = [inChat name];
327                 }
328                 
329         } else if (groupChatsByContactGroup) {
330                 if ([inChat isGroupChat]) {
331                         containerID = AILocalizedString(@"Group Chat",nil);
332                         
333                 } else {
334                         AIListObject    *group = [[[inChat listObject] parentContact] containingObject];
335                         
336                         //If the contact is in the contact list root, we don't have a group
337                         if (group && (group != [[adium contactController] contactList])) {
338                                 containerID = [group displayName];
339                         }
340                 }
341         }
342         
343         if (!containerID) {
344                 //Open new chats into the first container (if not available, create a new one)
345                 if ([containers count] > 0) {
346                         containerID = [[containers objectAtIndex:0] objectForKey:@"ID"];
347                 } else {
348                         containerID = AILocalizedString(@"Chat",nil);
349                 }
350         }
352         //Determine the correct placement for this chat within the container
353         [interfacePlugin openChat:inChat inContainerWithID:containerID atIndex:-1];
354         if (![inChat isOpen]) {
355                 [inChat setIsOpen:YES];
356                 
357                 //Post the notification last, so observers receive a chat whose isOpen flag is yes.
358                 [[adium notificationCenter] postNotificationName:Chat_DidOpen object:inChat userInfo:nil];
359         }
363  * @brief Close the interface for a chat
365  * Tell the interface plugin to close the chat.
366  */
367 - (void)closeChat:(AIChat *)inChat
369         if (inChat) {
370                 if ([[adium chatController] closeChat:inChat]) {
371                         [interfacePlugin closeChat:inChat];
372                 }
373         }
376 //Consolidate chats into a single container
377 - (void)consolidateChats
379         //We work with copies of these arrays, since moving chats may change their contents
380         NSArray                 *openContainers = [[interfacePlugin openContainers] copy];
381         NSEnumerator    *containerEnumerator = [openContainers objectEnumerator];
382         NSString                *firstContainerID = [containerEnumerator nextObject];
383         NSString                *containerID;
384         
385         //For all containers but the first, move the chats they contain to the first container
386         while ((containerID = [containerEnumerator nextObject])) {
387                 NSArray                 *openChats = [[interfacePlugin openChatsInContainerWithID:containerID] copy];
388                 NSEnumerator    *chatEnumerator = [openChats objectEnumerator];
389                 AIChat                  *chat;
391                 //Move all the chats, providing a target index if chat sorting is enabled
392                 while ((chat = [chatEnumerator nextObject])) {
393                         [interfacePlugin moveChat:chat
394                                         toContainerWithID:firstContainerID
395                                                                 index:-1];
396                 }
397                 
398                 [openChats release];
399         }
400         
401         [self chatOrderDidChange];
402         
403         [openContainers release];
406 //Active chat
407 - (AIChat *)activeChat
409         return activeChat;
411 //Set the active chat window
412 - (void)setActiveChat:(AIChat *)inChat
414         [interfacePlugin setActiveChat:inChat];
416 //Last chat to be active (should only be nil if no chats are open)
417 - (AIChat *)mostRecentActiveChat
419         return mostRecentActiveChat;
421 //Solely for key-value pairing purposes
422 - (void)setMostRecentActiveChat:(AIChat *)inChat
424         [self setActiveChat:inChat];
427 //Returns an array of open chats (cached, so call as frequently as desired)
428 - (NSArray *)openChats
430         if (!_cachedOpenChats) {
431                 _cachedOpenChats = [[interfacePlugin openChats] retain];
432         }
433         
434         return _cachedOpenChats;
438 - (NSArray *)openChatsInContainerWithID:(NSString *)containerID
440         return [interfacePlugin openChatsInContainerWithID:containerID];
443 //Resets the cache of open chats
444 - (void)_resetOpenChatsCache
446         [_cachedOpenChats release]; _cachedOpenChats = nil;
451 //Interface plugin callbacks -------------------------------------------------------------------------------------------
452 //These methods are called by the interface to let us know what's going on.  We're informed of chats opening, closing,
453 //changing order, etc.
454 #pragma mark Interface plugin callbacks
455 //A chat window did open: rebuild our window menu to show the new chat
456 - (void)chatDidOpen:(AIChat *)inChat
458         [self _resetOpenChatsCache];
459         [self buildWindowMenu];
462 //A chat has become active: update our chat closing keys and flag this chat as selected in the window menu
463 - (void)chatDidBecomeActive:(AIChat *)inChat
465         AIChat  *previouslyActiveChat = activeChat;
466         
467         activeChat = [inChat retain];
468         
469         [self updateCloseMenuKeys];
470         [self updateActiveWindowMenuItem];
471         
472         if (inChat && (inChat != mostRecentActiveChat)) {
473                 [mostRecentActiveChat release]; mostRecentActiveChat = nil;
474                 mostRecentActiveChat = [inChat retain];
475         }
476         
477         [[adium notificationCenter] postNotificationName:Chat_BecameActive
478                                                                                           object:inChat 
479                                                                                         userInfo:(previouslyActiveChat ?
480                                                                                                           [NSDictionary dictionaryWithObject:previouslyActiveChat
481                                                                                                                                                                   forKey:@"PreviouslyActiveChat"] :
482                                                                                                           nil)];
483         
484         if (inChat) {
485                 /* Clear the unviewed content on the next event loop so other methods have a chance to react to the chat becoming
486                 * active. Specifically, this lets the handleReopenWithVisibleWindows: method have a chance to know that this chat
487                 * had unviewed content.
488                 */
489                 [inChat performSelector:@selector(clearUnviewedContentCount)
490                                          withObject:nil
491                                          afterDelay:0];
492         }
493         
494         [previouslyActiveChat release]; 
497 //A chat has become visible: send out a notification for components and plugins to take action
498 - (void)chatDidBecomeVisible:(AIChat *)inChat inWindow:(NSWindow *)inWindow
500         [[adium notificationCenter] postNotificationName:@"AIChatDidBecomeVisible"
501                                                                                           object:inChat
502                                                                                         userInfo:[NSDictionary dictionaryWithObject:inWindow
503                                                                                                                                                                  forKey:@"NSWindow"]];
507  * @brief Find the window currently displaying a chat
509  * If the chat is not in any window, or is not visible in any window, returns nil
510  */
511 - (NSWindow *)windowForChat:(AIChat *)inChat
513         return [interfacePlugin windowForChat:inChat];
517  * @brief Find the chat active in a window
519  * If the window does not have an active chat, nil is returned
520  */
521 - (AIChat *)activeChatInWindow:(NSWindow *)window
523         return [interfacePlugin activeChatInWindow:window];
526 //A chat window did close: rebuild our window menu to remove the chat
527 - (void)chatDidClose:(AIChat *)inChat
529         [self _resetOpenChatsCache];
530         [inChat clearUnviewedContentCount];
531         [self buildWindowMenu];
532         
533         if (inChat == activeChat) {
534                 [activeChat release]; activeChat = nil;
535         }
536         
537         if (inChat == mostRecentActiveChat) {
538                 [mostRecentActiveChat release]; mostRecentActiveChat = nil;
539         }
542 //The order of chats has changed: rebuild our window menu to reflect the new order
543 - (void)chatOrderDidChange
545         [self _resetOpenChatsCache];
546         [self buildWindowMenu];
549 #pragma mark Unviewed content
551 //Content was received, increase the unviewed content count of the chat (if it's not currently active)
552 - (void)didReceiveContent:(NSNotification *)notification
554         AIChat          *chat = [[notification userInfo] objectForKey:@"AIChat"];
555         
556         if (chat != activeChat) {
557                 [chat incrementUnviewedContentCount];
558         }
562 //Chat close menus -----------------------------------------------------------------------------------------------------
563 #pragma mark Chat close menus
564 //Close the active window
565 - (IBAction)closeMenu:(id)sender
567     [[[NSApplication sharedApplication] keyWindow] performClose:nil];
570 //Close the active chat
571 - (IBAction)closeChatMenu:(id)sender
573         if (activeChat) [self closeChat:activeChat];
576 - (IBAction)closeContextualChat:(id)sender
578         [self closeChat:[[adium menuController] currentContextMenuChat]];
581 //Loop through open chats and close them
582 - (IBAction)closeAllChats:(id)sender
584         NSEnumerator    *containerEnumerator = [[[[interfacePlugin openChats] copy] autorelease] objectEnumerator];
585         AIChat                  *chatToClose;
587         while ((chatToClose = [containerEnumerator nextObject])) {
588                 [self closeChat:chatToClose];
589         }
592 //Updates the key equivalents on 'close' and 'close chat' (dynamically changed to make cmd-w less destructive)
593 - (void)updateCloseMenuKeys
595         if (activeChat && !closeMenuConfiguredForChat) {
596         [menuItem_close setKeyEquivalent:@"W"];
597         [menuItem_closeChat setKeyEquivalent:@"w"];
598                 closeMenuConfiguredForChat = YES;
599         } else if (!activeChat && closeMenuConfiguredForChat) {
600         [menuItem_close setKeyEquivalent:@"w"];
601                 [menuItem_closeChat removeKeyEquivalent];               
602                 closeMenuConfiguredForChat = NO;
603         }
607 //Window Menu ----------------------------------------------------------------------------------------------------------
608 #pragma mark Window Menu
609 //Make a chat window active (Invoked by a selection in the window menu)
610 - (IBAction)showChatWindow:(id)sender
612         [self setActiveChat:[sender representedObject]];
613     [[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
616 //Updates the 'check' icon so it's next to the active window
617 - (void)updateActiveWindowMenuItem
619     NSEnumerator        *enumerator = [windowMenuArray objectEnumerator];
620     NSMenuItem          *item;
622     while ((item = [enumerator nextObject])) {
623                 if ([item representedObject]) [item setState:([item representedObject] == activeChat ? NSOnState : NSOffState)];
624     }
627 //Builds the window menu
628 //This function gets called whenever chats are opened, closed, or re-ordered - so improvements and optimizations here
629 //would probably be helpful
630 - (void)buildWindowMenu
631 {       
632     NSMenuItem                          *item;
633     NSEnumerator                        *enumerator;
634     int                                         windowKey = 1;
635         
636     //Remove any existing menus
637     enumerator = [windowMenuArray objectEnumerator];
638     while ((item = [enumerator nextObject])) {
639         [[adium menuController] removeMenuItem:item];
640     }
641     [windowMenuArray release]; windowMenuArray = [[NSMutableArray alloc] init];
642         
643     //Messages window and any open messasges
644         NSEnumerator    *containerEnumerator = [[interfacePlugin openContainersAndChats] objectEnumerator];
645         NSDictionary    *containerDict;
646         
647         while ((containerDict = [containerEnumerator nextObject])) {
648                 NSString                *containerName = [containerDict objectForKey:@"Name"];
649                 NSArray                 *contentArray = [containerDict objectForKey:@"Content"];
650                 NSEnumerator    *contentEnumerator = [contentArray objectEnumerator];
651                 AIChat                  *chat;
652                 
653                 //Add a menu item for the container
654                 if ([contentArray count] > 1) {
655                         item = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:containerName
656                                                                                                                                                 target:nil
657                                                                                                                                                 action:nil
658                                                                                                                                  keyEquivalent:@""];
659                         [self _addItemToMainMenuAndDock:item];
660                         [item release];
661                 }
662                 
663                 //Add items for the chats it contains
664                 while ((chat = [contentEnumerator nextObject])) {
665                         NSString                *windowKeyString;
666                         
667                         //Prepare a key equivalent for the controller
668                         if (windowKey < 10) {
669                                 windowKeyString = [NSString stringWithFormat:@"%i",(windowKey)];
670                         } else if (windowKey == 10) {
671                                 windowKeyString = [NSString stringWithString:@"0"];
672                         } else {
673                                 windowKeyString = [NSString stringWithString:@""];
674                         }
675                         
676                         item = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:[chat displayName]
677                                                                                                                                                 target:self
678                                                                                                                                                 action:@selector(showChatWindow:)
679                                                                                                                                  keyEquivalent:windowKeyString];
680                         if ([contentArray count] > 1) [item setIndentationLevel:1];
681                         [item setRepresentedObject:chat];
682                         [item setImage:[chat chatMenuImage]];
683                         [self _addItemToMainMenuAndDock:item];
684                         [item release];
686                         windowKey++;
687                 }
688         }
690         [self updateActiveWindowMenuItem];
693 //Adds a menu item to the internal array, dock menu, and main menu
694 - (void)_addItemToMainMenuAndDock:(NSMenuItem *)item
696         //Add to main menu first
697         [[adium menuController] addMenuItem:item toLocation:LOC_Window_Fixed];
698         [windowMenuArray addObject:item];
699         
700         //Make a copy, and add to the dock
701         item = [item copy];
702         [item setKeyEquivalent:@""];
703         [[adium menuController] addMenuItem:item toLocation:LOC_Dock_Status];
704         [windowMenuArray addObject:item];
705         [item release];
709 //Chat Cycling ---------------------------------------------------------------------------------------------------------
710 #pragma mark Chat Cycling
711 //Select the next message
712 - (void)nextChat:(id)sender
714         NSArray *openChats = [self openChats];
716         if ([openChats count]) {
717                 if (activeChat) {
718                         int chatIndex = [openChats indexOfObject:activeChat]+1;
719                         [self setActiveChat:[openChats objectAtIndex:(chatIndex < [openChats count] ? chatIndex : 0)]];
720                 } else {
721                         [self setActiveChat:[openChats objectAtIndex:0]];
722                 }
723         }
726 //Select the previous message
727 - (void)previousChat:(id)sender
729         NSArray *openChats = [self openChats];
730         
731         if ([openChats count]) {
732                 if (activeChat) {
733                         int chatIndex = [openChats indexOfObject:activeChat]-1;
734                         [self setActiveChat:[openChats objectAtIndex:(chatIndex >= 0 ? chatIndex : [openChats count]-1)]];
735                 } else {
736                         [self setActiveChat:[openChats lastObject]];
737                 }
738         }
741 //Selected contact ------------------------------------------------
742 #pragma mark Selected contact
743 - (id)_performSelectorOnFirstAvailableResponder:(SEL)selector
745     NSResponder *responder = [[[NSApplication sharedApplication] mainWindow] firstResponder];
746     //Check the first responder
747     if ([responder respondsToSelector:selector]) {
748         return [responder performSelector:selector];
749     }
750         
751     //Search the responder chain
752     do{
753         responder = [responder nextResponder];
754         if ([responder respondsToSelector:selector]) {
755             return [responder performSelector:selector];
756         }
757                 
758     } while (responder != nil);
759         
760     //None found, return nil
761     return nil;
763 - (id)_performSelectorOnFirstAvailableResponder:(SEL)selector conformingToProtocol:(Protocol *)protocol
765         NSResponder *responder = [[[NSApplication sharedApplication] mainWindow] firstResponder];
766         //Check the first responder
767         if ([responder conformsToProtocol:protocol] && [responder respondsToSelector:selector]) {
768                 return [responder performSelector:selector];
769         }
770         
771     //Search the responder chain
772     do{
773         responder = [responder nextResponder];
774         if ([responder conformsToProtocol:protocol] && [responder respondsToSelector:selector]) {
775             return [responder performSelector:selector];
776         }
777                 
778     } while (responder != nil);
779         
780     //None found, return nil
781     return nil;
784 //Returns the "selected"(represented) contact (By finding the first responder that returns a contact)
785 //If no listObject is found, try to find a list object selected in a group chat
786 - (AIListObject *)selectedListObject
788         AIListObject *listObject = [self _performSelectorOnFirstAvailableResponder:@selector(listObject)];
789         if ( !listObject) {
790                 listObject = [self _performSelectorOnFirstAvailableResponder:@selector(preferredListObject)];
791         }
792         return listObject;
794 - (AIListObject *)selectedListObjectInContactList
796         return [self _performSelectorOnFirstAvailableResponder:@selector(listObject) conformingToProtocol:@protocol(ContactListOutlineView)];
798 - (NSArray *)arrayOfSelectedListObjectsInContactList
800         return [self _performSelectorOnFirstAvailableResponder:@selector(arrayOfListObjects) conformingToProtocol:@protocol(ContactListOutlineView)];
803 //Message View ---------------------------------------------------------------------------------------------------------
804 //Message view is abstracted from the containing interface, since they're not directly related to eachother
805 #pragma mark Message View
806 //Registers a view to handle the contact list
807 - (void)registerMessageViewPlugin:(id <AIMessageViewPlugin>)inPlugin
809     [messageViewArray addObject:inPlugin];
811 - (id <AIMessageViewController>)messageViewControllerForChat:(AIChat *)inChat
813         //Sometimes our users find it amusing to disable plugins that are located within the Adium bundle.  This error
814         //trap prevents us from crashing if they happen to disable all the available message view plugins.
815         //PUT THAT PLUGIN BACK IT WAS IMPORTANT!
816         if ([messageViewArray count] == 0) {
817                 NSRunCriticalAlertPanel(@"No Message View Plugin Installed",
818                                                                 @"Adium cannot find its message view plugin, please re-install.  If you've manually disabled Adium's message view plugin, please re-enable it.",
819                                                                 @"Quit",
820                                                                 nil,
821                                                                 nil);
822                 [NSApp terminate:nil];
823         }
824         
825         return [[messageViewArray objectAtIndex:0] messageViewControllerForChat:inChat];
829 //Error Display --------------------------------------------------------------------------------------------------------
830 #pragma mark Error Display
831 - (void)handleErrorMessage:(NSString *)inTitle withDescription:(NSString *)inDesc
833     [self handleMessage:inTitle withDescription:inDesc withWindowTitle:ERROR_MESSAGE_WINDOW_TITLE];
836 - (void)handleMessage:(NSString *)inTitle withDescription:(NSString *)inDesc withWindowTitle:(NSString *)inWindowTitle;
838     NSDictionary        *errorDict;
839     
840     //Post a notification that an error was recieved
841     errorDict = [NSDictionary dictionaryWithObjectsAndKeys:inTitle,@"Title",inDesc,@"Description",inWindowTitle,@"Window Title",nil];
842     [[adium notificationCenter] postNotificationName:Interface_ShouldDisplayErrorMessage object:nil userInfo:errorDict];
845 //Display then clear the last disconnection error
846 - (void)account:(AIAccount *)inAccount disconnectedWithError:(NSString *)disconnectionError
848 //      [AdiumDisconnectionErrorController account:inAccount disconnectedWithError:disconnectionError];
851 //Question Display -----------------------------------------------------------------------------------------------------
852 #pragma mark Question Display
853 - (void)displayQuestion:(NSString *)inTitle withAttributedDescription:(NSAttributedString *)inDesc withWindowTitle:(NSString *)inWindowTitle
854                   defaultButton:(NSString *)inDefaultButton alternateButton:(NSString *)inAlternateButton otherButton:(NSString *)inOtherButton
855                                  target:(id)inTarget selector:(SEL)inSelector userInfo:(id)inUserInfo
857         NSMutableDictionary *questionDict = [NSMutableDictionary dictionary];
858         
859         if(inTitle != nil)
860                 [questionDict setObject:inTitle forKey:@"Title"];
861         if(inDesc != nil)
862                 [questionDict setObject:inDesc forKey:@"Description"];
863         if(inWindowTitle != nil)
864                 [questionDict setObject:inWindowTitle forKey:@"Window Title"];
865         if(inDefaultButton != nil)
866                 [questionDict setObject:inDefaultButton forKey:@"Default Button"];
867         if(inAlternateButton != nil)
868                 [questionDict setObject:inAlternateButton forKey:@"Alternate Button"];
869         if(inOtherButton != nil)
870                 [questionDict setObject:inOtherButton forKey:@"Other Button"];
871         if(inTarget != nil)
872                 [questionDict setObject:inTarget forKey:@"Target"];
873         if(inSelector != NULL)
874                 [questionDict setObject:NSStringFromSelector(inSelector) forKey:@"Selector"];
875         if(inUserInfo != nil)
876                 [questionDict setObject:inUserInfo forKey:@"Userinfo"];
877         
878         [[adium notificationCenter] postNotificationName:Interface_ShouldDisplayQuestion object:nil userInfo:questionDict];
881 - (void)displayQuestion:(NSString *)inTitle withDescription:(NSString *)inDesc withWindowTitle:(NSString *)inWindowTitle
882                   defaultButton:(NSString *)inDefaultButton alternateButton:(NSString *)inAlternateButton otherButton:(NSString *)inOtherButton
883                                  target:(id)inTarget selector:(SEL)inSelector userInfo:(id)inUserInfo
885         [self displayQuestion:inTitle
886 withAttributedDescription:[[[NSAttributedString alloc] initWithString:inDesc
887                                                                                                                    attributes:[NSDictionary dictionaryWithObject:[NSFont systemFontOfSize:0]
888                                                                                                                                                                                                   forKey:NSFontAttributeName]] autorelease]
889                   withWindowTitle:inWindowTitle
890                         defaultButton:inDefaultButton
891                   alternateButton:inAlternateButton
892                           otherButton:inOtherButton
893                                    target:inTarget
894                                  selector:inSelector
895                                  userInfo:inUserInfo];
897 //Synchronized Flashing ------------------------------------------------------------------------------------------------
898 #pragma mark Synchronized Flashing
899 //Register to observe the synchronized flashing
900 - (void)registerFlashObserver:(id <AIFlashObserver>)inObserver
902     //Setup the timer if we don't have one yet
903     if (!flashObserverArray) {
904         flashObserverArray = [[NSMutableArray alloc] init];
905         flashTimer = [[NSTimer scheduledTimerWithTimeInterval:(1.0/2.0) 
906                                                        target:self 
907                                                      selector:@selector(flashTimer:) 
908                                                      userInfo:nil
909                                                       repeats:YES] retain];
910     }
911     
912     //Add the new observer to the array
913     [flashObserverArray addObject:inObserver];
916 //Unregister from observing flashing
917 - (void)unregisterFlashObserver:(id <AIFlashObserver>)inObserver
919     //Remove the observer from our array
920     [flashObserverArray removeObject:inObserver];
921     
922     //Release the observer array and uninstall the timer
923     if ([flashObserverArray count] == 0) {
924         [flashObserverArray release]; flashObserverArray = nil;
925         [flashTimer invalidate];
926         [flashTimer release]; flashTimer = nil;
927     }
930 //Timer, invoke a flash
931 - (void)flashTimer:(NSTimer *)inTimer
933     NSEnumerator        *enumerator;
934     id<AIFlashObserver> observer;
935     
936     flashState++;
937     
938     enumerator = [flashObserverArray objectEnumerator];
939     while ((observer = [enumerator nextObject])) {
940         [observer flash:flashState];
941     }
944 //Current state of flashing.  This is an integer the increases by 1 with every flash.  Mod to whatever range is desired
945 - (int)flashState
947     return flashState;
951 //Tooltips -------------------------------------------------------------------------------------------------------------
952 #pragma mark Tooltips
953 //Registers code to display tooltip info about a contact
954 - (void)registerContactListTooltipEntry:(id <AIContactListTooltipEntry>)inEntry secondaryEntry:(BOOL)isSecondary
956     if (isSecondary)
957         [contactListTooltipSecondaryEntryArray addObject:inEntry];
958     else
959         [contactListTooltipEntryArray addObject:inEntry];
962 //list object tooltips
963 - (void)showTooltipForListObject:(AIListObject *)object atScreenPoint:(NSPoint)point onWindow:(NSWindow *)inWindow 
965     if (object) {
966         if (object == tooltipListObject) { //If we already have this tooltip open
967                                          //Move the existing tooltip
968             [AITooltipUtilities showTooltipWithTitle:tooltipTitle
969                                                                                                 body:tooltipBody
970                                                                                            image:tooltipImage 
971                                                                                 imageOnRight:DISPLAY_IMAGE_ON_RIGHT 
972                                                                                         onWindow:inWindow
973                                                                                          atPoint:point 
974                                                                                  orientation:TooltipBelow];
975             
976         } else { //This is a new tooltip
977             NSArray                     *tabArray;
978             NSMutableParagraphStyle     *paragraphStyleTitle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
979             NSMutableParagraphStyle     *paragraphStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
980             
981             //Hold onto the new object
982             [tooltipListObject release]; tooltipListObject = [object retain];
983             
984             //Buddy Icon
985             [tooltipImage release];
986                         tooltipImage = [[tooltipListObject userIcon] retain];
987                         if (!tooltipImage) tooltipImage = [[AIServiceIcons serviceIconForObject:tooltipListObject
988                                                                                                                                                          type:AIServiceIconLarge
989                                                                                                                                                 direction:AIIconNormal] retain];
990             
991             //Reset the maxLabelWidth for the tooltip generation
992             maxLabelWidth = 0;
993             
994             //Build a tooltip string for the primary information
995             [tooltipTitle release]; tooltipTitle = [[self _tooltipTitleForObject:object] retain];
996             
997             //If there is an image, set the title tab and indentation settings independently
998             if (tooltipImage) {
999                 //Set a right-align tab at the maximum label width and a left-align just past it
1000                 tabArray = [[NSArray alloc] initWithObjects:[[[NSTextTab alloc] initWithType:NSRightTabStopType 
1001                                                                                                                                                                         location:maxLabelWidth] autorelease]
1002                                                             ,[[[NSTextTab alloc] initWithType:NSLeftTabStopType 
1003                                                                                    location:maxLabelWidth + LABEL_ENTRY_SPACING] autorelease]
1004                                                             ,nil];
1005                 
1006                 [paragraphStyleTitle setTabStops:tabArray];
1007                 [tabArray release];
1008                 tabArray = nil;
1009                 [paragraphStyleTitle setHeadIndent:(maxLabelWidth + LABEL_ENTRY_SPACING)];
1010                 
1011                 [tooltipTitle addAttribute:NSParagraphStyleAttributeName 
1012                                      value:paragraphStyleTitle
1013                                      range:NSMakeRange(0,[tooltipTitle length])];
1014                 
1015                 //Reset the max label width since the body will be independent
1016                 maxLabelWidth = 0;
1017             }
1018             
1019             //Build a tooltip string for the secondary information
1020             [tooltipBody release]; tooltipBody = nil;
1021             tooltipBody = [[self _tooltipBodyForObject:object] retain];
1022             
1023             //Set a right-align tab at the maximum label width for the body and a left-align just past it
1024             tabArray = [[NSArray alloc] initWithObjects:[[[NSTextTab alloc] initWithType:NSRightTabStopType 
1025                                                                                  location:maxLabelWidth] autorelease]
1026                                                         ,[[[NSTextTab alloc] initWithType:NSLeftTabStopType 
1027                                                                                 location:maxLabelWidth + LABEL_ENTRY_SPACING] autorelease]
1028                                                         ,nil];
1029             [paragraphStyle setTabStops:tabArray];
1030             [tabArray release];
1031             [paragraphStyle setHeadIndent:(maxLabelWidth + LABEL_ENTRY_SPACING)];
1032             
1033             [tooltipBody addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:NSMakeRange(0,[tooltipBody length])];
1034             //If there is no image, also use these settings for the top part
1035             if (!tooltipImage) {
1036                 [tooltipTitle addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:NSMakeRange(0,[tooltipTitle length])];
1037             }
1038             
1039             //Display the new tooltip
1040             [AITooltipUtilities showTooltipWithTitle:tooltipTitle
1041                                                 body:tooltipBody 
1042                                                image:tooltipImage
1043                                         imageOnRight:DISPLAY_IMAGE_ON_RIGHT
1044                                             onWindow:inWindow
1045                                              atPoint:point 
1046                                          orientation:TooltipBelow];
1047                         
1048                         [paragraphStyleTitle release];
1049                         [paragraphStyle release];
1050         }
1051         
1052     } else {
1053         //Hide the existing tooltip
1054         if (tooltipListObject) {
1055             [AITooltipUtilities showTooltipWithTitle:nil 
1056                                                 body:nil
1057                                                image:nil 
1058                                             onWindow:nil
1059                                              atPoint:point
1060                                          orientation:TooltipBelow];
1061             [tooltipListObject release]; tooltipListObject = nil;
1062                         
1063                         [tooltipTitle release]; tooltipTitle = nil;
1064                         [tooltipBody release]; tooltipBody = nil;
1065                         [tooltipImage release]; tooltipImage = nil;
1066         }
1067     }
1070 - (NSAttributedString *)_tooltipTitleForObject:(AIListObject *)object
1072     NSMutableAttributedString           *titleString = [[NSMutableAttributedString alloc] init];
1073     
1074     id <AIContactListTooltipEntry>              tooltipEntry;
1075     NSEnumerator                                                *enumerator;
1076     NSEnumerator                        *labelEnumerator;
1077     NSMutableArray                      *labelArray = [NSMutableArray array];
1078     NSMutableArray                      *entryArray = [NSMutableArray array];
1079     NSMutableAttributedString           *entryString;
1080     float                               labelWidth;
1081     BOOL                                isFirst = YES;
1082     
1083     NSString                            *formattedUID = [object formattedUID];
1084     
1085     //Configure fonts and attributes
1086     NSFontManager                       *fontManager = [NSFontManager sharedFontManager];
1087     NSFont                              *toolTipsFont = [NSFont toolTipsFontOfSize:10];
1088     NSMutableDictionary                 *titleDict = [NSMutableDictionary dictionaryWithObjectsAndKeys:
1089         [fontManager convertFont:[NSFont toolTipsFontOfSize:12] toHaveTrait:NSBoldFontMask],NSFontAttributeName, nil];
1090     NSMutableDictionary                 *labelDict = [NSMutableDictionary dictionaryWithObjectsAndKeys:
1091         [fontManager convertFont:[NSFont toolTipsFontOfSize:9] toHaveTrait:NSBoldFontMask], NSFontAttributeName, nil];
1092     NSMutableDictionary                 *labelEndLineDict = [NSMutableDictionary dictionaryWithObjectsAndKeys:[NSFont toolTipsFontOfSize:2] , NSFontAttributeName, nil];
1093     NSMutableDictionary                 *entryDict =[NSMutableDictionary dictionaryWithObjectsAndKeys:
1094         toolTipsFont, NSFontAttributeName, nil];
1095         
1096         //Get the user's display name as an attributed string
1097     NSAttributedString                  *displayName = [[NSAttributedString alloc] initWithString:[object displayName]
1098                                                                                                                                                                            attributes:titleDict];
1099         NSAttributedString                                      *filteredDisplayName = [[adium contentController] filterAttributedString:displayName
1100                                                                                                                                                                                                  usingFilterType:AIFilterTooltips
1101                                                                                                                                                                                                            direction:AIFilterIncoming
1102                                                                                                                                                                                                                  context:nil];
1103         
1104         //Append the user's display name
1105         if (filteredDisplayName) {
1106                 [titleString appendAttributedString:filteredDisplayName];
1107         }
1108         
1109         //Append the user's formatted UID if there is one that's different to the display name
1110         if (formattedUID && (!([[[displayName string] compactedString] isEqualToString:[formattedUID compactedString]]))) {
1111                 [titleString appendString:[NSString stringWithFormat:@" (%@)", formattedUID] withAttributes:titleDict];
1112         }
1113         [displayName release];
1114     
1115     if ([object isKindOfClass:[AIListContact class]]) {
1116                 if ((![object isKindOfClass:[AIMetaContact class]] || [(AIMetaContact *)object containsOnlyOneService]) &&
1117                         [object userIcon]) {
1118                         NSImage *serviceIcon = [[AIServiceIcons serviceIconForObject:object type:AIServiceIconSmall direction:AIIconNormal]
1119                                                                         imageByScalingToSize:NSMakeSize(14,14)];
1120                         if (serviceIcon) {
1121                                 NSTextAttachment                *attachment;
1122                                 NSTextAttachmentCell    *cell;
1123                                 
1124                                 cell = [[NSTextAttachmentCell alloc] init];
1125                                 [cell setImage:serviceIcon];
1126                                 
1127                                 attachment = [[NSTextAttachment alloc] init];
1128                                 [attachment setAttachmentCell:cell];
1129                                 [cell release];
1130         
1131                                 [titleString appendString:@" " withAttributes:nil];
1132                                 [titleString appendAttributedString:[NSAttributedString attributedStringWithAttachment:attachment]];
1133                                 [attachment release];
1134                         }
1135                 }
1136         }
1137                 
1138     if ([object isKindOfClass:[AIListGroup class]]) {
1139         [titleString appendString:[NSString stringWithFormat:@" (%i/%i)",[(AIListGroup *)object visibleCount],[(AIListGroup *)object containedObjectsCount]] 
1140                    withAttributes:titleDict];
1141     }
1142     
1143     //Entries from plugins
1144     
1145     //Calculate the widest label while loading the arrays
1146     enumerator = [contactListTooltipEntryArray objectEnumerator];
1147     
1148     while ((tooltipEntry = [enumerator nextObject])) {
1149         
1150         entryString = [[tooltipEntry entryForObject:object] mutableCopy];
1151         if (entryString && [entryString length]) {
1152             
1153             NSString        *labelString = [tooltipEntry labelForObject:object];
1154             if (labelString && [labelString length]) {
1155                 
1156                 [entryArray addObject:entryString];
1157                 [labelArray addObject:labelString];
1158                 
1159                 NSAttributedString * labelAttribString = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@:",labelString] 
1160                                                                                                                                                                                  attributes:labelDict];
1161                 
1162                 //The largest size should be the label's size plus the distance to the next tab at least a space past its end
1163                 labelWidth = [labelAttribString size].width;
1164                 [labelAttribString release];
1165                 
1166                 if (labelWidth > maxLabelWidth)
1167                     maxLabelWidth = labelWidth;
1168             }
1169         }
1170         [entryString release];
1171     }
1172     
1173     //Add labels plus entires to the toolTip
1174     enumerator = [entryArray objectEnumerator];
1175     labelEnumerator = [labelArray objectEnumerator];
1176     
1177     while ((entryString = [enumerator nextObject])) {        
1178         NSAttributedString * labelAttribString = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"\t%@:\t",[labelEnumerator nextObject]]
1179                                                                                                                                                                  attributes:labelDict];
1180         
1181         //Add a carriage return
1182         [titleString appendString:@"\n" withAttributes:labelEndLineDict];
1183         
1184         if (isFirst) {
1185             //skip a line
1186             [titleString appendString:@"\n" withAttributes:labelEndLineDict];
1187             isFirst = NO;
1188         }
1189         
1190         //Add the label (with its spacing)
1191         [titleString appendAttributedString:labelAttribString];
1192                 [labelAttribString release];
1194                 [entryString addAttributes:entryDict range:NSMakeRange(0,[entryString length])];
1195         [titleString appendAttributedString:entryString];
1196     }
1198     return [titleString autorelease];
1201 - (NSAttributedString *)_tooltipBodyForObject:(AIListObject *)object
1203     NSMutableAttributedString       *tipString = [[NSMutableAttributedString alloc] init];
1204     
1205     //Configure fonts and attributes
1206     NSFontManager                   *fontManager = [NSFontManager sharedFontManager];
1207     NSFont                          *toolTipsFont = [NSFont toolTipsFontOfSize:10];
1208     NSMutableDictionary             *labelDict = [NSMutableDictionary dictionaryWithObjectsAndKeys:
1209         [fontManager convertFont:[NSFont toolTipsFontOfSize:9] toHaveTrait:NSBoldFontMask], NSFontAttributeName, nil];
1210     NSMutableDictionary             *labelEndLineDict = [NSMutableDictionary dictionaryWithObjectsAndKeys:[NSFont toolTipsFontOfSize:1], NSFontAttributeName, nil];
1211     NSMutableDictionary             *entryDict =[NSMutableDictionary dictionaryWithObjectsAndKeys:
1212         toolTipsFont, NSFontAttributeName, nil];
1213     
1214     //Entries from plugins
1215     id <AIContactListTooltipEntry>  tooltipEntry;
1216     NSEnumerator                    *enumerator;
1217     NSEnumerator                    *labelEnumerator; 
1218     NSMutableArray                  *labelArray = [NSMutableArray array];
1219     NSMutableArray                  *entryArray = [NSMutableArray array];    
1220     NSMutableAttributedString       *entryString;
1221     float                           labelWidth;
1222     BOOL                            firstEntry = YES;
1223     
1224     //Calculate the widest label while loading the arrays
1225         enumerator = [contactListTooltipSecondaryEntryArray objectEnumerator];
1226         
1227         while ((tooltipEntry = [enumerator nextObject])) {
1228                 
1229                 entryString = [[tooltipEntry entryForObject:object] mutableCopy];
1230                 if (entryString && [entryString length]) {
1231                         
1232                         NSString        *labelString = [tooltipEntry labelForObject:object];
1233                         if (labelString && [labelString length]) {
1234                                 
1235                                 [entryArray addObject:entryString];
1236                                 [labelArray addObject:labelString];
1237                                 
1238                                 NSAttributedString * labelAttribString = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@:",labelString] 
1239                                                                                                                                                                                  attributes:labelDict];
1240                                 
1241                                 //The largest size should be the label's size plus the distance to the next tab at least a space past its end
1242                                 labelWidth = [labelAttribString size].width;
1243                                 [labelAttribString release];
1244                                 
1245                                 if (labelWidth > maxLabelWidth)
1246                                         maxLabelWidth = labelWidth;
1247                         }
1248                 }
1249                 [entryString release];
1250         }
1251                 
1252     //Add labels plus entires to the toolTip
1253     enumerator = [entryArray objectEnumerator];
1254     labelEnumerator = [labelArray objectEnumerator];
1255     while ((entryString = [enumerator nextObject])) {
1256         NSMutableAttributedString *labelString = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"\t%@:\t",[labelEnumerator nextObject]]
1257                                                                                                                                                                                 attributes:labelDict];
1258         
1259         if (firstEntry) {
1260             firstEntry = NO;
1261         } else {
1262             //Add a carriage return and skip a line
1263             [tipString appendString:@"\n\n" withAttributes:labelEndLineDict];
1264         }
1265         
1266         //Add the label (with its spacing)
1267         [tipString appendAttributedString:labelString];
1268         [labelString release];
1270         NSRange fullLength = NSMakeRange(0, [entryString length]);
1271         
1272         //remove any background coloration
1273         [entryString removeAttribute:NSBackgroundColorAttributeName range:fullLength];
1274         
1275         //adjust foreground colors for the tooltip background
1276         [entryString adjustColorsToShowOnBackground:[NSColor colorWithCalibratedRed:1.000 green:1.000 blue:0.800 alpha:1.0]];
1278         //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
1279                 if ([entryString replaceOccurrencesOfString:@"\r" withString:@"\r\t\t" options:NSLiteralSearch range:fullLength])
1280             fullLength = NSMakeRange(0, [entryString length]);
1281         if ([entryString replaceOccurrencesOfString:@"\n" withString:@"\n\t\t" options:NSLiteralSearch range:fullLength])
1282             fullLength = NSMakeRange(0, [entryString length]);
1283                 
1284         //Run the entry through the filters and add it to tipString
1285                 entryString = [[[adium contentController] filterAttributedString:entryString
1286                                                                                                                  usingFilterType:AIFilterTooltips
1287                                                                                                                            direction:AIFilterIncoming
1288                                                                                                                                  context:object] mutableCopy];
1289                 
1290                 [entryString addAttributes:entryDict range:NSMakeRange(0,[entryString length])];
1291         [tipString appendAttributedString:entryString];
1292                 [entryString release];
1293     }
1295     return [tipString autorelease];
1298 //Custom pasting ----------------------------------------------------------------------------------------------------
1299 #pragma mark Custom Pasting
1300 //Paste, stripping formatting
1301 - (IBAction)paste:(id)sender
1303         [self _pasteWithPreferredSelector:@selector(pasteAsPlainTextWithTraits:) sender:sender];
1306 //Paste with formatting
1307 - (IBAction)pasteAndMatchStyle:(id)sender
1309         [self _pasteWithPreferredSelector:@selector(pasteAsPlainText:) sender:sender];
1312 - (IBAction)pasteWithImagesAndColors:(id)sender
1314         [self _pasteWithPreferredSelector:@selector(pasteAsRichText:) sender:sender];   
1318  * @brief Send a paste message, using preferredSelector if possible and paste: if not
1320  * Walks the responder chain looking for a responder which can handle pasting, skipping instances of
1321  * WebHTMLView.  These are skipped because we can control what paste does to WebView (by using a custom subclass) but
1322  * have no control over what the WebHTMLView would do.
1324  * If no responder is found, repeats the process looking for the simpler paste: selector.
1325  */
1326 - (void)_pasteWithPreferredSelector:(SEL)selector sender:(id)sender
1328         NSWindow        *keyWindow = [[NSApplication sharedApplication] keyWindow];
1329         NSResponder     *responder;
1331         //First, look for a responder which can handle the preferred selector
1332         if (!(responder = [keyWindow earliestResponderWhichRespondsToSelector:selector
1333                                                                                                                   andIsNotOfClass:NSClassFromString(@"WebHTMLView")])) {                
1334                 //No responder found.  Try again, looking for one which will respond to paste:
1335                 selector = @selector(paste:);
1336                 responder = [keyWindow earliestResponderWhichRespondsToSelector:selector
1337                                                                                                                 andIsNotOfClass:NSClassFromString(@"WebHTMLView")];
1338         }
1340         //Sending pasteAsRichText: to a non rich text NSTextView won't do anything; change it to a generic paste:
1341         if ([responder isKindOfClass:[NSTextView class]] && ![(NSTextView *)responder isRichText]) {
1342                 selector = @selector(paste:);
1343         }
1345         if (selector) {
1346                 [keyWindow makeFirstResponder:responder];
1347                 [responder performSelector:selector
1348                                                 withObject:sender];
1349         }
1352 //Custom Printing ------------------------------------------------------------------------------------------------------
1353 #pragma mark Custom Printing
1354 - (IBAction)adiumPrint:(id)sender
1356         //Pass the print command to the window, which is responsible for routing it to the correct place or
1357         //creating a view and printing.  Adium will not print from a window that does not respond to adiumPrint:
1358         NSWindow        *keyWindowController = [[[NSApplication sharedApplication] keyWindow] windowController];
1359         if ([keyWindowController respondsToSelector:@selector(adiumPrint:)]) {
1360                 [keyWindowController performSelector:@selector(adiumPrint:)
1361                                                                   withObject:sender];
1362         }
1365 #pragma mark Preferences Display
1366 - (IBAction)showPreferenceWindow:(id)sender
1368         [[adium preferenceController] showPreferenceWindow:sender];
1371 #pragma mark Font Panel
1372 - (IBAction)toggleFontPanel:(id)sender
1374         if ([NSFontPanel sharedFontPanelExists] &&
1375                 [[NSFontPanel sharedFontPanel] isVisible]) {
1376                 [[NSFontPanel sharedFontPanel] close];
1378         } else {
1379                 NSFontPanel     *fontPanel = [NSFontPanel sharedFontPanel];
1380                 
1381                 if (!fontPanelAccessoryView) {
1382                         [NSBundle loadNibNamed:@"FontPanelAccessoryView" owner:self];
1383                         [fontPanel setAccessoryView:fontPanelAccessoryView];
1384                 }
1385                 
1386                 [fontPanel orderFront:self]; 
1387         }
1390 - (IBAction)setFontPanelSettingsAsDefaultFont:(id)sender
1392         NSFont  *selectedFont = [[NSFontManager sharedFontManager] selectedFont];
1394         [[adium preferenceController] setPreference:[selectedFont stringRepresentation]
1395                                                                                  forKey:KEY_FORMATTING_FONT
1396                                                                                   group:PREF_GROUP_FORMATTING];
1397         
1398         //We can't get foreground/background color from the font panel so far as I can tell... so we do the best we can.
1399         NSWindow        *keyWindow = [[NSApplication sharedApplication] keyWindow];
1400         NSResponder *responder = [keyWindow firstResponder]; 
1401         if ([responder isKindOfClass:[NSTextView class]]) {
1402                 NSDictionary    *typingAttributes = [(NSTextView *)responder typingAttributes];
1403                 NSColor                 *foregroundColor, *backgroundColor;
1405                 if ((foregroundColor = [typingAttributes objectForKey:NSForegroundColorAttributeName])) {
1406                         [[adium preferenceController] setPreference:[foregroundColor stringRepresentation]
1407                                                                                                  forKey:KEY_FORMATTING_TEXT_COLOR
1408                                                                                                   group:PREF_GROUP_FORMATTING];
1409                 }
1411                 if ((backgroundColor = [typingAttributes objectForKey:AIBodyColorAttributeName])) {
1412                         [[adium preferenceController] setPreference:[backgroundColor stringRepresentation]
1413                                                                                                  forKey:KEY_FORMATTING_BACKGROUND_COLOR
1414                                                                                                   group:PREF_GROUP_FORMATTING];
1415                 }
1416         }
1419 //Custom Dimming menu items --------------------------------------------------------------------------------------------
1420 #pragma mark Custom Dimming menu items
1421 //The standard ones do not dim correctly when unavailable
1422 - (IBAction)toggleFontTrait:(id)sender
1424     NSFontManager       *fontManager = [NSFontManager sharedFontManager];
1425     
1426     if ([fontManager traitsOfFont:[fontManager selectedFont]] & [sender tag]) {
1427         [fontManager removeFontTrait:sender];
1428     } else {
1429         [fontManager addFontTrait:sender];
1430     }
1433 - (void)toggleToolbarShown:(id)sender
1435         NSWindow        *window = [[NSApplication sharedApplication] keyWindow];        
1436         [window toggleToolbarShown:sender];
1439 - (void)runToolbarCustomizationPalette:(id)sender
1441         NSWindow        *window = [[NSApplication sharedApplication] keyWindow];        
1442         [window runToolbarCustomizationPalette:sender];
1445 //Menu item validation
1446 - (BOOL)validateMenuItem:(id <NSMenuItem>)menuItem
1448         NSWindow        *keyWindow = [[NSApplication sharedApplication] keyWindow];
1449         NSResponder *responder = [keyWindow firstResponder]; 
1451     if (menuItem == menuItem_bold || menuItem == menuItem_italic) {
1452                 NSFont                  *selectedFont = [[NSFontManager sharedFontManager] selectedFont];
1453                 
1454                 //We must be in a text view, have text on the pasteboard, and have a font that supports bold or italic
1455                 if ([responder isKindOfClass:[NSTextView class]]) {
1456                         return (menuItem == menuItem_bold ? [selectedFont supportsBold] : [selectedFont supportsItalics]);
1457                 }
1458                 return NO;
1459                 
1460         } else if (menuItem == menuItem_paste || menuItem == menuItem_pasteAndMatchStyle) {
1461                 return [[NSPasteboard generalPasteboard] availableTypeFromArray:[NSArray arrayWithObjects:NSStringPboardType, NSRTFPboardType, NSTIFFPboardType, NSPICTPboardType, NSPDFPboardType, NSURLPboardType, NSFilenamesPboardType, NSFilesPromisePboardType, NSRTFDPboardType, nil]] != nil;
1462         
1463         } else if (menuItem == menuItem_showToolbar) {
1464                 [menuItem_showToolbar setTitle:([[keyWindow toolbar] isVisible] ? 
1465                                                                                 AILocalizedString(@"Hide Toolbar",nil) : 
1466                                                                                 AILocalizedString(@"Show Toolbar",nil))];
1467                 return [keyWindow toolbar] != nil;
1468         
1469         } else if (menuItem == menuItem_customizeToolbar) {
1470                 return ([keyWindow toolbar] != nil && [[keyWindow toolbar] isVisible] && [[keyWindow windowController] canCustomizeToolbar]);
1472         } else if (menuItem == menuItem_close) {
1473                 return (keyWindow && ([[keyWindow standardWindowButton:NSWindowCloseButton] isEnabled] ||
1474                                                           ([[keyWindow windowController] respondsToSelector:@selector(windowPermitsClose)] &&
1475                                                            [[keyWindow windowController] windowPermitsClose])));
1476                 
1477         } else if (menuItem == menuItem_closeChat) {
1478                 return activeChat != nil;
1479                 
1480         } else if( menuItem == menuItem_closeAllChats) {
1481                 return [[self openChats] count] > 0;
1483         } else if (menuItem == menuItem_print) {
1484                 NSWindowController *windowController = [keyWindow windowController];
1486                 return ([windowController respondsToSelector:@selector(adiumPrint:)] &&
1487                                 (![windowController respondsToSelector:@selector(validatePrintMenuItem:)] ||
1488                                  [windowController validatePrintMenuItem:menuItem]));
1489                 
1490         } else if (menuItem == menuItem_showFonts) {
1491                 [menuItem_showFonts setTitle:(([NSFontPanel sharedFontPanelExists] && [[NSFontPanel sharedFontPanel] isVisible]) ?
1492                                                                           AILocalizedString(@"Hide Fonts",nil) :
1493                                                                           AILocalizedString(@"Show Fonts",nil))];
1494                 return YES;
1495         } else {
1496                 return YES;
1497         }
1500 #pragma mark Window levels
1501 - (NSMenu *)menuForWindowLevelsNotifyingTarget:(id)target
1503         NSMenu          *windowPositionMenu = [[NSMenu allocWithZone:[NSMenu zone]] init];
1504         NSMenuItem      *menuItem;
1505         
1506         menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Above other windows",nil)
1507                                                                                                                                         target:target
1508                                                                                                                                         action:@selector(selectedWindowLevel:)
1509                                                                                                                          keyEquivalent:@""];
1510         [menuItem setEnabled:YES];
1511         [menuItem setTag:AIFloatingWindowLevel];
1512         [windowPositionMenu addItem:menuItem];
1513         [menuItem release];
1514         
1515         menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Normally",nil)
1516                                                                                                                                         target:target
1517                                                                                                                                         action:@selector(selectedWindowLevel:)
1518                                                                                                                          keyEquivalent:@""];
1519         [menuItem setEnabled:YES];
1520         [menuItem setTag:AINormalWindowLevel];
1521         [windowPositionMenu addItem:menuItem];
1522         [menuItem release];
1523         
1524         menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Below other windows",nil)
1525                                                                                                                                         target:target
1526                                                                                                                                         action:@selector(selectedWindowLevel:)
1527                                                                                                                          keyEquivalent:@""];
1528         [menuItem setEnabled:YES];
1529         [menuItem setTag:AIDesktopWindowLevel];
1530         [windowPositionMenu addItem:menuItem];
1531         [menuItem release];
1532         
1533         [windowPositionMenu setAutoenablesItems:NO];
1535         return [windowPositionMenu autorelease];
1538 @end