merged [21403]: Fixed an exception thrown when clearing all complete file transfers...
[adiumx.git] / Source / AIInterfaceController.m
blob2cd2052a73c4ded1a24a11a55ea33ba178beb946
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.
234          */
236         //If no windows are visible, show the contact list
237         if (![self contactListIsVisibleAndMain] && [[interfacePlugin openContainers] count] == 0) {
238                 [self showContactList:nil];
239         } else {
240                 AIChat  *mostRecentUnviewedChat;
242                 //If windows are open, try switching to a chat with unviewed content
243                 if ((mostRecentUnviewedChat = [[adium chatController] mostRecentUnviewedChat])) {
244                         if ([mostRecentActiveChat unviewedContentCount]) {
245                                 //If the most recently active chat has unviewed content, ensure it is in the front
246                                 [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         [NSApp unhide:nil];
276         //We handled the reopen; return NO so NSApp does nothing.
277     return NO; 
280 //Contact List ---------------------------------------------------------------------------------------------------------
281 #pragma mark Contact list
282 //Toggle the contact list
283 - (IBAction)toggleContactList:(id)sender
285     if ([self contactListIsVisibleAndMain]) {
286                 [self closeContactList:nil];
287     } else {
288                 [self showContactList:nil];
289     } 
292 //Show the contact list window
293 - (IBAction)showContactList:(id)sender
295         [contactListPlugin showContactListAndBringToFront:YES];
298 //Show the contact list window and bring Adium to the front
299 - (IBAction)showContactListAndBringToFront:(id)sender
301         [contactListPlugin showContactListAndBringToFront:YES];
302         [[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
305 //Close the contact list window
306 - (IBAction)closeContactList:(id)sender
308         [contactListPlugin closeContactList];
311 //Return if the contact list is open or not.
312 - (BOOL)contactListIsVisibleAndMain
314         return [contactListPlugin contactListIsVisibleAndMain];
317 - (BOOL)contactListIsVisible
319         return [contactListPlugin contactListIsVisible];
323 //Messaging ------------------------------------------------------------------------------------------------------------
324 //Methods for instructing the interface to provide a representation of chats, and to determine which chat has user focus
325 #pragma mark Messaging
326 //Open a window for the chat
327 - (void)openChat:(AIChat *)inChat
329         NSArray         *containers = [interfacePlugin openContainersAndChats];
330         NSString        *containerID = nil;
331         
332         //Determine the correct container for this chat
333         
334         if (!tabbedChatting) {
335                 //We're not using tabs; each chat starts in its own container, based on the destination object or the chat name
336                 if ([inChat listObject]) {
337                         containerID = [[inChat listObject] internalObjectID];
338                 } else {
339                         containerID = [inChat name];
340                 }
341                 
342         } else if (groupChatsByContactGroup) {
343                 if ([inChat isGroupChat]) {
344                         containerID = AILocalizedString(@"Group Chat",nil);
345                         
346                 } else {
347                         AIListObject    *group = [[[inChat listObject] parentContact] containingObject];
348                         
349                         //If the contact is in the contact list root, we don't have a group
350                         if (group && (group != [[adium contactController] contactList])) {
351                                 containerID = [group displayName];
352                         }
353                 }
354         }
355         
356         if (!containerID) {
357                 //Open new chats into the first container (if not available, create a new one)
358                 if ([containers count] > 0) {
359                         containerID = [[containers objectAtIndex:0] objectForKey:@"ID"];
360                 } else {
361                         containerID = AILocalizedString(@"Chat",nil);
362                 }
363         }
365         //Determine the correct placement for this chat within the container
366         [interfacePlugin openChat:inChat inContainerWithID:containerID atIndex:-1];
367         if (![inChat isOpen]) {
368                 [inChat setIsOpen:YES];
369                 
370                 //Post the notification last, so observers receive a chat whose isOpen flag is yes.
371                 [[adium notificationCenter] postNotificationName:Chat_DidOpen object:inChat userInfo:nil];
372         }
376  * @brief Close the interface for a chat
378  * Tell the interface plugin to close the chat.
379  */
380 - (void)closeChat:(AIChat *)inChat
382         if (inChat) {
383                 if ([[adium chatController] closeChat:inChat]) {
384                         [interfacePlugin closeChat:inChat];
385                 }
386         }
389 //Consolidate chats into a single container
390 - (void)consolidateChats
392         //We work with copies of these arrays, since moving chats may change their contents
393         NSArray                 *openContainers = [[interfacePlugin openContainers] copy];
394         NSEnumerator    *containerEnumerator = [openContainers objectEnumerator];
395         NSString                *firstContainerID = [containerEnumerator nextObject];
396         NSString                *containerID;
397         
398         //For all containers but the first, move the chats they contain to the first container
399         while ((containerID = [containerEnumerator nextObject])) {
400                 NSArray                 *openChats = [[interfacePlugin openChatsInContainerWithID:containerID] copy];
401                 NSEnumerator    *chatEnumerator = [openChats objectEnumerator];
402                 AIChat                  *chat;
404                 //Move all the chats, providing a target index if chat sorting is enabled
405                 while ((chat = [chatEnumerator nextObject])) {
406                         [interfacePlugin moveChat:chat
407                                         toContainerWithID:firstContainerID
408                                                                 index:-1];
409                 }
410                 
411                 [openChats release];
412         }
413         
414         [self chatOrderDidChange];
415         
416         [openContainers release];
419 //Active chat
420 - (AIChat *)activeChat
422         return activeChat;
424 //Set the active chat window
425 - (void)setActiveChat:(AIChat *)inChat
427         [interfacePlugin setActiveChat:inChat];
429 //Last chat to be active (should only be nil if no chats are open)
430 - (AIChat *)mostRecentActiveChat
432         return mostRecentActiveChat;
434 //Solely for key-value pairing purposes
435 - (void)setMostRecentActiveChat:(AIChat *)inChat
437         [self setActiveChat:inChat];
440 //Returns an array of open chats (cached, so call as frequently as desired)
441 - (NSArray *)openChats
443         if (!_cachedOpenChats) {
444                 _cachedOpenChats = [[interfacePlugin openChats] retain];
445         }
446         
447         return _cachedOpenChats;
451 - (NSArray *)openChatsInContainerWithID:(NSString *)containerID
453         return [interfacePlugin openChatsInContainerWithID:containerID];
456 //Resets the cache of open chats
457 - (void)_resetOpenChatsCache
459         [_cachedOpenChats release]; _cachedOpenChats = nil;
464 //Interface plugin callbacks -------------------------------------------------------------------------------------------
465 //These methods are called by the interface to let us know what's going on.  We're informed of chats opening, closing,
466 //changing order, etc.
467 #pragma mark Interface plugin callbacks
468 //A chat window did open: rebuild our window menu to show the new chat
469 - (void)chatDidOpen:(AIChat *)inChat
471         [self _resetOpenChatsCache];
472         [self buildWindowMenu];
475 //A chat has become active: update our chat closing keys and flag this chat as selected in the window menu
476 - (void)chatDidBecomeActive:(AIChat *)inChat
478         AIChat  *previouslyActiveChat = activeChat;
479         
480         activeChat = [inChat retain];
481         
482         [self updateCloseMenuKeys];
483         [self updateActiveWindowMenuItem];
484         
485         if (inChat && (inChat != mostRecentActiveChat)) {
486                 [mostRecentActiveChat release]; mostRecentActiveChat = nil;
487                 mostRecentActiveChat = [inChat retain];
488         }
489         
490         [[adium notificationCenter] postNotificationName:Chat_BecameActive
491                                                                                           object:inChat 
492                                                                                         userInfo:(previouslyActiveChat ?
493                                                                                                           [NSDictionary dictionaryWithObject:previouslyActiveChat
494                                                                                                                                                                   forKey:@"PreviouslyActiveChat"] :
495                                                                                                           nil)];
496         
497         if (inChat) {
498                 /* Clear the unviewed content on the next event loop so other methods have a chance to react to the chat becoming
499                 * active. Specifically, this lets the handleReopenWithVisibleWindows: method have a chance to know that this chat
500                 * had unviewed content.
501                 */
502                 [inChat performSelector:@selector(clearUnviewedContentCount)
503                                          withObject:nil
504                                          afterDelay:0];
505         }
506         
507         [previouslyActiveChat release]; 
510 //A chat has become visible: send out a notification for components and plugins to take action
511 - (void)chatDidBecomeVisible:(AIChat *)inChat inWindow:(NSWindow *)inWindow
513         [[adium notificationCenter] postNotificationName:@"AIChatDidBecomeVisible"
514                                                                                           object:inChat
515                                                                                         userInfo:[NSDictionary dictionaryWithObject:inWindow
516                                                                                                                                                                  forKey:@"NSWindow"]];
520  * @brief Find the window currently displaying a chat
522  * If the chat is not in any window, or is not visible in any window, returns nil
523  */
524 - (NSWindow *)windowForChat:(AIChat *)inChat
526         return [interfacePlugin windowForChat:inChat];
530  * @brief Find the chat active in a window
532  * If the window does not have an active chat, nil is returned
533  */
534 - (AIChat *)activeChatInWindow:(NSWindow *)window
536         return [interfacePlugin activeChatInWindow:window];
539 //A chat window did close: rebuild our window menu to remove the chat
540 - (void)chatDidClose:(AIChat *)inChat
542         [self _resetOpenChatsCache];
543         [inChat clearUnviewedContentCount];
544         [self buildWindowMenu];
545         
546         if (inChat == activeChat) {
547                 [activeChat release]; activeChat = nil;
548         }
549         
550         if (inChat == mostRecentActiveChat) {
551                 [mostRecentActiveChat release]; mostRecentActiveChat = nil;
552         }
555 //The order of chats has changed: rebuild our window menu to reflect the new order
556 - (void)chatOrderDidChange
558         [self _resetOpenChatsCache];
559         [self buildWindowMenu];
562 #pragma mark Unviewed content
564 //Content was received, increase the unviewed content count of the chat (if it's not currently active)
565 - (void)didReceiveContent:(NSNotification *)notification
567         AIChat          *chat = [[notification userInfo] objectForKey:@"AIChat"];
568         
569         if (chat != activeChat) {
570                 [chat incrementUnviewedContentCount];
571         }
575 //Chat close menus -----------------------------------------------------------------------------------------------------
576 #pragma mark Chat close menus
577 //Close the active window
578 - (IBAction)closeMenu:(id)sender
580     [[[NSApplication sharedApplication] keyWindow] performClose:nil];
583 //Close the active chat
584 - (IBAction)closeChatMenu:(id)sender
586         if (activeChat) [self closeChat:activeChat];
589 - (IBAction)closeContextualChat:(id)sender
591         [self closeChat:[[adium menuController] currentContextMenuChat]];
594 //Loop through open chats and close them
595 - (IBAction)closeAllChats:(id)sender
597         NSEnumerator    *containerEnumerator = [[[[interfacePlugin openChats] copy] autorelease] objectEnumerator];
598         AIChat                  *chatToClose;
600         while ((chatToClose = [containerEnumerator nextObject])) {
601                 [self closeChat:chatToClose];
602         }
605 //Updates the key equivalents on 'close' and 'close chat' (dynamically changed to make cmd-w less destructive)
606 - (void)updateCloseMenuKeys
608         if (activeChat && !closeMenuConfiguredForChat) {
609         [menuItem_close setKeyEquivalent:@"W"];
610         [menuItem_closeChat setKeyEquivalent:@"w"];
611                 closeMenuConfiguredForChat = YES;
612         } else if (!activeChat && closeMenuConfiguredForChat) {
613         [menuItem_close setKeyEquivalent:@"w"];
614                 [menuItem_closeChat removeKeyEquivalent];               
615                 closeMenuConfiguredForChat = NO;
616         }
620 //Window Menu ----------------------------------------------------------------------------------------------------------
621 #pragma mark Window Menu
622 //Make a chat window active (Invoked by a selection in the window menu)
623 - (IBAction)showChatWindow:(id)sender
625         [self setActiveChat:[sender representedObject]];
626     [[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
629 //Updates the 'check' icon so it's next to the active window
630 - (void)updateActiveWindowMenuItem
632     NSEnumerator        *enumerator = [windowMenuArray objectEnumerator];
633     NSMenuItem          *item;
635     while ((item = [enumerator nextObject])) {
636                 if ([item representedObject]) [item setState:([item representedObject] == activeChat ? NSOnState : NSOffState)];
637     }
640 //Builds the window menu
641 //This function gets called whenever chats are opened, closed, or re-ordered - so improvements and optimizations here
642 //would probably be helpful
643 - (void)buildWindowMenu
644 {       
645     NSMenuItem                          *item;
646     NSEnumerator                        *enumerator;
647     int                                         windowKey = 1;
648         
649     //Remove any existing menus
650     enumerator = [windowMenuArray objectEnumerator];
651     while ((item = [enumerator nextObject])) {
652         [[adium menuController] removeMenuItem:item];
653     }
654     [windowMenuArray release]; windowMenuArray = [[NSMutableArray alloc] init];
655         
656     //Messages window and any open messasges
657         NSEnumerator    *containerEnumerator = [[interfacePlugin openContainersAndChats] objectEnumerator];
658         NSDictionary    *containerDict;
659         
660         while ((containerDict = [containerEnumerator nextObject])) {
661                 NSString                *containerName = [containerDict objectForKey:@"Name"];
662                 NSArray                 *contentArray = [containerDict objectForKey:@"Content"];
663                 NSEnumerator    *contentEnumerator = [contentArray objectEnumerator];
664                 AIChat                  *chat;
665                 
666                 //Add a menu item for the container
667                 if ([contentArray count] > 1) {
668                         item = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:containerName
669                                                                                                                                                 target:nil
670                                                                                                                                                 action:nil
671                                                                                                                                  keyEquivalent:@""];
672                         [self _addItemToMainMenuAndDock:item];
673                         [item release];
674                 }
675                 
676                 //Add items for the chats it contains
677                 while ((chat = [contentEnumerator nextObject])) {
678                         NSString                *windowKeyString;
679                         
680                         //Prepare a key equivalent for the controller
681                         if (windowKey < 10) {
682                                 windowKeyString = [NSString stringWithFormat:@"%i",(windowKey)];
683                         } else if (windowKey == 10) {
684                                 windowKeyString = [NSString stringWithString:@"0"];
685                         } else {
686                                 windowKeyString = [NSString stringWithString:@""];
687                         }
688                         
689                         item = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:[chat displayName]
690                                                                                                                                                 target:self
691                                                                                                                                                 action:@selector(showChatWindow:)
692                                                                                                                                  keyEquivalent:windowKeyString];
693                         if ([contentArray count] > 1) [item setIndentationLevel:1];
694                         [item setRepresentedObject:chat];
695                         [item setImage:[chat chatMenuImage]];
696                         [self _addItemToMainMenuAndDock:item];
697                         [item release];
699                         windowKey++;
700                 }
701         }
703         [self updateActiveWindowMenuItem];
706 //Adds a menu item to the internal array, dock menu, and main menu
707 - (void)_addItemToMainMenuAndDock:(NSMenuItem *)item
709         //Add to main menu first
710         [[adium menuController] addMenuItem:item toLocation:LOC_Window_Fixed];
711         [windowMenuArray addObject:item];
712         
713         //Make a copy, and add to the dock
714         item = [item copy];
715         [item setKeyEquivalent:@""];
716         [[adium menuController] addMenuItem:item toLocation:LOC_Dock_Status];
717         [windowMenuArray addObject:item];
718         [item release];
722 //Chat Cycling ---------------------------------------------------------------------------------------------------------
723 #pragma mark Chat Cycling
724 //Select the next message
725 - (void)nextChat:(id)sender
727         NSArray *openChats = [self openChats];
729         if ([openChats count]) {
730                 if (activeChat) {
731                         int chatIndex = [openChats indexOfObject:activeChat]+1;
732                         [self setActiveChat:[openChats objectAtIndex:(chatIndex < [openChats count] ? chatIndex : 0)]];
733                 } else {
734                         [self setActiveChat:[openChats objectAtIndex:0]];
735                 }
736         }
739 //Select the previous message
740 - (void)previousChat:(id)sender
742         NSArray *openChats = [self openChats];
743         
744         if ([openChats count]) {
745                 if (activeChat) {
746                         int chatIndex = [openChats indexOfObject:activeChat]-1;
747                         [self setActiveChat:[openChats objectAtIndex:(chatIndex >= 0 ? chatIndex : [openChats count]-1)]];
748                 } else {
749                         [self setActiveChat:[openChats lastObject]];
750                 }
751         }
754 //Selected contact ------------------------------------------------
755 #pragma mark Selected contact
756 - (id)_performSelectorOnFirstAvailableResponder:(SEL)selector
758     NSResponder *responder = [[[NSApplication sharedApplication] mainWindow] firstResponder];
759     //Check the first responder
760     if ([responder respondsToSelector:selector]) {
761         return [responder performSelector:selector];
762     }
763         
764     //Search the responder chain
765     do{
766         responder = [responder nextResponder];
767         if ([responder respondsToSelector:selector]) {
768             return [responder performSelector:selector];
769         }
770                 
771     } while (responder != nil);
772         
773     //None found, return nil
774     return nil;
776 - (id)_performSelectorOnFirstAvailableResponder:(SEL)selector conformingToProtocol:(Protocol *)protocol
778         NSResponder *responder = [[[NSApplication sharedApplication] mainWindow] firstResponder];
779         //Check the first responder
780         if ([responder conformsToProtocol:protocol] && [responder respondsToSelector:selector]) {
781                 return [responder performSelector:selector];
782         }
783         
784     //Search the responder chain
785     do{
786         responder = [responder nextResponder];
787         if ([responder conformsToProtocol:protocol] && [responder respondsToSelector:selector]) {
788             return [responder performSelector:selector];
789         }
790                 
791     } while (responder != nil);
792         
793     //None found, return nil
794     return nil;
797 //Returns the "selected"(represented) contact (By finding the first responder that returns a contact)
798 //If no listObject is found, try to find a list object selected in a group chat
799 - (AIListObject *)selectedListObject
801         AIListObject *listObject = [self _performSelectorOnFirstAvailableResponder:@selector(listObject)];
802         if ( !listObject) {
803                 listObject = [self _performSelectorOnFirstAvailableResponder:@selector(preferredListObject)];
804         }
805         return listObject;
807 - (AIListObject *)selectedListObjectInContactList
809         return [self _performSelectorOnFirstAvailableResponder:@selector(listObject) conformingToProtocol:@protocol(ContactListOutlineView)];
811 - (NSArray *)arrayOfSelectedListObjectsInContactList
813         return [self _performSelectorOnFirstAvailableResponder:@selector(arrayOfListObjects) conformingToProtocol:@protocol(ContactListOutlineView)];
816 //Message View ---------------------------------------------------------------------------------------------------------
817 //Message view is abstracted from the containing interface, since they're not directly related to eachother
818 #pragma mark Message View
819 //Registers a view to handle the contact list
820 - (void)registerMessageDisplayPlugin:(id <AIMessageDisplayPlugin>)inPlugin
822     [messageViewArray addObject:inPlugin];
824 - (id <AIMessageDisplayController>)messageDisplayControllerForChat:(AIChat *)inChat
826         //Sometimes our users find it amusing to disable plugins that are located within the Adium bundle.  This error
827         //trap prevents us from crashing if they happen to disable all the available message view plugins.
828         //PUT THAT PLUGIN BACK IT WAS IMPORTANT!
829         if ([messageViewArray count] == 0) {
830                 NSRunCriticalAlertPanel(@"No Message View Plugin Installed",
831                                                                 @"Adium cannot find its message view plugin. Please re-install.  If you've manually disabled Adium's message view plugin, please re-enable it.",
832                                                                 @"Quit",
833                                                                 nil,
834                                                                 nil);
835                 [NSApp terminate:nil];
836         }
837         
838         return [[messageViewArray objectAtIndex:0] messageDisplayControllerForChat:inChat];
842 //Error Display --------------------------------------------------------------------------------------------------------
843 #pragma mark Error Display
844 - (void)handleErrorMessage:(NSString *)inTitle withDescription:(NSString *)inDesc
846     [self handleMessage:inTitle withDescription:inDesc withWindowTitle:ERROR_MESSAGE_WINDOW_TITLE];
849 - (void)handleMessage:(NSString *)inTitle withDescription:(NSString *)inDesc withWindowTitle:(NSString *)inWindowTitle;
851     NSDictionary        *errorDict;
852     
853     //Post a notification that an error was recieved
854     errorDict = [NSDictionary dictionaryWithObjectsAndKeys:inTitle,@"Title",inDesc,@"Description",inWindowTitle,@"Window Title",nil];
855     [[adium notificationCenter] postNotificationName:Interface_ShouldDisplayErrorMessage object:nil userInfo:errorDict];
858 //Display then clear the last disconnection error
859 - (void)account:(AIAccount *)inAccount disconnectedWithError:(NSString *)disconnectionError
861 //      [AdiumDisconnectionErrorController account:inAccount disconnectedWithError:disconnectionError];
864 //Question Display -----------------------------------------------------------------------------------------------------
865 #pragma mark Question Display
866 - (void)displayQuestion:(NSString *)inTitle withAttributedDescription:(NSAttributedString *)inDesc withWindowTitle:(NSString *)inWindowTitle
867                   defaultButton:(NSString *)inDefaultButton alternateButton:(NSString *)inAlternateButton otherButton:(NSString *)inOtherButton
868                                  target:(id)inTarget selector:(SEL)inSelector userInfo:(id)inUserInfo
870         NSMutableDictionary *questionDict = [NSMutableDictionary dictionary];
871         
872         if(inTitle != nil)
873                 [questionDict setObject:inTitle forKey:@"Title"];
874         if(inDesc != nil)
875                 [questionDict setObject:inDesc forKey:@"Description"];
876         if(inWindowTitle != nil)
877                 [questionDict setObject:inWindowTitle forKey:@"Window Title"];
878         if(inDefaultButton != nil)
879                 [questionDict setObject:inDefaultButton forKey:@"Default Button"];
880         if(inAlternateButton != nil)
881                 [questionDict setObject:inAlternateButton forKey:@"Alternate Button"];
882         if(inOtherButton != nil)
883                 [questionDict setObject:inOtherButton forKey:@"Other Button"];
884         if(inTarget != nil)
885                 [questionDict setObject:inTarget forKey:@"Target"];
886         if(inSelector != NULL)
887                 [questionDict setObject:NSStringFromSelector(inSelector) forKey:@"Selector"];
888         if(inUserInfo != nil)
889                 [questionDict setObject:inUserInfo forKey:@"Userinfo"];
890         
891         [[adium notificationCenter] postNotificationName:Interface_ShouldDisplayQuestion object:nil userInfo:questionDict];
894 - (void)displayQuestion:(NSString *)inTitle withDescription:(NSString *)inDesc withWindowTitle:(NSString *)inWindowTitle
895                   defaultButton:(NSString *)inDefaultButton alternateButton:(NSString *)inAlternateButton otherButton:(NSString *)inOtherButton
896                                  target:(id)inTarget selector:(SEL)inSelector userInfo:(id)inUserInfo
898         [self displayQuestion:inTitle
899 withAttributedDescription:[[[NSAttributedString alloc] initWithString:inDesc
900                                                                                                                    attributes:[NSDictionary dictionaryWithObject:[NSFont systemFontOfSize:0]
901                                                                                                                                                                                                   forKey:NSFontAttributeName]] autorelease]
902                   withWindowTitle:inWindowTitle
903                         defaultButton:inDefaultButton
904                   alternateButton:inAlternateButton
905                           otherButton:inOtherButton
906                                    target:inTarget
907                                  selector:inSelector
908                                  userInfo:inUserInfo];
910 //Synchronized Flashing ------------------------------------------------------------------------------------------------
911 #pragma mark Synchronized Flashing
912 //Register to observe the synchronized flashing
913 - (void)registerFlashObserver:(id <AIFlashObserver>)inObserver
915     //Setup the timer if we don't have one yet
916     if (!flashObserverArray) {
917         flashObserverArray = [[NSMutableArray alloc] init];
918         flashTimer = [[NSTimer scheduledTimerWithTimeInterval:(1.0/2.0) 
919                                                        target:self 
920                                                      selector:@selector(flashTimer:) 
921                                                      userInfo:nil
922                                                       repeats:YES] retain];
923     }
924     
925     //Add the new observer to the array
926     [flashObserverArray addObject:inObserver];
929 //Unregister from observing flashing
930 - (void)unregisterFlashObserver:(id <AIFlashObserver>)inObserver
932     //Remove the observer from our array
933     [flashObserverArray removeObject:inObserver];
934     
935     //Release the observer array and uninstall the timer
936     if ([flashObserverArray count] == 0) {
937         [flashObserverArray release]; flashObserverArray = nil;
938         [flashTimer invalidate];
939         [flashTimer release]; flashTimer = nil;
940     }
943 //Timer, invoke a flash
944 - (void)flashTimer:(NSTimer *)inTimer
946     NSEnumerator        *enumerator;
947     id<AIFlashObserver> observer;
948     
949     flashState++;
950     
951     enumerator = [flashObserverArray objectEnumerator];
952     while ((observer = [enumerator nextObject])) {
953         [observer flash:flashState];
954     }
957 //Current state of flashing.  This is an integer the increases by 1 with every flash.  Mod to whatever range is desired
958 - (int)flashState
960     return flashState;
964 //Tooltips -------------------------------------------------------------------------------------------------------------
965 #pragma mark Tooltips
966 //Registers code to display tooltip info about a contact
967 - (void)registerContactListTooltipEntry:(id <AIContactListTooltipEntry>)inEntry secondaryEntry:(BOOL)isSecondary
969     if (isSecondary)
970         [contactListTooltipSecondaryEntryArray addObject:inEntry];
971     else
972         [contactListTooltipEntryArray addObject:inEntry];
975 //Unregisters code to display tooltip info about a contact
976 - (void)unregisterContactListTooltipEntry:(id <AIContactListTooltipEntry>)inEntry secondaryEntry:(BOOL)isSecondary
978     if (isSecondary)
979         [contactListTooltipSecondaryEntryArray removeObject:inEntry];
980     else
981         [contactListTooltipEntryArray removeObject:inEntry];
984 //list object tooltips
985 - (void)showTooltipForListObject:(AIListObject *)object atScreenPoint:(NSPoint)point onWindow:(NSWindow *)inWindow 
987     if (object) {
988         if (object == tooltipListObject) { //If we already have this tooltip open
989                                          //Move the existing tooltip
990             [AITooltipUtilities showTooltipWithTitle:tooltipTitle
991                                                                                                 body:tooltipBody
992                                                                                            image:tooltipImage 
993                                                                                 imageOnRight:DISPLAY_IMAGE_ON_RIGHT 
994                                                                                         onWindow:inWindow
995                                                                                          atPoint:point 
996                                                                                  orientation:TooltipBelow];
997             
998         } else { //This is a new tooltip
999             NSArray                     *tabArray;
1000             NSMutableParagraphStyle     *paragraphStyleTitle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
1001             NSMutableParagraphStyle     *paragraphStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
1002             
1003             //Hold onto the new object
1004             [tooltipListObject release]; tooltipListObject = [object retain];
1005             
1006             //Buddy Icon
1007             [tooltipImage release];
1008                         tooltipImage = [[tooltipListObject userIcon] retain];
1009                         if (!tooltipImage) tooltipImage = [[AIServiceIcons serviceIconForObject:tooltipListObject
1010                                                                                                                                                          type:AIServiceIconLarge
1011                                                                                                                                                 direction:AIIconNormal] retain];
1012             
1013             //Reset the maxLabelWidth for the tooltip generation
1014             maxLabelWidth = 0;
1015             
1016             //Build a tooltip string for the primary information
1017             [tooltipTitle release]; tooltipTitle = [[self _tooltipTitleForObject:object] retain];
1018             
1019             //If there is an image, set the title tab and indentation settings independently
1020             if (tooltipImage) {
1021                 //Set a right-align tab at the maximum label width and a left-align just past it
1022                 tabArray = [[NSArray alloc] initWithObjects:[[[NSTextTab alloc] initWithType:NSRightTabStopType 
1023                                                                                                                                                                         location:maxLabelWidth] autorelease]
1024                                                             ,[[[NSTextTab alloc] initWithType:NSLeftTabStopType 
1025                                                                                    location:maxLabelWidth + LABEL_ENTRY_SPACING] autorelease]
1026                                                             ,nil];
1027                 
1028                 [paragraphStyleTitle setTabStops:tabArray];
1029                 [tabArray release];
1030                 tabArray = nil;
1031                 [paragraphStyleTitle setHeadIndent:(maxLabelWidth + LABEL_ENTRY_SPACING)];
1032                 
1033                 [tooltipTitle addAttribute:NSParagraphStyleAttributeName 
1034                                      value:paragraphStyleTitle
1035                                      range:NSMakeRange(0,[tooltipTitle length])];
1036                 
1037                 //Reset the max label width since the body will be independent
1038                 maxLabelWidth = 0;
1039             }
1040             
1041             //Build a tooltip string for the secondary information
1042             [tooltipBody release]; tooltipBody = nil;
1043             tooltipBody = [[self _tooltipBodyForObject:object] retain];
1044             
1045             //Set a right-align tab at the maximum label width for the body and a left-align just past it
1046             tabArray = [[NSArray alloc] initWithObjects:[[[NSTextTab alloc] initWithType:NSRightTabStopType 
1047                                                                                  location:maxLabelWidth] autorelease]
1048                                                         ,[[[NSTextTab alloc] initWithType:NSLeftTabStopType 
1049                                                                                 location:maxLabelWidth + LABEL_ENTRY_SPACING] autorelease]
1050                                                         ,nil];
1051             [paragraphStyle setTabStops:tabArray];
1052             [tabArray release];
1053             [paragraphStyle setHeadIndent:(maxLabelWidth + LABEL_ENTRY_SPACING)];
1054             
1055             [tooltipBody addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:NSMakeRange(0,[tooltipBody length])];
1056             //If there is no image, also use these settings for the top part
1057             if (!tooltipImage) {
1058                 [tooltipTitle addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:NSMakeRange(0,[tooltipTitle length])];
1059             }
1060             
1061             //Display the new tooltip
1062             [AITooltipUtilities showTooltipWithTitle:tooltipTitle
1063                                                 body:tooltipBody 
1064                                                image:tooltipImage
1065                                         imageOnRight:DISPLAY_IMAGE_ON_RIGHT
1066                                             onWindow:inWindow
1067                                              atPoint:point 
1068                                          orientation:TooltipBelow];
1069                         
1070                         [paragraphStyleTitle release];
1071                         [paragraphStyle release];
1072         }
1073         
1074     } else {
1075         //Hide the existing tooltip
1076         if (tooltipListObject) {
1077             [AITooltipUtilities showTooltipWithTitle:nil 
1078                                                 body:nil
1079                                                image:nil 
1080                                             onWindow:nil
1081                                              atPoint:point
1082                                          orientation:TooltipBelow];
1083             [tooltipListObject release]; tooltipListObject = nil;
1084                         
1085                         [tooltipTitle release]; tooltipTitle = nil;
1086                         [tooltipBody release]; tooltipBody = nil;
1087                         [tooltipImage release]; tooltipImage = nil;
1088         }
1089     }
1092 - (NSAttributedString *)_tooltipTitleForObject:(AIListObject *)object
1094     NSMutableAttributedString           *titleString = [[NSMutableAttributedString alloc] init];
1095     
1096     id <AIContactListTooltipEntry>              tooltipEntry;
1097     NSEnumerator                                                *enumerator;
1098     NSEnumerator                        *labelEnumerator;
1099     NSMutableArray                      *labelArray = [NSMutableArray array];
1100     NSMutableArray                      *entryArray = [NSMutableArray array];
1101     NSMutableAttributedString           *entryString;
1102     float                               labelWidth;
1103     BOOL                                isFirst = YES;
1104     
1105     NSString                            *formattedUID = [object formattedUID];
1106     
1107     //Configure fonts and attributes
1108     NSFontManager                       *fontManager = [NSFontManager sharedFontManager];
1109     NSFont                              *toolTipsFont = [NSFont toolTipsFontOfSize:10];
1110     NSMutableDictionary                 *titleDict = [NSMutableDictionary dictionaryWithObjectsAndKeys:
1111         [fontManager convertFont:[NSFont toolTipsFontOfSize:12] toHaveTrait:NSBoldFontMask],NSFontAttributeName, nil];
1112     NSMutableDictionary                 *labelDict = [NSMutableDictionary dictionaryWithObjectsAndKeys:
1113         [fontManager convertFont:[NSFont toolTipsFontOfSize:9] toHaveTrait:NSBoldFontMask], NSFontAttributeName, nil];
1114     NSMutableDictionary                 *labelEndLineDict = [NSMutableDictionary dictionaryWithObjectsAndKeys:[NSFont toolTipsFontOfSize:2] , NSFontAttributeName, nil];
1115     NSMutableDictionary                 *entryDict =[NSMutableDictionary dictionaryWithObjectsAndKeys:
1116         toolTipsFont, NSFontAttributeName, nil];
1117         
1118         //Get the user's display name as an attributed string
1119     NSAttributedString                  *displayName = [[NSAttributedString alloc] initWithString:[object displayName]
1120                                                                                                                                                                            attributes:titleDict];
1121         NSAttributedString                                      *filteredDisplayName = [[adium contentController] filterAttributedString:displayName
1122                                                                                                                                                                                                  usingFilterType:AIFilterTooltips
1123                                                                                                                                                                                                            direction:AIFilterIncoming
1124                                                                                                                                                                                                                  context:nil];
1125         
1126         //Append the user's display name
1127         if (filteredDisplayName) {
1128                 [titleString appendAttributedString:filteredDisplayName];
1129         }
1130         
1131         //Append the user's formatted UID if there is one that's different to the display name
1132         if (formattedUID && (!([[[displayName string] compactedString] isEqualToString:[formattedUID compactedString]]))) {
1133                 [titleString appendString:[NSString stringWithFormat:@" (%@)", formattedUID] withAttributes:titleDict];
1134         }
1135         [displayName release];
1136     
1137     if ([object isKindOfClass:[AIListContact class]]) {
1138                 if ((![object isKindOfClass:[AIMetaContact class]] || [(AIMetaContact *)object containsOnlyOneService]) &&
1139                         [object userIcon]) {
1140                         NSImage *serviceIcon = [[AIServiceIcons serviceIconForObject:object type:AIServiceIconSmall direction:AIIconNormal]
1141                                                                         imageByScalingToSize:NSMakeSize(14,14)];
1142                         if (serviceIcon) {
1143                                 NSTextAttachment                *attachment;
1144                                 NSTextAttachmentCell    *cell;
1145                                 
1146                                 cell = [[NSTextAttachmentCell alloc] init];
1147                                 [cell setImage:serviceIcon];
1148                                 
1149                                 attachment = [[NSTextAttachment alloc] init];
1150                                 [attachment setAttachmentCell:cell];
1151                                 [cell release];
1152         
1153                                 [titleString appendString:@" " withAttributes:nil];
1154                                 [titleString appendAttributedString:[NSAttributedString attributedStringWithAttachment:attachment]];
1155                                 [attachment release];
1156                         }
1157                 }
1158         }
1159                 
1160     if ([object isKindOfClass:[AIListGroup class]]) {
1161         [titleString appendString:[NSString stringWithFormat:@" (%i/%i)",[(AIListGroup *)object visibleCount],[(AIListGroup *)object containedObjectsCount]] 
1162                    withAttributes:titleDict];
1163     }
1164     
1165     //Entries from plugins
1166     
1167     //Calculate the widest label while loading the arrays
1168     enumerator = [contactListTooltipEntryArray objectEnumerator];
1169     
1170     while ((tooltipEntry = [enumerator nextObject])) {
1171         
1172         entryString = [[tooltipEntry entryForObject:object] mutableCopy];
1173         if (entryString && [entryString length]) {
1174             
1175             NSString        *labelString = [tooltipEntry labelForObject:object];
1176             if (labelString && [labelString length]) {
1177                 
1178                 [entryArray addObject:entryString];
1179                 [labelArray addObject:labelString];
1180                 
1181                 NSAttributedString * labelAttribString = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@:",labelString] 
1182                                                                                                                                                                                  attributes:labelDict];
1183                 
1184                 //The largest size should be the label's size plus the distance to the next tab at least a space past its end
1185                 labelWidth = [labelAttribString size].width;
1186                 [labelAttribString release];
1187                 
1188                 if (labelWidth > maxLabelWidth)
1189                     maxLabelWidth = labelWidth;
1190             }
1191         }
1192         [entryString release];
1193     }
1194     
1195     //Add labels plus entires to the toolTip
1196     enumerator = [entryArray objectEnumerator];
1197     labelEnumerator = [labelArray objectEnumerator];
1198     
1199     while ((entryString = [enumerator nextObject])) {        
1200         NSAttributedString * labelAttribString = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"\t%@:\t",[labelEnumerator nextObject]]
1201                                                                                                                                                                  attributes:labelDict];
1202         
1203         //Add a carriage return
1204         [titleString appendString:@"\n" withAttributes:labelEndLineDict];
1205         
1206         if (isFirst) {
1207             //skip a line
1208             [titleString appendString:@"\n" withAttributes:labelEndLineDict];
1209             isFirst = NO;
1210         }
1211         
1212         //Add the label (with its spacing)
1213         [titleString appendAttributedString:labelAttribString];
1214                 [labelAttribString release];
1216                 [entryString addAttributes:entryDict range:NSMakeRange(0,[entryString length])];
1217         [titleString appendAttributedString:entryString];
1218     }
1220     return [titleString autorelease];
1223 - (NSAttributedString *)_tooltipBodyForObject:(AIListObject *)object
1225     NSMutableAttributedString       *tipString = [[NSMutableAttributedString alloc] init];
1226     
1227     //Configure fonts and attributes
1228     NSFontManager                   *fontManager = [NSFontManager sharedFontManager];
1229     NSFont                          *toolTipsFont = [NSFont toolTipsFontOfSize:10];
1230     NSMutableDictionary             *labelDict = [NSMutableDictionary dictionaryWithObjectsAndKeys:
1231         [fontManager convertFont:[NSFont toolTipsFontOfSize:9] toHaveTrait:NSBoldFontMask], NSFontAttributeName, nil];
1232     NSMutableDictionary             *labelEndLineDict = [NSMutableDictionary dictionaryWithObjectsAndKeys:[NSFont toolTipsFontOfSize:1], NSFontAttributeName, nil];
1233     NSMutableDictionary             *entryDict =[NSMutableDictionary dictionaryWithObjectsAndKeys:
1234         toolTipsFont, NSFontAttributeName, nil];
1235     
1236     //Entries from plugins
1237     id <AIContactListTooltipEntry>  tooltipEntry;
1238     NSEnumerator                    *enumerator;
1239     NSEnumerator                    *labelEnumerator; 
1240     NSMutableArray                  *labelArray = [NSMutableArray array];
1241     NSMutableArray                  *entryArray = [NSMutableArray array];    
1242     NSMutableAttributedString       *entryString;
1243     float                           labelWidth;
1244     BOOL                            firstEntry = YES;
1245     
1246     //Calculate the widest label while loading the arrays
1247         enumerator = [contactListTooltipSecondaryEntryArray objectEnumerator];
1248         
1249         while ((tooltipEntry = [enumerator nextObject])) {
1250                 
1251                 entryString = [[tooltipEntry entryForObject:object] mutableCopy];
1252                 if (entryString && [entryString length]) {
1253                         
1254                         NSString        *labelString = [tooltipEntry labelForObject:object];
1255                         if (labelString && [labelString length]) {
1256                                 
1257                                 [entryArray addObject:entryString];
1258                                 [labelArray addObject:labelString];
1259                                 
1260                                 NSAttributedString * labelAttribString = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@:",labelString] 
1261                                                                                                                                                                                  attributes:labelDict];
1262                                 
1263                                 //The largest size should be the label's size plus the distance to the next tab at least a space past its end
1264                                 labelWidth = [labelAttribString size].width;
1265                                 [labelAttribString release];
1266                                 
1267                                 if (labelWidth > maxLabelWidth)
1268                                         maxLabelWidth = labelWidth;
1269                         }
1270                 }
1271                 [entryString release];
1272         }
1273                 
1274     //Add labels plus entires to the toolTip
1275     enumerator = [entryArray objectEnumerator];
1276     labelEnumerator = [labelArray objectEnumerator];
1277     while ((entryString = [enumerator nextObject])) {
1278         NSMutableAttributedString *labelString = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"\t%@:\t",[labelEnumerator nextObject]]
1279                                                                                                                                                                                 attributes:labelDict];
1280         
1281         if (firstEntry) {
1282             firstEntry = NO;
1283         } else {
1284             //Add a carriage return and skip a line
1285             [tipString appendString:@"\n\n" withAttributes:labelEndLineDict];
1286         }
1287         
1288         //Add the label (with its spacing)
1289         [tipString appendAttributedString:labelString];
1290         [labelString release];
1292         NSRange fullLength = NSMakeRange(0, [entryString length]);
1293         
1294         //remove any background coloration
1295         [entryString removeAttribute:NSBackgroundColorAttributeName range:fullLength];
1296         
1297         //adjust foreground colors for the tooltip background
1298         [entryString adjustColorsToShowOnBackground:[NSColor colorWithCalibratedRed:1.000 green:1.000 blue:0.800 alpha:1.0]];
1300         //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
1301                 if ([entryString replaceOccurrencesOfString:@"\r" withString:@"\r\t\t" options:NSLiteralSearch range:fullLength])
1302             fullLength = NSMakeRange(0, [entryString length]);
1303         if ([entryString replaceOccurrencesOfString:@"\n" withString:@"\n\t\t" options:NSLiteralSearch range:fullLength])
1304             fullLength = NSMakeRange(0, [entryString length]);
1305                 
1306         //Run the entry through the filters and add it to tipString
1307                 entryString = [[[adium contentController] filterAttributedString:entryString
1308                                                                                                                  usingFilterType:AIFilterTooltips
1309                                                                                                                            direction:AIFilterIncoming
1310                                                                                                                                  context:object] mutableCopy];
1311                 
1312                 [entryString addAttributes:entryDict range:NSMakeRange(0,[entryString length])];
1313         [tipString appendAttributedString:entryString];
1314                 [entryString release];
1315     }
1317     return [tipString autorelease];
1320 //Custom pasting ----------------------------------------------------------------------------------------------------
1321 #pragma mark Custom Pasting
1322 //Paste, stripping formatting
1323 - (IBAction)paste:(id)sender
1325         [self _pasteWithPreferredSelector:@selector(pasteAsPlainTextWithTraits:) sender:sender];
1328 //Paste with formatting
1329 - (IBAction)pasteAndMatchStyle:(id)sender
1331         [self _pasteWithPreferredSelector:@selector(pasteAsPlainText:) sender:sender];
1334 - (IBAction)pasteWithImagesAndColors:(id)sender
1336         [self _pasteWithPreferredSelector:@selector(pasteAsRichText:) sender:sender];   
1340  * @brief Send a paste message, using preferredSelector if possible and paste: if not
1342  * Walks the responder chain looking for a responder which can handle pasting, skipping instances of
1343  * WebHTMLView.  These are skipped because we can control what paste does to WebView (by using a custom subclass) but
1344  * have no control over what the WebHTMLView would do.
1346  * If no responder is found, repeats the process looking for the simpler paste: selector.
1347  */
1348 - (void)_pasteWithPreferredSelector:(SEL)selector sender:(id)sender
1350         NSWindow        *keyWindow = [[NSApplication sharedApplication] keyWindow];
1351         NSResponder     *responder;
1353         //First, look for a responder which can handle the preferred selector
1354         if (!(responder = [keyWindow earliestResponderWhichRespondsToSelector:selector
1355                                                                                                                   andIsNotOfClass:NSClassFromString(@"WebHTMLView")])) {                
1356                 //No responder found.  Try again, looking for one which will respond to paste:
1357                 selector = @selector(paste:);
1358                 responder = [keyWindow earliestResponderWhichRespondsToSelector:selector
1359                                                                                                                 andIsNotOfClass:NSClassFromString(@"WebHTMLView")];
1360         }
1362         //Sending pasteAsRichText: to a non rich text NSTextView won't do anything; change it to a generic paste:
1363         if ([responder isKindOfClass:[NSTextView class]] && ![(NSTextView *)responder isRichText]) {
1364                 selector = @selector(paste:);
1365         }
1367         if (selector) {
1368                 [keyWindow makeFirstResponder:responder];
1369                 [responder performSelector:selector
1370                                                 withObject:sender];
1371         }
1374 //Custom Printing ------------------------------------------------------------------------------------------------------
1375 #pragma mark Custom Printing
1376 - (IBAction)adiumPrint:(id)sender
1378         //Pass the print command to the window, which is responsible for routing it to the correct place or
1379         //creating a view and printing.  Adium will not print from a window that does not respond to adiumPrint:
1380         NSWindow        *keyWindowController = [[[NSApplication sharedApplication] keyWindow] windowController];
1381         if ([keyWindowController respondsToSelector:@selector(adiumPrint:)]) {
1382                 [keyWindowController performSelector:@selector(adiumPrint:)
1383                                                                   withObject:sender];
1384         }
1387 #pragma mark Preferences Display
1388 - (IBAction)showPreferenceWindow:(id)sender
1390         [[adium preferenceController] showPreferenceWindow:sender];
1393 #pragma mark Font Panel
1394 - (IBAction)toggleFontPanel:(id)sender
1396         if ([NSFontPanel sharedFontPanelExists] &&
1397                 [[NSFontPanel sharedFontPanel] isVisible]) {
1398                 [[NSFontPanel sharedFontPanel] close];
1400         } else {
1401                 NSFontPanel     *fontPanel = [NSFontPanel sharedFontPanel];
1402                 
1403                 if (!fontPanelAccessoryView) {
1404                         [NSBundle loadNibNamed:@"FontPanelAccessoryView" owner:self];
1405                         [fontPanel setAccessoryView:fontPanelAccessoryView];
1406                 }
1407                 
1408                 [fontPanel orderFront:self]; 
1409         }
1412 - (IBAction)setFontPanelSettingsAsDefaultFont:(id)sender
1414         NSFont  *selectedFont = [[NSFontManager sharedFontManager] selectedFont];
1416         [[adium preferenceController] setPreference:[selectedFont stringRepresentation]
1417                                                                                  forKey:KEY_FORMATTING_FONT
1418                                                                                   group:PREF_GROUP_FORMATTING];
1419         
1420         //We can't get foreground/background color from the font panel so far as I can tell... so we do the best we can.
1421         NSWindow        *keyWindow = [[NSApplication sharedApplication] keyWindow];
1422         NSResponder *responder = [keyWindow firstResponder]; 
1423         if ([responder isKindOfClass:[NSTextView class]]) {
1424                 NSDictionary    *typingAttributes = [(NSTextView *)responder typingAttributes];
1425                 NSColor                 *foregroundColor, *backgroundColor;
1427                 if ((foregroundColor = [typingAttributes objectForKey:NSForegroundColorAttributeName])) {
1428                         [[adium preferenceController] setPreference:[foregroundColor stringRepresentation]
1429                                                                                                  forKey:KEY_FORMATTING_TEXT_COLOR
1430                                                                                                   group:PREF_GROUP_FORMATTING];
1431                 }
1433                 if ((backgroundColor = [typingAttributes objectForKey:AIBodyColorAttributeName])) {
1434                         [[adium preferenceController] setPreference:[backgroundColor stringRepresentation]
1435                                                                                                  forKey:KEY_FORMATTING_BACKGROUND_COLOR
1436                                                                                                   group:PREF_GROUP_FORMATTING];
1437                 }
1438         }
1441 //Custom Dimming menu items --------------------------------------------------------------------------------------------
1442 #pragma mark Custom Dimming menu items
1443 //The standard ones do not dim correctly when unavailable
1444 - (IBAction)toggleFontTrait:(id)sender
1446     NSFontManager       *fontManager = [NSFontManager sharedFontManager];
1447     
1448     if ([fontManager traitsOfFont:[fontManager selectedFont]] & [sender tag]) {
1449         [fontManager removeFontTrait:sender];
1450     } else {
1451         [fontManager addFontTrait:sender];
1452     }
1455 - (void)toggleToolbarShown:(id)sender
1457         NSWindow        *window = [[NSApplication sharedApplication] keyWindow];        
1458         [window toggleToolbarShown:sender];
1461 - (void)runToolbarCustomizationPalette:(id)sender
1463         NSWindow        *window = [[NSApplication sharedApplication] keyWindow];        
1464         [window runToolbarCustomizationPalette:sender];
1467 //Menu item validation
1468 - (BOOL)validateMenuItem:(NSMenuItem *)menuItem
1470         NSWindow        *keyWindow = [[NSApplication sharedApplication] keyWindow];
1471         NSResponder *responder = [keyWindow firstResponder]; 
1473     if (menuItem == menuItem_bold || menuItem == menuItem_italic) {
1474                 NSFont                  *selectedFont = [[NSFontManager sharedFontManager] selectedFont];
1475                 
1476                 //We must be in a text view, have text on the pasteboard, and have a font that supports bold or italic
1477                 if ([responder isKindOfClass:[NSTextView class]]) {
1478                         return (menuItem == menuItem_bold ? [selectedFont supportsBold] : [selectedFont supportsItalics]);
1479                 }
1480                 return NO;
1481                 
1482         } else if (menuItem == menuItem_paste || menuItem == menuItem_pasteAndMatchStyle || menuItem == menuItem_pasteWithImagesAndColors) {
1484                 //The user can paste if the pasteboard contains an image, some text, one or more files, or one or more URLs.
1485                 NSPasteboard *pboard = [NSPasteboard generalPasteboard];
1486                 NSArray *nonImageTypes = [NSArray arrayWithObjects:
1487                         NSStringPboardType,
1488                         NSRTFPboardType,
1489                         NSURLPboardType,
1490                         NSFilenamesPboardType,
1491                         NSFilesPromisePboardType,
1492                         NSRTFDPboardType,
1493                         nil];
1494                 return ([pboard availableTypeFromArray:nonImageTypes] != nil) || [NSImage canInitWithPasteboard:pboard];
1495         
1496         } else if (menuItem == menuItem_showToolbar) {
1497                 [menuItem_showToolbar setTitle:([[keyWindow toolbar] isVisible] ? 
1498                                                                                 AILocalizedString(@"Hide Toolbar",nil) : 
1499                                                                                 AILocalizedString(@"Show Toolbar",nil))];
1500                 return [keyWindow toolbar] != nil;
1501         
1502         } else if (menuItem == menuItem_customizeToolbar) {
1503                 return ([keyWindow toolbar] != nil && [[keyWindow toolbar] isVisible] && [[keyWindow windowController] canCustomizeToolbar]);
1505         } else if (menuItem == menuItem_close) {
1506                 return (keyWindow && ([[keyWindow standardWindowButton:NSWindowCloseButton] isEnabled] ||
1507                                                           ([[keyWindow windowController] respondsToSelector:@selector(windowPermitsClose)] &&
1508                                                            [[keyWindow windowController] windowPermitsClose])));
1509                 
1510         } else if (menuItem == menuItem_closeChat) {
1511                 return activeChat != nil;
1512                 
1513         } else if( menuItem == menuItem_closeAllChats) {
1514                 return [[self openChats] count] > 0;
1516         } else if (menuItem == menuItem_print) {
1517                 NSWindowController *windowController = [keyWindow windowController];
1519                 return ([windowController respondsToSelector:@selector(adiumPrint:)] &&
1520                                 (![windowController respondsToSelector:@selector(validatePrintMenuItem:)] ||
1521                                  [windowController validatePrintMenuItem:menuItem]));
1522                 
1523         } else if (menuItem == menuItem_showFonts) {
1524                 [menuItem_showFonts setTitle:(([NSFontPanel sharedFontPanelExists] && [[NSFontPanel sharedFontPanel] isVisible]) ?
1525                                                                           AILocalizedString(@"Hide Fonts",nil) :
1526                                                                           AILocalizedString(@"Show Fonts",nil))];
1527                 return YES;
1528         } else {
1529                 return YES;
1530         }
1533 #pragma mark Window levels
1534 - (NSMenu *)menuForWindowLevelsNotifyingTarget:(id)target
1536         NSMenu          *windowPositionMenu = [[NSMenu allocWithZone:[NSMenu zone]] init];
1537         NSMenuItem      *menuItem;
1538         
1539         menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Above other windows",nil)
1540                                                                                                                                         target:target
1541                                                                                                                                         action:@selector(selectedWindowLevel:)
1542                                                                                                                          keyEquivalent:@""];
1543         [menuItem setEnabled:YES];
1544         [menuItem setTag:AIFloatingWindowLevel];
1545         [windowPositionMenu addItem:menuItem];
1546         [menuItem release];
1547         
1548         menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Normally",nil)
1549                                                                                                                                         target:target
1550                                                                                                                                         action:@selector(selectedWindowLevel:)
1551                                                                                                                          keyEquivalent:@""];
1552         [menuItem setEnabled:YES];
1553         [menuItem setTag:AINormalWindowLevel];
1554         [windowPositionMenu addItem:menuItem];
1555         [menuItem release];
1556         
1557         menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Below other windows",nil)
1558                                                                                                                                         target:target
1559                                                                                                                                         action:@selector(selectedWindowLevel:)
1560                                                                                                                          keyEquivalent:@""];
1561         [menuItem setEnabled:YES];
1562         [menuItem setTag:AIDesktopWindowLevel];
1563         [windowPositionMenu addItem:menuItem];
1564         [menuItem release];
1565         
1566         [windowPositionMenu setAutoenablesItems:NO];
1568         return [windowPositionMenu autorelease];
1571 @end