2 * Adium is the legal property of its developers, whose names are listed in the copyright file included
3 * with this source distribution.
5 * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
6 * General Public License as published by the Free Software Foundation; either version 2 of the License,
7 * or (at your option) any later version.
9 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
10 * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
11 * Public License for more details.
13 * You should have received a copy of the GNU General Public License along with this program; if not,
14 * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
19 #import "AIInterfaceController.h"
21 #import <Adium/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;
65 - (void)updateActiveWindowMenuItem;
66 - (void)buildWindowMenu;
68 - (AIChat *)mostRecentActiveChat;
72 * @class AIInterfaceController
73 * @brief Interface controller
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.
80 * Contact list window requests, such as toggling window visibilty are routed to the contact list controller component.
82 * Error messages are routed through the interface controller.
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.
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.
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.
93 @implementation AIInterfaceController
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.
101 [[KFTypeSelectTableView class] poseAsClass:[NSTableView class]];
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;
112 tooltipListObject = nil;
116 flashObserverArray = nil;
120 windowMenuArray = nil;
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];
133 NSWindow *keyWindow = [[NSApplication sharedApplication] keyWindow];
134 [responderChain appendFormat:@"%@ (%i): ",keyWindow,[keyWindow respondsToSelector:@selector(print:)]];
136 NSResponder *responder = [keyWindow firstResponder];
138 //First, walk down the responder chain looking for a responder which can handle the preferred selector
140 [responderChain appendFormat:@"%@ (%i)",responder,[responder respondsToSelector:@selector(print:)]];
141 responder = [responder nextResponder];
142 if (responder) [responderChain appendString:@" -> "];
145 NSLog(responderChain);
149 - (void)controllerDidLoad
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")
160 action:@selector(toggleContactList:)
162 [[adium menuController] addMenuItem:menuItem toLocation:LOC_Window_Fixed];
163 [[adium menuController] addMenuItem:[[menuItem copy] autorelease] toLocation:LOC_Dock_Status];
166 menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Close Chat","Title for the close chat menu item")
168 action:@selector(closeContextualChat:)
170 [[adium menuController] addContextualMenuItem:menuItem toLocation:Context_Tab_Action];
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];
190 [contactListViewArray release]; contactListViewArray = nil;
191 [messageViewArray release]; messageViewArray = nil;
192 [interfaceArray release]; interfaceArray = nil;
194 [tooltipListObject release]; tooltipListObject = nil;
195 [tooltipTitle release]; tooltipTitle = nil;
196 [tooltipBody release]; tooltipBody = nil;
197 [tooltipImage release]; tooltipImage = nil;
199 [[adium notificationCenter] removeObserver:self];
200 [[adium preferenceController] unregisterPreferenceObserver:self];
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
222 [[adium notificationCenter] removeObserver:self name:Contact_OrderChanged object:nil];
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];
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];
248 //Otherwise, switch to the chat which most recently received content
249 [self setActiveChat:mostRecentUnviewedChat];
253 NSEnumerator *enumerator;
254 NSWindow *window, *targetWindow = nil;
255 BOOL unMinimizedWindows = 0;
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++;
267 //If there are no unminimized windows, unminimize the last one
268 if (unMinimizedWindows == 0 && targetWindow) {
269 [targetWindow deminiaturize:nil];
274 //We handled the reopen; return NO so NSApp does nothing.
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];
286 [self showContactList:nil];
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;
319 //Determine the correct container for this chat
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];
326 containerID = [inChat name];
329 } else if (groupChatsByContactGroup) {
330 if ([inChat isGroupChat]) {
331 containerID = AILocalizedString(@"Group Chat",nil);
334 AIListObject *group = [[[inChat listObject] parentContact] containingObject];
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];
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"];
348 containerID = AILocalizedString(@"Chat",nil);
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];
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];
363 * @brief Close the interface for a chat
365 * Tell the interface plugin to close the chat.
367 - (void)closeChat:(AIChat *)inChat
370 if ([[adium chatController] closeChat:inChat]) {
371 [interfacePlugin closeChat:inChat];
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;
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];
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
401 [self chatOrderDidChange];
403 [openContainers release];
407 - (AIChat *)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];
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;
467 activeChat = [inChat retain];
469 [self updateCloseMenuKeys];
470 [self updateActiveWindowMenuItem];
472 if (inChat && (inChat != mostRecentActiveChat)) {
473 [mostRecentActiveChat release]; mostRecentActiveChat = nil;
474 mostRecentActiveChat = [inChat retain];
477 [[adium notificationCenter] postNotificationName:Chat_BecameActive
479 userInfo:(previouslyActiveChat ?
480 [NSDictionary dictionaryWithObject:previouslyActiveChat
481 forKey:@"PreviouslyActiveChat"] :
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.
489 [inChat performSelector:@selector(clearUnviewedContentCount)
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"
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
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
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];
533 if (inChat == activeChat) {
534 [activeChat release]; activeChat = nil;
537 if (inChat == mostRecentActiveChat) {
538 [mostRecentActiveChat release]; mostRecentActiveChat = nil;
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"];
556 if (chat != activeChat) {
557 [chat incrementUnviewedContentCount];
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];
587 while ((chatToClose = [containerEnumerator nextObject])) {
588 [self closeChat:chatToClose];
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;
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];
622 while ((item = [enumerator nextObject])) {
623 if ([item representedObject]) [item setState:([item representedObject] == activeChat ? NSOnState : NSOffState)];
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
633 NSEnumerator *enumerator;
636 //Remove any existing menus
637 enumerator = [windowMenuArray objectEnumerator];
638 while ((item = [enumerator nextObject])) {
639 [[adium menuController] removeMenuItem:item];
641 [windowMenuArray release]; windowMenuArray = [[NSMutableArray alloc] init];
643 //Messages window and any open messasges
644 NSEnumerator *containerEnumerator = [[interfacePlugin openContainersAndChats] objectEnumerator];
645 NSDictionary *containerDict;
647 while ((containerDict = [containerEnumerator nextObject])) {
648 NSString *containerName = [containerDict objectForKey:@"Name"];
649 NSArray *contentArray = [containerDict objectForKey:@"Content"];
650 NSEnumerator *contentEnumerator = [contentArray objectEnumerator];
653 //Add a menu item for the container
654 if ([contentArray count] > 1) {
655 item = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:containerName
659 [self _addItemToMainMenuAndDock:item];
663 //Add items for the chats it contains
664 while ((chat = [contentEnumerator nextObject])) {
665 NSString *windowKeyString;
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"];
673 windowKeyString = [NSString stringWithString:@""];
676 item = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:[chat displayName]
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];
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];
700 //Make a copy, and add to the dock
702 [item setKeyEquivalent:@""];
703 [[adium menuController] addMenuItem:item toLocation:LOC_Dock_Status];
704 [windowMenuArray addObject:item];
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]) {
718 int chatIndex = [openChats indexOfObject:activeChat]+1;
719 [self setActiveChat:[openChats objectAtIndex:(chatIndex < [openChats count] ? chatIndex : 0)]];
721 [self setActiveChat:[openChats objectAtIndex:0]];
726 //Select the previous message
727 - (void)previousChat:(id)sender
729 NSArray *openChats = [self openChats];
731 if ([openChats count]) {
733 int chatIndex = [openChats indexOfObject:activeChat]-1;
734 [self setActiveChat:[openChats objectAtIndex:(chatIndex >= 0 ? chatIndex : [openChats count]-1)]];
736 [self setActiveChat:[openChats lastObject]];
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];
751 //Search the responder chain
753 responder = [responder nextResponder];
754 if ([responder respondsToSelector:selector]) {
755 return [responder performSelector:selector];
758 } while (responder != nil);
760 //None found, 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];
771 //Search the responder chain
773 responder = [responder nextResponder];
774 if ([responder conformsToProtocol:protocol] && [responder respondsToSelector:selector]) {
775 return [responder performSelector:selector];
778 } while (responder != nil);
780 //None found, 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)];
790 listObject = [self _performSelectorOnFirstAvailableResponder:@selector(preferredListObject)];
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.",
822 [NSApp terminate:nil];
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;
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];
860 [questionDict setObject:inTitle forKey:@"Title"];
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"];
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"];
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
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)
907 selector:@selector(flashTimer:)
909 repeats:YES] retain];
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];
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;
930 //Timer, invoke a flash
931 - (void)flashTimer:(NSTimer *)inTimer
933 NSEnumerator *enumerator;
934 id<AIFlashObserver> observer;
938 enumerator = [flashObserverArray objectEnumerator];
939 while ((observer = [enumerator nextObject])) {
940 [observer flash:flashState];
944 //Current state of flashing. This is an integer the increases by 1 with every flash. Mod to whatever range is desired
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
957 [contactListTooltipSecondaryEntryArray addObject:inEntry];
959 [contactListTooltipEntryArray addObject:inEntry];
962 //list object tooltips
963 - (void)showTooltipForListObject:(AIListObject *)object atScreenPoint:(NSPoint)point onWindow:(NSWindow *)inWindow
966 if (object == tooltipListObject) { //If we already have this tooltip open
967 //Move the existing tooltip
968 [AITooltipUtilities showTooltipWithTitle:tooltipTitle
971 imageOnRight:DISPLAY_IMAGE_ON_RIGHT
974 orientation:TooltipBelow];
976 } else { //This is a new tooltip
978 NSMutableParagraphStyle *paragraphStyleTitle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
979 NSMutableParagraphStyle *paragraphStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
981 //Hold onto the new object
982 [tooltipListObject release]; tooltipListObject = [object retain];
985 [tooltipImage release];
986 tooltipImage = [[tooltipListObject userIcon] retain];
987 if (!tooltipImage) tooltipImage = [[AIServiceIcons serviceIconForObject:tooltipListObject
988 type:AIServiceIconLarge
989 direction:AIIconNormal] retain];
991 //Reset the maxLabelWidth for the tooltip generation
994 //Build a tooltip string for the primary information
995 [tooltipTitle release]; tooltipTitle = [[self _tooltipTitleForObject:object] retain];
997 //If there is an image, set the title tab and indentation settings independently
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]
1006 [paragraphStyleTitle setTabStops:tabArray];
1009 [paragraphStyleTitle setHeadIndent:(maxLabelWidth + LABEL_ENTRY_SPACING)];
1011 [tooltipTitle addAttribute:NSParagraphStyleAttributeName
1012 value:paragraphStyleTitle
1013 range:NSMakeRange(0,[tooltipTitle length])];
1015 //Reset the max label width since the body will be independent
1019 //Build a tooltip string for the secondary information
1020 [tooltipBody release]; tooltipBody = nil;
1021 tooltipBody = [[self _tooltipBodyForObject:object] retain];
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]
1029 [paragraphStyle setTabStops:tabArray];
1031 [paragraphStyle setHeadIndent:(maxLabelWidth + LABEL_ENTRY_SPACING)];
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])];
1039 //Display the new tooltip
1040 [AITooltipUtilities showTooltipWithTitle:tooltipTitle
1043 imageOnRight:DISPLAY_IMAGE_ON_RIGHT
1046 orientation:TooltipBelow];
1048 [paragraphStyleTitle release];
1049 [paragraphStyle release];
1053 //Hide the existing tooltip
1054 if (tooltipListObject) {
1055 [AITooltipUtilities showTooltipWithTitle:nil
1060 orientation:TooltipBelow];
1061 [tooltipListObject release]; tooltipListObject = nil;
1063 [tooltipTitle release]; tooltipTitle = nil;
1064 [tooltipBody release]; tooltipBody = nil;
1065 [tooltipImage release]; tooltipImage = nil;
1070 - (NSAttributedString *)_tooltipTitleForObject:(AIListObject *)object
1072 NSMutableAttributedString *titleString = [[NSMutableAttributedString alloc] init];
1074 id <AIContactListTooltipEntry> tooltipEntry;
1075 NSEnumerator *enumerator;
1076 NSEnumerator *labelEnumerator;
1077 NSMutableArray *labelArray = [NSMutableArray array];
1078 NSMutableArray *entryArray = [NSMutableArray array];
1079 NSMutableAttributedString *entryString;
1083 NSString *formattedUID = [object formattedUID];
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];
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
1104 //Append the user's display name
1105 if (filteredDisplayName) {
1106 [titleString appendAttributedString:filteredDisplayName];
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];
1113 [displayName release];
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)];
1121 NSTextAttachment *attachment;
1122 NSTextAttachmentCell *cell;
1124 cell = [[NSTextAttachmentCell alloc] init];
1125 [cell setImage:serviceIcon];
1127 attachment = [[NSTextAttachment alloc] init];
1128 [attachment setAttachmentCell:cell];
1131 [titleString appendString:@" " withAttributes:nil];
1132 [titleString appendAttributedString:[NSAttributedString attributedStringWithAttachment:attachment]];
1133 [attachment release];
1138 if ([object isKindOfClass:[AIListGroup class]]) {
1139 [titleString appendString:[NSString stringWithFormat:@" (%i/%i)",[(AIListGroup *)object visibleCount],[(AIListGroup *)object containedObjectsCount]]
1140 withAttributes:titleDict];
1143 //Entries from plugins
1145 //Calculate the widest label while loading the arrays
1146 enumerator = [contactListTooltipEntryArray objectEnumerator];
1148 while ((tooltipEntry = [enumerator nextObject])) {
1150 entryString = [[tooltipEntry entryForObject:object] mutableCopy];
1151 if (entryString && [entryString length]) {
1153 NSString *labelString = [tooltipEntry labelForObject:object];
1154 if (labelString && [labelString length]) {
1156 [entryArray addObject:entryString];
1157 [labelArray addObject:labelString];
1159 NSAttributedString * labelAttribString = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@:",labelString]
1160 attributes:labelDict];
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];
1166 if (labelWidth > maxLabelWidth)
1167 maxLabelWidth = labelWidth;
1170 [entryString release];
1173 //Add labels plus entires to the toolTip
1174 enumerator = [entryArray objectEnumerator];
1175 labelEnumerator = [labelArray objectEnumerator];
1177 while ((entryString = [enumerator nextObject])) {
1178 NSAttributedString * labelAttribString = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"\t%@:\t",[labelEnumerator nextObject]]
1179 attributes:labelDict];
1181 //Add a carriage return
1182 [titleString appendString:@"\n" withAttributes:labelEndLineDict];
1186 [titleString appendString:@"\n" withAttributes:labelEndLineDict];
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];
1198 return [titleString autorelease];
1201 - (NSAttributedString *)_tooltipBodyForObject:(AIListObject *)object
1203 NSMutableAttributedString *tipString = [[NSMutableAttributedString alloc] init];
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];
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;
1222 BOOL firstEntry = YES;
1224 //Calculate the widest label while loading the arrays
1225 enumerator = [contactListTooltipSecondaryEntryArray objectEnumerator];
1227 while ((tooltipEntry = [enumerator nextObject])) {
1229 entryString = [[tooltipEntry entryForObject:object] mutableCopy];
1230 if (entryString && [entryString length]) {
1232 NSString *labelString = [tooltipEntry labelForObject:object];
1233 if (labelString && [labelString length]) {
1235 [entryArray addObject:entryString];
1236 [labelArray addObject:labelString];
1238 NSAttributedString * labelAttribString = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@:",labelString]
1239 attributes:labelDict];
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];
1245 if (labelWidth > maxLabelWidth)
1246 maxLabelWidth = labelWidth;
1249 [entryString release];
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];
1262 //Add a carriage return and skip a line
1263 [tipString appendString:@"\n\n" withAttributes:labelEndLineDict];
1266 //Add the label (with its spacing)
1267 [tipString appendAttributedString:labelString];
1268 [labelString release];
1270 NSRange fullLength = NSMakeRange(0, [entryString length]);
1272 //remove any background coloration
1273 [entryString removeAttribute:NSBackgroundColorAttributeName range:fullLength];
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]);
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];
1290 [entryString addAttributes:entryDict range:NSMakeRange(0,[entryString length])];
1291 [tipString appendAttributedString:entryString];
1292 [entryString release];
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.
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")];
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:);
1346 [keyWindow makeFirstResponder:responder];
1347 [responder performSelector:selector
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:)
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];
1379 NSFontPanel *fontPanel = [NSFontPanel sharedFontPanel];
1381 if (!fontPanelAccessoryView) {
1382 [NSBundle loadNibNamed:@"FontPanelAccessoryView" owner:self];
1383 [fontPanel setAccessoryView:fontPanelAccessoryView];
1386 [fontPanel orderFront:self];
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];
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];
1411 if ((backgroundColor = [typingAttributes objectForKey:AIBodyColorAttributeName])) {
1412 [[adium preferenceController] setPreference:[backgroundColor stringRepresentation]
1413 forKey:KEY_FORMATTING_BACKGROUND_COLOR
1414 group:PREF_GROUP_FORMATTING];
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];
1426 if ([fontManager traitsOfFont:[fontManager selectedFont]] & [sender tag]) {
1427 [fontManager removeFontTrait:sender];
1429 [fontManager addFontTrait:sender];
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];
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]);
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;
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;
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])));
1477 } else if (menuItem == menuItem_closeChat) {
1478 return activeChat != nil;
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]));
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))];
1500 #pragma mark Window levels
1501 - (NSMenu *)menuForWindowLevelsNotifyingTarget:(id)target
1503 NSMenu *windowPositionMenu = [[NSMenu allocWithZone:[NSMenu zone]] init];
1504 NSMenuItem *menuItem;
1506 menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Above other windows",nil)
1508 action:@selector(selectedWindowLevel:)
1510 [menuItem setEnabled:YES];
1511 [menuItem setTag:AIFloatingWindowLevel];
1512 [windowPositionMenu addItem:menuItem];
1515 menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Normally",nil)
1517 action:@selector(selectedWindowLevel:)
1519 [menuItem setEnabled:YES];
1520 [menuItem setTag:AINormalWindowLevel];
1521 [windowPositionMenu addItem:menuItem];
1524 menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Below other windows",nil)
1526 action:@selector(selectedWindowLevel:)
1528 [menuItem setEnabled:YES];
1529 [menuItem setTag:AIDesktopWindowLevel];
1530 [windowPositionMenu addItem:menuItem];
1533 [windowPositionMenu setAutoenablesItems:NO];
1535 return [windowPositionMenu autorelease];