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.
236 //If no windows are visible, show the contact list
237 if (![self contactListIsVisibleAndMain] && [[interfacePlugin openContainers] count] == 0) {
238 [self showContactList:nil];
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];
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];
276 //We handled the reopen; return NO so NSApp does nothing.
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];
288 [self showContactList:nil];
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;
332 //Determine the correct container for this chat
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];
339 containerID = [inChat name];
342 } else if (groupChatsByContactGroup) {
343 if ([inChat isGroupChat]) {
344 containerID = AILocalizedString(@"Group Chat",nil);
347 AIListObject *group = [[[inChat listObject] parentContact] containingObject];
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];
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"];
361 containerID = AILocalizedString(@"Chat",nil);
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];
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];
376 * @brief Close the interface for a chat
378 * Tell the interface plugin to close the chat.
380 - (void)closeChat:(AIChat *)inChat
383 if ([[adium chatController] closeChat:inChat]) {
384 [interfacePlugin closeChat:inChat];
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;
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];
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
414 [self chatOrderDidChange];
416 [openContainers release];
420 - (AIChat *)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];
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;
480 activeChat = [inChat retain];
482 [self updateCloseMenuKeys];
483 [self updateActiveWindowMenuItem];
485 if (inChat && (inChat != mostRecentActiveChat)) {
486 [mostRecentActiveChat release]; mostRecentActiveChat = nil;
487 mostRecentActiveChat = [inChat retain];
490 [[adium notificationCenter] postNotificationName:Chat_BecameActive
492 userInfo:(previouslyActiveChat ?
493 [NSDictionary dictionaryWithObject:previouslyActiveChat
494 forKey:@"PreviouslyActiveChat"] :
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.
502 [inChat performSelector:@selector(clearUnviewedContentCount)
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"
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
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
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];
546 if (inChat == activeChat) {
547 [activeChat release]; activeChat = nil;
550 if (inChat == mostRecentActiveChat) {
551 [mostRecentActiveChat release]; mostRecentActiveChat = nil;
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"];
569 if (chat != activeChat) {
570 [chat incrementUnviewedContentCount];
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];
600 while ((chatToClose = [containerEnumerator nextObject])) {
601 [self closeChat:chatToClose];
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;
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];
635 while ((item = [enumerator nextObject])) {
636 if ([item representedObject]) [item setState:([item representedObject] == activeChat ? NSOnState : NSOffState)];
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
646 NSEnumerator *enumerator;
649 //Remove any existing menus
650 enumerator = [windowMenuArray objectEnumerator];
651 while ((item = [enumerator nextObject])) {
652 [[adium menuController] removeMenuItem:item];
654 [windowMenuArray release]; windowMenuArray = [[NSMutableArray alloc] init];
656 //Messages window and any open messasges
657 NSEnumerator *containerEnumerator = [[interfacePlugin openContainersAndChats] objectEnumerator];
658 NSDictionary *containerDict;
660 while ((containerDict = [containerEnumerator nextObject])) {
661 NSString *containerName = [containerDict objectForKey:@"Name"];
662 NSArray *contentArray = [containerDict objectForKey:@"Content"];
663 NSEnumerator *contentEnumerator = [contentArray objectEnumerator];
666 //Add a menu item for the container
667 if ([contentArray count] > 1) {
668 item = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:containerName
672 [self _addItemToMainMenuAndDock:item];
676 //Add items for the chats it contains
677 while ((chat = [contentEnumerator nextObject])) {
678 NSString *windowKeyString;
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"];
686 windowKeyString = [NSString stringWithString:@""];
689 item = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:[chat displayName]
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];
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];
713 //Make a copy, and add to the dock
715 [item setKeyEquivalent:@""];
716 [[adium menuController] addMenuItem:item toLocation:LOC_Dock_Status];
717 [windowMenuArray addObject:item];
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]) {
731 int chatIndex = [openChats indexOfObject:activeChat]+1;
732 [self setActiveChat:[openChats objectAtIndex:(chatIndex < [openChats count] ? chatIndex : 0)]];
734 [self setActiveChat:[openChats objectAtIndex:0]];
739 //Select the previous message
740 - (void)previousChat:(id)sender
742 NSArray *openChats = [self openChats];
744 if ([openChats count]) {
746 int chatIndex = [openChats indexOfObject:activeChat]-1;
747 [self setActiveChat:[openChats objectAtIndex:(chatIndex >= 0 ? chatIndex : [openChats count]-1)]];
749 [self setActiveChat:[openChats lastObject]];
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];
764 //Search the responder chain
766 responder = [responder nextResponder];
767 if ([responder respondsToSelector:selector]) {
768 return [responder performSelector:selector];
771 } while (responder != nil);
773 //None found, 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];
784 //Search the responder chain
786 responder = [responder nextResponder];
787 if ([responder conformsToProtocol:protocol] && [responder respondsToSelector:selector]) {
788 return [responder performSelector:selector];
791 } while (responder != nil);
793 //None found, 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)];
803 listObject = [self _performSelectorOnFirstAvailableResponder:@selector(preferredListObject)];
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.",
835 [NSApp terminate:nil];
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;
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];
873 [questionDict setObject:inTitle forKey:@"Title"];
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"];
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"];
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
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)
920 selector:@selector(flashTimer:)
922 repeats:YES] retain];
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];
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;
943 //Timer, invoke a flash
944 - (void)flashTimer:(NSTimer *)inTimer
946 NSEnumerator *enumerator;
947 id<AIFlashObserver> observer;
951 enumerator = [flashObserverArray objectEnumerator];
952 while ((observer = [enumerator nextObject])) {
953 [observer flash:flashState];
957 //Current state of flashing. This is an integer the increases by 1 with every flash. Mod to whatever range is desired
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
970 [contactListTooltipSecondaryEntryArray addObject:inEntry];
972 [contactListTooltipEntryArray addObject:inEntry];
975 //Unregisters code to display tooltip info about a contact
976 - (void)unregisterContactListTooltipEntry:(id <AIContactListTooltipEntry>)inEntry secondaryEntry:(BOOL)isSecondary
979 [contactListTooltipSecondaryEntryArray removeObject:inEntry];
981 [contactListTooltipEntryArray removeObject:inEntry];
984 //list object tooltips
985 - (void)showTooltipForListObject:(AIListObject *)object atScreenPoint:(NSPoint)point onWindow:(NSWindow *)inWindow
988 if (object == tooltipListObject) { //If we already have this tooltip open
989 //Move the existing tooltip
990 [AITooltipUtilities showTooltipWithTitle:tooltipTitle
993 imageOnRight:DISPLAY_IMAGE_ON_RIGHT
996 orientation:TooltipBelow];
998 } else { //This is a new tooltip
1000 NSMutableParagraphStyle *paragraphStyleTitle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
1001 NSMutableParagraphStyle *paragraphStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
1003 //Hold onto the new object
1004 [tooltipListObject release]; tooltipListObject = [object retain];
1007 [tooltipImage release];
1008 tooltipImage = [[tooltipListObject userIcon] retain];
1009 if (!tooltipImage) tooltipImage = [[AIServiceIcons serviceIconForObject:tooltipListObject
1010 type:AIServiceIconLarge
1011 direction:AIIconNormal] retain];
1013 //Reset the maxLabelWidth for the tooltip generation
1016 //Build a tooltip string for the primary information
1017 [tooltipTitle release]; tooltipTitle = [[self _tooltipTitleForObject:object] retain];
1019 //If there is an image, set the title tab and indentation settings independently
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]
1028 [paragraphStyleTitle setTabStops:tabArray];
1031 [paragraphStyleTitle setHeadIndent:(maxLabelWidth + LABEL_ENTRY_SPACING)];
1033 [tooltipTitle addAttribute:NSParagraphStyleAttributeName
1034 value:paragraphStyleTitle
1035 range:NSMakeRange(0,[tooltipTitle length])];
1037 //Reset the max label width since the body will be independent
1041 //Build a tooltip string for the secondary information
1042 [tooltipBody release]; tooltipBody = nil;
1043 tooltipBody = [[self _tooltipBodyForObject:object] retain];
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]
1051 [paragraphStyle setTabStops:tabArray];
1053 [paragraphStyle setHeadIndent:(maxLabelWidth + LABEL_ENTRY_SPACING)];
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])];
1061 //Display the new tooltip
1062 [AITooltipUtilities showTooltipWithTitle:tooltipTitle
1065 imageOnRight:DISPLAY_IMAGE_ON_RIGHT
1068 orientation:TooltipBelow];
1070 [paragraphStyleTitle release];
1071 [paragraphStyle release];
1075 //Hide the existing tooltip
1076 if (tooltipListObject) {
1077 [AITooltipUtilities showTooltipWithTitle:nil
1082 orientation:TooltipBelow];
1083 [tooltipListObject release]; tooltipListObject = nil;
1085 [tooltipTitle release]; tooltipTitle = nil;
1086 [tooltipBody release]; tooltipBody = nil;
1087 [tooltipImage release]; tooltipImage = nil;
1092 - (NSAttributedString *)_tooltipTitleForObject:(AIListObject *)object
1094 NSMutableAttributedString *titleString = [[NSMutableAttributedString alloc] init];
1096 id <AIContactListTooltipEntry> tooltipEntry;
1097 NSEnumerator *enumerator;
1098 NSEnumerator *labelEnumerator;
1099 NSMutableArray *labelArray = [NSMutableArray array];
1100 NSMutableArray *entryArray = [NSMutableArray array];
1101 NSMutableAttributedString *entryString;
1105 NSString *formattedUID = [object formattedUID];
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];
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
1126 //Append the user's display name
1127 if (filteredDisplayName) {
1128 [titleString appendAttributedString:filteredDisplayName];
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];
1135 [displayName release];
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)];
1143 NSTextAttachment *attachment;
1144 NSTextAttachmentCell *cell;
1146 cell = [[NSTextAttachmentCell alloc] init];
1147 [cell setImage:serviceIcon];
1149 attachment = [[NSTextAttachment alloc] init];
1150 [attachment setAttachmentCell:cell];
1153 [titleString appendString:@" " withAttributes:nil];
1154 [titleString appendAttributedString:[NSAttributedString attributedStringWithAttachment:attachment]];
1155 [attachment release];
1160 if ([object isKindOfClass:[AIListGroup class]]) {
1161 [titleString appendString:[NSString stringWithFormat:@" (%i/%i)",[(AIListGroup *)object visibleCount],[(AIListGroup *)object containedObjectsCount]]
1162 withAttributes:titleDict];
1165 //Entries from plugins
1167 //Calculate the widest label while loading the arrays
1168 enumerator = [contactListTooltipEntryArray objectEnumerator];
1170 while ((tooltipEntry = [enumerator nextObject])) {
1172 entryString = [[tooltipEntry entryForObject:object] mutableCopy];
1173 if (entryString && [entryString length]) {
1175 NSString *labelString = [tooltipEntry labelForObject:object];
1176 if (labelString && [labelString length]) {
1178 [entryArray addObject:entryString];
1179 [labelArray addObject:labelString];
1181 NSAttributedString * labelAttribString = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@:",labelString]
1182 attributes:labelDict];
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];
1188 if (labelWidth > maxLabelWidth)
1189 maxLabelWidth = labelWidth;
1192 [entryString release];
1195 //Add labels plus entires to the toolTip
1196 enumerator = [entryArray objectEnumerator];
1197 labelEnumerator = [labelArray objectEnumerator];
1199 while ((entryString = [enumerator nextObject])) {
1200 NSAttributedString * labelAttribString = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"\t%@:\t",[labelEnumerator nextObject]]
1201 attributes:labelDict];
1203 //Add a carriage return
1204 [titleString appendString:@"\n" withAttributes:labelEndLineDict];
1208 [titleString appendString:@"\n" withAttributes:labelEndLineDict];
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];
1220 return [titleString autorelease];
1223 - (NSAttributedString *)_tooltipBodyForObject:(AIListObject *)object
1225 NSMutableAttributedString *tipString = [[NSMutableAttributedString alloc] init];
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];
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;
1244 BOOL firstEntry = YES;
1246 //Calculate the widest label while loading the arrays
1247 enumerator = [contactListTooltipSecondaryEntryArray objectEnumerator];
1249 while ((tooltipEntry = [enumerator nextObject])) {
1251 entryString = [[tooltipEntry entryForObject:object] mutableCopy];
1252 if (entryString && [entryString length]) {
1254 NSString *labelString = [tooltipEntry labelForObject:object];
1255 if (labelString && [labelString length]) {
1257 [entryArray addObject:entryString];
1258 [labelArray addObject:labelString];
1260 NSAttributedString * labelAttribString = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@:",labelString]
1261 attributes:labelDict];
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];
1267 if (labelWidth > maxLabelWidth)
1268 maxLabelWidth = labelWidth;
1271 [entryString release];
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];
1284 //Add a carriage return and skip a line
1285 [tipString appendString:@"\n\n" withAttributes:labelEndLineDict];
1288 //Add the label (with its spacing)
1289 [tipString appendAttributedString:labelString];
1290 [labelString release];
1292 NSRange fullLength = NSMakeRange(0, [entryString length]);
1294 //remove any background coloration
1295 [entryString removeAttribute:NSBackgroundColorAttributeName range:fullLength];
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]);
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];
1312 [entryString addAttributes:entryDict range:NSMakeRange(0,[entryString length])];
1313 [tipString appendAttributedString:entryString];
1314 [entryString release];
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.
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")];
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:);
1368 [keyWindow makeFirstResponder:responder];
1369 [responder performSelector:selector
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:)
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];
1401 NSFontPanel *fontPanel = [NSFontPanel sharedFontPanel];
1403 if (!fontPanelAccessoryView) {
1404 [NSBundle loadNibNamed:@"FontPanelAccessoryView" owner:self];
1405 [fontPanel setAccessoryView:fontPanelAccessoryView];
1408 [fontPanel orderFront:self];
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];
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];
1433 if ((backgroundColor = [typingAttributes objectForKey:AIBodyColorAttributeName])) {
1434 [[adium preferenceController] setPreference:[backgroundColor stringRepresentation]
1435 forKey:KEY_FORMATTING_BACKGROUND_COLOR
1436 group:PREF_GROUP_FORMATTING];
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];
1448 if ([fontManager traitsOfFont:[fontManager selectedFont]] & [sender tag]) {
1449 [fontManager removeFontTrait:sender];
1451 [fontManager addFontTrait:sender];
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];
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]);
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:
1490 NSFilenamesPboardType,
1491 NSFilesPromisePboardType,
1494 return ([pboard availableTypeFromArray:nonImageTypes] != nil) || [NSImage canInitWithPasteboard:pboard];
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;
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])));
1510 } else if (menuItem == menuItem_closeChat) {
1511 return activeChat != nil;
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]));
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))];
1533 #pragma mark Window levels
1534 - (NSMenu *)menuForWindowLevelsNotifyingTarget:(id)target
1536 NSMenu *windowPositionMenu = [[NSMenu allocWithZone:[NSMenu zone]] init];
1537 NSMenuItem *menuItem;
1539 menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Above other windows",nil)
1541 action:@selector(selectedWindowLevel:)
1543 [menuItem setEnabled:YES];
1544 [menuItem setTag:AIFloatingWindowLevel];
1545 [windowPositionMenu addItem:menuItem];
1548 menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Normally",nil)
1550 action:@selector(selectedWindowLevel:)
1552 [menuItem setEnabled:YES];
1553 [menuItem setTag:AINormalWindowLevel];
1554 [windowPositionMenu addItem:menuItem];
1557 menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Below other windows",nil)
1559 action:@selector(selectedWindowLevel:)
1561 [menuItem setEnabled:YES];
1562 [menuItem setTag:AIDesktopWindowLevel];
1563 [windowPositionMenu addItem:menuItem];
1566 [windowPositionMenu setAutoenablesItems:NO];
1568 return [windowPositionMenu autorelease];