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"
46 #import <KNShelfSplitview.h>
48 #define ERROR_MESSAGE_WINDOW_TITLE AILocalizedString(@"Adium : Error","Error message window title")
49 #define LABEL_ENTRY_SPACING 4.0
50 #define DISPLAY_IMAGE_ON_RIGHT NO
52 #define PREF_GROUP_FORMATTING @"Formatting"
53 #define KEY_FORMATTING_FONT @"Default Font"
55 #define MESSAGES_WINDOW_MENU_TITLE AILocalizedString(@"Chats","Title for the messages window menu item")
57 //#define LOG_RESPONDER_CHAIN
59 @interface AIInterfaceController (PRIVATE)
60 - (void)_resetOpenChatsCache;
61 - (void)_addItemToMainMenuAndDock:(NSMenuItem *)item;
62 - (NSAttributedString *)_tooltipTitleForObject:(AIListObject *)object;
63 - (NSAttributedString *)_tooltipBodyForObject:(AIListObject *)object;
64 - (void)_pasteWithPreferredSelector:(SEL)preferredSelector sender:(id)sender;
65 - (void)updateCloseMenuKeys;
68 - (void)updateActiveWindowMenuItem;
69 - (void)buildWindowMenu;
71 - (AIChat *)mostRecentActiveChat;
75 * @class AIInterfaceController
76 * @brief Interface controller
78 * Chat window related requests, such as opening and closing chats, are routed through the interface controller
79 * to the appropriate component. The interface controller keeps track of the most recently active chat, handles chat
80 * cycling (switching between chats), chat sorting, and so on. The interface controller also handles switching to
81 * an appropriate window or chat when the dock icon is clicked for a 'reopen' event.
83 * Contact list window requests, such as toggling window visibilty are routed to the contact list controller component.
85 * Error messages are routed through the interface controller.
87 * Tooltips, such as seen on hover in the contact list are generated and displayed here. Tooltip display components and
88 * plugins register with the interface controller to be queried for contact information when a tooltip is displayed.
90 * When displays in Adium flash, such as in the dock or the contact list for unviewed content, the interface controller
91 * manages keeping the flashing synchronized.
93 * Finally, the interface controller manages many menu items, providing better menu item validation and target routing
94 * than the responder chain alone would do.
96 @implementation AIInterfaceController
100 if ((self = [super init])) {
101 /* Use KFTypeSelectTableView as our NSTableView base class to allow type-select searching of all
102 * table and outline views throughout Adium.
104 [[KFTypeSelectTableView class] poseAsClass:[NSTableView class]];
106 contactListViewArray = [[NSMutableArray alloc] init];
107 messageViewArray = [[NSMutableArray alloc] init];
108 contactListTooltipEntryArray = [[NSMutableArray alloc] init];
109 contactListTooltipSecondaryEntryArray = [[NSMutableArray alloc] init];
110 closeMenuConfiguredForChat = NO;
111 _cachedOpenChats = nil;
112 mostRecentActiveChat = nil;
115 tooltipListObject = nil;
119 flashObserverArray = nil;
123 windowMenuArray = nil;
125 #ifdef LOG_RESPONDER_CHAIN
126 [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(reportResponderChain:) userInfo:nil repeats:YES];
133 #ifdef LOG_RESPONDER_CHAIN
134 //Can be called by a timer to periodically log the responder chain
135 //[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(reportResponderChain:) userInfo:nil repeats:YES];
136 - (void)reportResponderChain:(NSTimer *)inTimer
138 NSMutableString *responderChain = [NSMutableString string];
140 NSWindow *keyWindow = [[NSApplication sharedApplication] keyWindow];
141 [responderChain appendFormat:@"%@ (%i): ",keyWindow,[keyWindow respondsToSelector:@selector(print:)]];
143 NSResponder *responder = [keyWindow firstResponder];
145 //First, walk down the responder chain looking for a responder which can handle the preferred selector
147 [responderChain appendFormat:@"%@ (%i)",responder,[responder respondsToSelector:@selector(print:)]];
148 responder = [responder nextResponder];
149 if (responder) [responderChain appendString:@" -> "];
152 NSLog(responderChain);
156 - (void)controllerDidLoad
159 [interfacePlugin openInterface];
161 //Open the contact list window
162 [self showContactList:nil];
164 //Userlist show/hide item
165 menuItem_toggleUserlist = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Toggle User List", nil)
167 action:@selector(toggleUserlist:)
169 [[adium menuController] addMenuItem:menuItem_toggleUserlist toLocation:LOC_View_General];
171 //Contact list menu item
172 NSMenuItem* menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Contact List","Name of the window which lists contacts")
174 action:@selector(toggleContactList:)
176 [[adium menuController] addMenuItem:menuItem toLocation:LOC_Window_Fixed];
177 [[adium menuController] addMenuItem:[[menuItem copy] autorelease] toLocation:LOC_Dock_Status];
180 menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Close Chat","Title for the close chat menu item")
182 action:@selector(closeContextualChat:)
184 [[adium menuController] addContextualMenuItem:menuItem toLocation:Context_Tab_Action];
187 //Observe preference changes
188 [[adium preferenceController] registerPreferenceObserver:self forGroup:PREF_GROUP_INTERFACE];
190 //Observe content so we can open chats as necessary
191 [[adium notificationCenter] addObserver:self selector:@selector(didReceiveContent:)
192 name:CONTENT_MESSAGE_RECEIVED object:nil];
193 [[adium notificationCenter] addObserver:self selector:@selector(didReceiveContent:)
194 name:CONTENT_MESSAGE_RECEIVED_GROUP object:nil];
197 - (void)controllerWillClose
199 [contactListPlugin closeContactList];
200 [interfacePlugin closeInterface];
206 [contactListViewArray release]; contactListViewArray = nil;
207 [messageViewArray release]; messageViewArray = nil;
208 [interfaceArray release]; interfaceArray = nil;
210 [tooltipListObject release]; tooltipListObject = nil;
211 [tooltipTitle release]; tooltipTitle = nil;
212 [tooltipBody release]; tooltipBody = nil;
213 [tooltipImage release]; tooltipImage = nil;
215 [[adium notificationCenter] removeObserver:self];
216 [[adium preferenceController] unregisterPreferenceObserver:self];
221 //Registers code to handle the interface
222 - (void)registerInterfaceController:(id <AIInterfaceComponent>)inController
224 if (!interfacePlugin) interfacePlugin = [inController retain];
227 //Register code to handle the contact list
228 - (void)registerContactListController:(id <AIContactListComponent>)inController
230 if (!contactListPlugin) contactListPlugin = [inController retain];
233 //Preferences changed
234 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key
235 object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
238 [[adium notificationCenter] removeObserver:self name:Contact_OrderChanged object:nil];
241 tabbedChatting = [[prefDict objectForKey:KEY_TABBED_CHATTING] boolValue];
242 groupChatsByContactGroup = [[prefDict objectForKey:KEY_GROUP_CHATS_BY_GROUP] boolValue];
245 //Handle a reopen/dock icon click
246 - (BOOL)handleReopenWithVisibleWindows:(BOOL)visibleWindows
248 if (![self contactListIsVisibleAndMain] && [[interfacePlugin openContainers] count] == 0) {
249 //The contact list is not visible, and there are no chat windows. Make the contact list visible.
250 [self showContactList:nil];
253 AIChat *mostRecentUnviewedChat;
255 //If windows are open, try switching to a chat with unviewed content
256 if ((mostRecentUnviewedChat = [[adium chatController] mostRecentUnviewedChat])) {
257 if ([mostRecentActiveChat unviewedContentCount]) {
258 //If the most recently active chat has unviewed content, ensure it is in the front
259 [self setActiveChat:mostRecentActiveChat];
261 //Otherwise, switch to the chat which most recently received content
262 [self setActiveChat:mostRecentUnviewedChat];
266 NSEnumerator *enumerator;
267 NSWindow *window, *targetWindow = nil;
268 BOOL unMinimizedWindows = 0;
270 //If there was no unviewed content, ensure that atleast one of Adium's windows is unminimized
271 enumerator = [[NSApp windows] objectEnumerator];
272 while ((window = [enumerator nextObject])) {
273 //Check stylemask to rule out the system menu's window (Which reports itself as visible like a real window)
274 if (([window styleMask] & (NSTitledWindowMask | NSClosableWindowMask | NSMiniaturizableWindowMask))) {
275 if (!targetWindow) targetWindow = window;
276 if (![window isMiniaturized]) unMinimizedWindows++;
280 //If there are no unminimized windows, unminimize the last one
281 if (unMinimizedWindows == 0 && targetWindow) {
282 [targetWindow deminiaturize:nil];
290 //Contact List ---------------------------------------------------------------------------------------------------------
291 #pragma mark Contact list
293 * @brief Toggles contact list between visible and hiden
295 - (IBAction)toggleContactList:(id)sender
297 if ([self contactListIsVisibleAndMain]) {
298 [self closeContactList:nil];
300 [[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
301 [self showContactList:nil];
306 * @brief Brings contact list to the front
308 - (IBAction)showContactList:(id)sender
310 [contactListPlugin showContactListAndBringToFront:YES];
314 * @brief Show the contact list window and bring Adium to the front
316 - (IBAction)showContactListAndBringToFront:(id)sender
318 [contactListPlugin showContactListAndBringToFront:YES];
319 [[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
323 * @brief Close the contact list window
325 - (IBAction)closeContactList:(id)sender
327 [contactListPlugin closeContactList];
331 * @returns YES if contact list is visible and selected, otherwise NO
333 - (BOOL)contactListIsVisibleAndMain
335 return [contactListPlugin contactListIsVisibleAndMain];
339 * @returns YES if contact list is visible, otherwise NO
341 - (BOOL)contactListIsVisible
343 return [contactListPlugin contactListIsVisible];
346 //Detachable Contact List ----------------------------------------------------------------------------------------------
347 #pragma mark Detachable Contact List
350 * @returns YES if detachable groups are allowed, otherwise NO
352 - (BOOL)allowDetachableGroups {
353 return [contactListPlugin allowDetachableContactList];
357 * @returns Created contact list controller for detached contact list
359 - (AIListWindowController *)detachContactList:(AIListGroup *)aContactList {
360 return [contactListPlugin detachContactList:aContactList];
364 //Messaging ------------------------------------------------------------------------------------------------------------
365 //Methods for instructing the interface to provide a representation of chats, and to determine which chat has user focus
366 #pragma mark Messaging
369 * @brief Opens window for chat
371 - (void)openChat:(AIChat *)inChat
373 NSArray *containers = [interfacePlugin openContainersAndChats];
374 NSString *containerID = nil;
375 NSString *containerName = nil;
377 //Determine the correct container for this chat
379 if (!tabbedChatting) {
380 //We're not using tabs; each chat starts in its own container, based on the destination object or the chat name
381 if ([inChat listObject]) {
382 containerID = [[inChat listObject] internalObjectID];
384 containerID = [inChat name];
387 } else if (groupChatsByContactGroup) {
388 if ([inChat isGroupChat]) {
389 containerID = AILocalizedString(@"Group Chat",nil);
392 AIListObject *group = [[[inChat listObject] parentContact] containingObject];
394 //If the contact is in the contact list root, we don't have a group
395 if (group && (group != [[adium contactController] contactList])) {
396 containerID = [group displayName];
400 containerName = containerID;
404 //Open new chats into the first container (if not available, create a new one)
405 if ([containers count] > 0) {
406 containerID = [[containers objectAtIndex:0] objectForKey:@"ID"];
412 //Determine the correct placement for this chat within the container
413 [interfacePlugin openChat:inChat inContainerWithID:containerID withName:containerName atIndex:-1];
414 if (![inChat isOpen]) {
415 [inChat setIsOpen:YES];
417 //Post the notification last, so observers receive a chat whose isOpen flag is yes.
418 [[adium notificationCenter] postNotificationName:Chat_DidOpen object:inChat userInfo:nil];
422 - (id)openChat:(AIChat *)inChat inContainerWithID:(NSString *)containerID atIndex:(int)index
424 NSArray *containers = [interfacePlugin openContainersAndChats];
427 //Open new chats into the first container (if not available, create a new one)
428 if ([containers count] > 0) {
429 containerID = [[containers objectAtIndex:0] objectForKey:@"ID"];
431 containerID = AILocalizedString(@"Chat",nil);
435 //Determine the correct placement for this chat within the container
436 id tabViewItem = [interfacePlugin openChat:inChat inContainerWithID:containerID withName:nil atIndex:index];
437 if (![inChat isOpen]) {
438 [inChat setIsOpen:YES];
440 //Post the notification last, so observers receive a chat whose isOpen flag is yes.
441 [[adium notificationCenter] postNotificationName:Chat_DidOpen object:inChat userInfo:nil];
447 * @brief Opens a container with a specific ID
449 * Asks the interfacePlugin to openContainerWithID:
451 - (id)openContainerWithID:(NSString *)containerID name:(NSString *)containerName
453 return [interfacePlugin openContainerWithID:containerID name:containerName];
457 * @brief Close the interface for a chat
459 * Tell the interface plugin to close the chat.
461 - (void)closeChat:(AIChat *)inChat
464 if ([[adium chatController] closeChat:inChat]) {
465 [interfacePlugin closeChat:inChat];
471 * @brief Consolidate chats into a single container
473 - (void)consolidateChats
475 //We work with copies of these arrays, since moving chats may change their contents
476 NSArray *openContainers = [[interfacePlugin openContainers] copy];
477 NSEnumerator *containerEnumerator = [openContainers objectEnumerator];
478 NSString *firstContainerID = [containerEnumerator nextObject];
479 NSString *containerID;
481 //For all containers but the first, move the chats they contain to the first container
482 while ((containerID = [containerEnumerator nextObject])) {
483 NSArray *openChats = [[interfacePlugin openChatsInContainerWithID:containerID] copy];
484 NSEnumerator *chatEnumerator = [openChats objectEnumerator];
487 //Move all the chats, providing a target index if chat sorting is enabled
488 while ((chat = [chatEnumerator nextObject])) {
489 [interfacePlugin moveChat:chat
490 toContainerWithID:firstContainerID
497 [self chatOrderDidChange];
499 [openContainers release];
503 * @returns Active chat
505 - (AIChat *)activeChat
511 * @brief Set the active chat window
513 - (void)setActiveChat:(AIChat *)inChat
515 [interfacePlugin setActiveChat:inChat];
519 * @returns Last chat to be active, nil if not chat is open
521 - (AIChat *)mostRecentActiveChat
523 return mostRecentActiveChat;
527 * @brief Sets active chat window based on chat
529 - (void)setMostRecentActiveChat:(AIChat *)inChat
531 [self setActiveChat:inChat];
535 * @returns Array of open chats (cached, so call as frequently as desired)
537 - (NSArray *)openChats
539 if (!_cachedOpenChats) {
540 _cachedOpenChats = [[interfacePlugin openChats] retain];
543 return _cachedOpenChats;
547 * @param containerID ID for chat window
549 * @returns Array of all chats in chat window
551 - (NSArray *)openChatsInContainerWithID:(NSString *)containerID
553 return [interfacePlugin openChatsInContainerWithID:containerID];
557 * @brief Resets the cache of open chats
559 - (void)_resetOpenChatsCache
561 [_cachedOpenChats release]; _cachedOpenChats = nil;
566 //Interface plugin callbacks -------------------------------------------------------------------------------------------
567 //These methods are called by the interface to let us know what's going on. We're informed of chats opening, closing,
568 //changing order, etc.
569 #pragma mark Interface plugin callbacks
571 * @breif A chat window did open: rebuild our window menu to show the new chat
573 * @param inChat Newly created chat
575 - (void)chatDidOpen:(AIChat *)inChat
577 [self _resetOpenChatsCache];
578 [self buildWindowMenu];
582 * @brief A chat has become active: update our chat closing keys and flag this chat as selected in the window menu
584 * @param inChat Chat which has become active
586 - (void)chatDidBecomeActive:(AIChat *)inChat
588 AIChat *previouslyActiveChat = activeChat;
590 activeChat = [inChat retain];
592 [self updateCloseMenuKeys];
593 [self updateActiveWindowMenuItem];
595 if (inChat && (inChat != mostRecentActiveChat)) {
596 [mostRecentActiveChat release]; mostRecentActiveChat = nil;
597 mostRecentActiveChat = [inChat retain];
600 [[adium notificationCenter] postNotificationName:Chat_BecameActive
602 userInfo:(previouslyActiveChat ?
603 [NSDictionary dictionaryWithObject:previouslyActiveChat
604 forKey:@"PreviouslyActiveChat"] :
608 /* Clear the unviewed content on the next event loop so other methods have a chance to react to the chat becoming
609 * active. Specifically, this lets the handleReopenWithVisibleWindows: method have a chance to know that this chat
610 * had unviewed content.
612 [inChat performSelector:@selector(clearUnviewedContentCount)
617 [previouslyActiveChat release];
621 * @brief A chat has become visible: send out a notification for components and plugins to take action
623 * @param inChat Chat that has become active
624 * @param nWindow Containing chat window
626 - (void)chatDidBecomeVisible:(AIChat *)inChat inWindow:(NSWindow *)inWindow
628 [[adium notificationCenter] postNotificationName:@"AIChatDidBecomeVisible"
630 userInfo:[NSDictionary dictionaryWithObject:inWindow
631 forKey:@"NSWindow"]];
635 * @brief Find the window currently displaying a chat
637 * @returns Window for chat otherwise if the chat is not in any window, or is not visible in any window, returns nil
639 - (NSWindow *)windowForChat:(AIChat *)inChat
641 return [interfacePlugin windowForChat:inChat];
645 * @brief Find the chat active in a window
647 * If the window does not have an active chat, nil is returned
649 - (AIChat *)activeChatInWindow:(NSWindow *)window
651 return [interfacePlugin activeChatInWindow:window];
655 * @brief A chat window did close: rebuild our window menu to remove the chat
657 * @param inChat Chat that closed
659 - (void)chatDidClose:(AIChat *)inChat
661 [self _resetOpenChatsCache];
662 [inChat clearUnviewedContentCount];
663 [self buildWindowMenu];
665 if (inChat == activeChat) {
666 [activeChat release]; activeChat = nil;
669 if (inChat == mostRecentActiveChat) {
670 [mostRecentActiveChat release]; mostRecentActiveChat = nil;
675 * @brief The order of chats has changed: rebuild our window menu to reflect the new order
677 - (void)chatOrderDidChange
679 [self _resetOpenChatsCache];
680 [self buildWindowMenu];
681 [[adium notificationCenter] postNotificationName:Chat_OrderDidChange object:nil userInfo:nil];
685 #pragma mark Unviewed content
688 * @breif Content was received, increase the unviewed content count of the chat (if it's not currently active)
690 - (void)didReceiveContent:(NSNotification *)notification
692 AIChat *chat = [[notification userInfo] objectForKey:@"AIChat"];
694 if (chat != activeChat) {
695 [chat incrementUnviewedContentCount];
700 //Chat close menus -----------------------------------------------------------------------------------------------------
701 #pragma mark Chat close menus
704 * @brief Closes currently active window
706 - (IBAction)closeMenu:(id)sender
708 [[[NSApplication sharedApplication] keyWindow] performClose:nil];
712 * @brief Closes currently active chat (if there is an active chat)
714 - (IBAction)closeChatMenu:(id)sender
716 if (activeChat) [self closeChat:activeChat];
720 * @brief Closes currently selected chat based on current chat contextual menu
722 - (IBAction)closeContextualChat:(id)sender
724 [self closeChat:[[adium menuController] currentContextMenuChat]];
728 * @brief Loop through open chats and close them
730 - (IBAction)closeAllChats:(id)sender
732 NSEnumerator *containerEnumerator = [[[[interfacePlugin openChats] copy] autorelease] objectEnumerator];
735 while ((chatToClose = [containerEnumerator nextObject])) {
736 [self closeChat:chatToClose];
741 * @brief Updates the key equivalents on 'close' and 'close chat' (dynamically changed to make cmd-w less destructive)
743 - (void)updateCloseMenuKeys
745 if (activeChat && !closeMenuConfiguredForChat) {
746 [menuItem_close setKeyEquivalent:@"W"];
747 [menuItem_closeChat setKeyEquivalent:@"w"];
748 closeMenuConfiguredForChat = YES;
749 } else if (!activeChat && closeMenuConfiguredForChat) {
750 [menuItem_close setKeyEquivalent:@"w"];
751 [menuItem_closeChat removeKeyEquivalent];
752 closeMenuConfiguredForChat = NO;
757 //Window Menu ----------------------------------------------------------------------------------------------------------
758 #pragma mark Window Menu
761 * @brief Make a chat window active
763 * Invoked by a selection in the window menu
765 - (IBAction)showChatWindow:(id)sender
767 [self setActiveChat:[sender representedObject]];
768 [[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
772 * @brief Updates the 'check' icon so it's next to the active window
774 - (void)updateActiveWindowMenuItem
776 NSEnumerator *enumerator = [windowMenuArray objectEnumerator];
779 while ((item = [enumerator nextObject])) {
780 if ([item representedObject]) [item setState:([item representedObject] == activeChat ? NSOnState : NSOffState)];
785 * @brief Builds the window menu
787 * This function gets called whenever chats are opened, closed, or re-ordered - so improvements and optimizations here
788 * would probably be helpful
790 - (void)buildWindowMenu
793 NSEnumerator *enumerator;
796 //Remove any existing menus
797 enumerator = [windowMenuArray objectEnumerator];
798 while ((item = [enumerator nextObject])) {
799 [[adium menuController] removeMenuItem:item];
801 [windowMenuArray release]; windowMenuArray = [[NSMutableArray alloc] init];
803 //Messages window and any open messasges
804 NSEnumerator *containerEnumerator = [[interfacePlugin openContainersAndChats] objectEnumerator];
805 NSDictionary *containerDict;
807 while ((containerDict = [containerEnumerator nextObject])) {
808 NSString *containerName = [containerDict objectForKey:@"Name"];
809 NSArray *contentArray = [containerDict objectForKey:@"Content"];
810 NSEnumerator *contentEnumerator = [contentArray objectEnumerator];
813 //Add a menu item for the container
814 if ([contentArray count] > 1) {
815 item = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:containerName
819 [self _addItemToMainMenuAndDock:item];
823 //Add items for the chats it contains
824 while ((chat = [contentEnumerator nextObject])) {
825 NSString *windowKeyString;
827 //Prepare a key equivalent for the controller
828 if (windowKey < 10) {
829 windowKeyString = [NSString stringWithFormat:@"%i",(windowKey)];
830 } else if (windowKey == 10) {
831 windowKeyString = [NSString stringWithString:@"0"];
833 windowKeyString = [NSString stringWithString:@""];
836 item = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:[chat displayName]
838 action:@selector(showChatWindow:)
839 keyEquivalent:windowKeyString];
840 if ([contentArray count] > 1) [item setIndentationLevel:1];
841 [item setRepresentedObject:chat];
842 [item setImage:[chat chatMenuImage]];
843 [self _addItemToMainMenuAndDock:item];
850 [self updateActiveWindowMenuItem];
854 * brief Adds a menu item to the internal array, dock menu, and main menu
856 * Should be used for adding a new window to the window menu (and dock menu)
858 - (void)_addItemToMainMenuAndDock:(NSMenuItem *)item
860 //Add to main menu first
861 [[adium menuController] addMenuItem:item toLocation:LOC_Window_Fixed];
862 [windowMenuArray addObject:item];
864 //Make a copy, and add to the dock
866 [item setKeyEquivalent:@""];
867 [[adium menuController] addMenuItem:item toLocation:LOC_Dock_Status];
868 [windowMenuArray addObject:item];
873 //Chat Cycling ---------------------------------------------------------------------------------------------------------
874 #pragma mark Chat Cycling
877 * @brief Cycles to the next active chat
879 - (void)nextChat:(id)sender
881 NSArray *openChats = [self openChats];
883 if ([openChats count]) {
885 int chatIndex = [openChats indexOfObject:activeChat]+1;
886 [self setActiveChat:[openChats objectAtIndex:(chatIndex < [openChats count] ? chatIndex : 0)]];
888 [self setActiveChat:[openChats objectAtIndex:0]];
894 * @brief Cycles to the previus active chat
896 - (void)previousChat:(id)sender
898 NSArray *openChats = [self openChats];
900 if ([openChats count]) {
902 int chatIndex = [openChats indexOfObject:activeChat]-1;
903 [self setActiveChat:[openChats objectAtIndex:(chatIndex >= 0 ? chatIndex : [openChats count]-1)]];
905 [self setActiveChat:[openChats lastObject]];
910 //Selected contact ------------------------------------------------
911 #pragma mark Selected contact
912 - (id)_performSelectorOnFirstAvailableResponder:(SEL)selector
914 NSResponder *responder = [[[NSApplication sharedApplication] mainWindow] firstResponder];
915 //Check the first responder
916 if ([responder respondsToSelector:selector]) {
917 return [responder performSelector:selector];
920 //Search the responder chain
922 responder = [responder nextResponder];
923 if ([responder respondsToSelector:selector]) {
924 return [responder performSelector:selector];
927 } while (responder != nil);
929 //None found, return nil
932 - (id)_performSelectorOnFirstAvailableResponder:(SEL)selector conformingToProtocol:(Protocol *)protocol
934 NSResponder *responder = [[[NSApplication sharedApplication] mainWindow] firstResponder];
935 //Check the first responder
936 if ([responder conformsToProtocol:protocol] && [responder respondsToSelector:selector]) {
937 return [responder performSelector:selector];
940 //Search the responder chain
942 responder = [responder nextResponder];
943 if ([responder conformsToProtocol:protocol] && [responder respondsToSelector:selector]) {
944 return [responder performSelector:selector];
947 } while (responder != nil);
949 //None found, return nil
954 * @returns The "selected"(represented) contact (By finding the first responder that returns a contact)
955 * If no listObject is found, try to find a list object selected in a group chat
957 - (AIListObject *)selectedListObject
959 AIListObject *listObject = [self _performSelectorOnFirstAvailableResponder:@selector(listObject)];
961 listObject = [self _performSelectorOnFirstAvailableResponder:@selector(preferredListObject)];
966 - (AIListObject *)selectedListObjectInContactList
968 return [self _performSelectorOnFirstAvailableResponder:@selector(listObject) conformingToProtocol:@protocol(ContactListOutlineView)];
970 - (NSArray *)arrayOfSelectedListObjectsInContactList
972 return [self _performSelectorOnFirstAvailableResponder:@selector(arrayOfListObjects) conformingToProtocol:@protocol(ContactListOutlineView)];
975 //Message View ---------------------------------------------------------------------------------------------------------
976 //Message view is abstracted from the containing interface, since they're not directly related to eachother
977 #pragma mark Message View
978 //Registers a view to handle the contact list
979 - (void)registerMessageDisplayPlugin:(id <AIMessageDisplayPlugin>)inPlugin
981 [messageViewArray addObject:inPlugin];
983 - (id <AIMessageDisplayController>)messageDisplayControllerForChat:(AIChat *)inChat
985 //Sometimes our users find it amusing to disable plugins that are located within the Adium bundle. This error
986 //trap prevents us from crashing if they happen to disable all the available message view plugins.
987 //PUT THAT PLUGIN BACK IT WAS IMPORTANT!
988 if ([messageViewArray count] == 0) {
989 NSRunCriticalAlertPanel(@"No Message View Plugin Installed",
990 @"Adium cannot find its message view plugin. Please re-install. If you've manually disabled Adium's message view plugin, please re-enable it.",
994 [NSApp terminate:nil];
997 return [[messageViewArray objectAtIndex:0] messageDisplayControllerForChat:inChat];
1001 //Error Display --------------------------------------------------------------------------------------------------------
1002 #pragma mark Error Display
1003 - (void)handleErrorMessage:(NSString *)inTitle withDescription:(NSString *)inDesc
1005 [self handleMessage:inTitle withDescription:inDesc withWindowTitle:ERROR_MESSAGE_WINDOW_TITLE];
1008 - (void)handleMessage:(NSString *)inTitle withDescription:(NSString *)inDesc withWindowTitle:(NSString *)inWindowTitle;
1010 NSDictionary *errorDict;
1012 //Post a notification that an error was recieved
1013 errorDict = [NSDictionary dictionaryWithObjectsAndKeys:inTitle,@"Title",inDesc,@"Description",inWindowTitle,@"Window Title",nil];
1014 [[adium notificationCenter] postNotificationName:Interface_ShouldDisplayErrorMessage object:nil userInfo:errorDict];
1017 //Display then clear the last disconnection error
1018 - (void)account:(AIAccount *)inAccount disconnectedWithError:(NSString *)disconnectionError
1020 // [AdiumDisconnectionErrorController account:inAccount disconnectedWithError:disconnectionError];
1023 //Question Display -----------------------------------------------------------------------------------------------------
1024 #pragma mark Question Display
1025 - (void)displayQuestion:(NSString *)inTitle withAttributedDescription:(NSAttributedString *)inDesc withWindowTitle:(NSString *)inWindowTitle
1026 defaultButton:(NSString *)inDefaultButton alternateButton:(NSString *)inAlternateButton otherButton:(NSString *)inOtherButton
1027 target:(id)inTarget selector:(SEL)inSelector userInfo:(id)inUserInfo
1029 NSMutableDictionary *questionDict = [NSMutableDictionary dictionary];
1032 [questionDict setObject:inTitle forKey:@"Title"];
1034 [questionDict setObject:inDesc forKey:@"Description"];
1035 if(inWindowTitle != nil)
1036 [questionDict setObject:inWindowTitle forKey:@"Window Title"];
1037 if(inDefaultButton != nil)
1038 [questionDict setObject:inDefaultButton forKey:@"Default Button"];
1039 if(inAlternateButton != nil)
1040 [questionDict setObject:inAlternateButton forKey:@"Alternate Button"];
1041 if(inOtherButton != nil)
1042 [questionDict setObject:inOtherButton forKey:@"Other Button"];
1044 [questionDict setObject:inTarget forKey:@"Target"];
1045 if(inSelector != NULL)
1046 [questionDict setObject:NSStringFromSelector(inSelector) forKey:@"Selector"];
1047 if(inUserInfo != nil)
1048 [questionDict setObject:inUserInfo forKey:@"Userinfo"];
1050 [[adium notificationCenter] postNotificationName:Interface_ShouldDisplayQuestion object:nil userInfo:questionDict];
1053 - (void)displayQuestion:(NSString *)inTitle withDescription:(NSString *)inDesc withWindowTitle:(NSString *)inWindowTitle
1054 defaultButton:(NSString *)inDefaultButton alternateButton:(NSString *)inAlternateButton otherButton:(NSString *)inOtherButton
1055 target:(id)inTarget selector:(SEL)inSelector userInfo:(id)inUserInfo
1057 [self displayQuestion:inTitle
1058 withAttributedDescription:[[[NSAttributedString alloc] initWithString:inDesc
1059 attributes:[NSDictionary dictionaryWithObject:[NSFont systemFontOfSize:0]
1060 forKey:NSFontAttributeName]] autorelease]
1061 withWindowTitle:inWindowTitle
1062 defaultButton:inDefaultButton
1063 alternateButton:inAlternateButton
1064 otherButton:inOtherButton
1067 userInfo:inUserInfo];
1069 //Synchronized Flashing ------------------------------------------------------------------------------------------------
1070 #pragma mark Synchronized Flashing
1071 //Register to observe the synchronized flashing
1072 - (void)registerFlashObserver:(id <AIFlashObserver>)inObserver
1074 //Setup the timer if we don't have one yet
1075 if (!flashObserverArray) {
1076 flashObserverArray = [[NSMutableArray alloc] init];
1077 flashTimer = [[NSTimer scheduledTimerWithTimeInterval:(1.0/2.0)
1079 selector:@selector(flashTimer:)
1081 repeats:YES] retain];
1084 //Add the new observer to the array
1085 [flashObserverArray addObject:inObserver];
1088 //Unregister from observing flashing
1089 - (void)unregisterFlashObserver:(id <AIFlashObserver>)inObserver
1091 //Remove the observer from our array
1092 [flashObserverArray removeObject:inObserver];
1094 //Release the observer array and uninstall the timer
1095 if ([flashObserverArray count] == 0) {
1096 [flashObserverArray release]; flashObserverArray = nil;
1097 [flashTimer invalidate];
1098 [flashTimer release]; flashTimer = nil;
1102 //Timer, invoke a flash
1103 - (void)flashTimer:(NSTimer *)inTimer
1105 NSEnumerator *enumerator;
1106 id<AIFlashObserver> observer;
1110 enumerator = [flashObserverArray objectEnumerator];
1111 while ((observer = [enumerator nextObject])) {
1112 [observer flash:flashState];
1116 //Current state of flashing. This is an integer the increases by 1 with every flash. Mod to whatever range is desired
1123 //Tooltips -------------------------------------------------------------------------------------------------------------
1124 #pragma mark Tooltips
1125 //Registers code to display tooltip info about a contact
1126 - (void)registerContactListTooltipEntry:(id <AIContactListTooltipEntry>)inEntry secondaryEntry:(BOOL)isSecondary
1129 [contactListTooltipSecondaryEntryArray addObject:inEntry];
1131 [contactListTooltipEntryArray addObject:inEntry];
1134 //Unregisters code to display tooltip info about a contact
1135 - (void)unregisterContactListTooltipEntry:(id <AIContactListTooltipEntry>)inEntry secondaryEntry:(BOOL)isSecondary
1138 [contactListTooltipSecondaryEntryArray removeObject:inEntry];
1140 [contactListTooltipEntryArray removeObject:inEntry];
1143 //list object tooltips
1144 - (void)showTooltipForListObject:(AIListObject *)object atScreenPoint:(NSPoint)point onWindow:(NSWindow *)inWindow
1147 if (object == tooltipListObject) { //If we already have this tooltip open
1148 //Move the existing tooltip
1149 [AITooltipUtilities showTooltipWithTitle:tooltipTitle
1152 imageOnRight:DISPLAY_IMAGE_ON_RIGHT
1155 orientation:TooltipBelow];
1157 } else { //This is a new tooltip
1159 NSMutableParagraphStyle *paragraphStyleTitle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
1160 NSMutableParagraphStyle *paragraphStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
1162 //Hold onto the new object
1163 [tooltipListObject release]; tooltipListObject = [object retain];
1166 [tooltipImage release];
1167 tooltipImage = [[tooltipListObject userIcon] retain];
1168 if (!tooltipImage) tooltipImage = [[AIServiceIcons serviceIconForObject:tooltipListObject
1169 type:AIServiceIconLarge
1170 direction:AIIconNormal] retain];
1172 //Reset the maxLabelWidth for the tooltip generation
1175 //Build a tooltip string for the primary information
1176 [tooltipTitle release]; tooltipTitle = [[self _tooltipTitleForObject:object] retain];
1178 //If there is an image, set the title tab and indentation settings independently
1180 //Set a right-align tab at the maximum label width and a left-align just past it
1181 tabArray = [[NSArray alloc] initWithObjects:[[[NSTextTab alloc] initWithType:NSRightTabStopType
1182 location:maxLabelWidth] autorelease]
1183 ,[[[NSTextTab alloc] initWithType:NSLeftTabStopType
1184 location:maxLabelWidth + LABEL_ENTRY_SPACING] autorelease]
1187 [paragraphStyleTitle setTabStops:tabArray];
1190 [paragraphStyleTitle setHeadIndent:(maxLabelWidth + LABEL_ENTRY_SPACING)];
1192 [tooltipTitle addAttribute:NSParagraphStyleAttributeName
1193 value:paragraphStyleTitle
1194 range:NSMakeRange(0,[tooltipTitle length])];
1196 //Reset the max label width since the body will be independent
1200 //Build a tooltip string for the secondary information
1201 [tooltipBody release]; tooltipBody = nil;
1202 tooltipBody = [[self _tooltipBodyForObject:object] retain];
1204 //Set a right-align tab at the maximum label width for the body and a left-align just past it
1205 tabArray = [[NSArray alloc] initWithObjects:[[[NSTextTab alloc] initWithType:NSRightTabStopType
1206 location:maxLabelWidth] autorelease]
1207 ,[[[NSTextTab alloc] initWithType:NSLeftTabStopType
1208 location:maxLabelWidth + LABEL_ENTRY_SPACING] autorelease]
1210 [paragraphStyle setTabStops:tabArray];
1212 [paragraphStyle setHeadIndent:(maxLabelWidth + LABEL_ENTRY_SPACING)];
1214 [tooltipBody addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:NSMakeRange(0,[tooltipBody length])];
1215 //If there is no image, also use these settings for the top part
1216 if (!tooltipImage) {
1217 [tooltipTitle addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:NSMakeRange(0,[tooltipTitle length])];
1220 //Display the new tooltip
1221 [AITooltipUtilities showTooltipWithTitle:tooltipTitle
1224 imageOnRight:DISPLAY_IMAGE_ON_RIGHT
1227 orientation:TooltipBelow];
1229 [paragraphStyleTitle release];
1230 [paragraphStyle release];
1234 //Hide the existing tooltip
1235 if (tooltipListObject) {
1236 [AITooltipUtilities showTooltipWithTitle:nil
1241 orientation:TooltipBelow];
1242 [tooltipListObject release]; tooltipListObject = nil;
1244 [tooltipTitle release]; tooltipTitle = nil;
1245 [tooltipBody release]; tooltipBody = nil;
1246 [tooltipImage release]; tooltipImage = nil;
1251 - (NSAttributedString *)_tooltipTitleForObject:(AIListObject *)object
1253 NSMutableAttributedString *titleString = [[NSMutableAttributedString alloc] init];
1255 id <AIContactListTooltipEntry> tooltipEntry;
1256 NSEnumerator *enumerator;
1257 NSEnumerator *labelEnumerator;
1258 NSMutableArray *labelArray = [NSMutableArray array];
1259 NSMutableArray *entryArray = [NSMutableArray array];
1260 NSMutableAttributedString *entryString;
1264 NSString *formattedUID = [object formattedUID];
1266 //Configure fonts and attributes
1267 NSFontManager *fontManager = [NSFontManager sharedFontManager];
1268 NSFont *toolTipsFont = [NSFont toolTipsFontOfSize:10];
1269 NSMutableDictionary *titleDict = [NSMutableDictionary dictionaryWithObject:[fontManager convertFont:[NSFont toolTipsFontOfSize:12] toHaveTrait:NSBoldFontMask]
1270 forKey:NSFontAttributeName];
1271 NSMutableDictionary *labelDict = [NSMutableDictionary dictionaryWithObject:[fontManager convertFont:[NSFont toolTipsFontOfSize:9] toHaveTrait:NSBoldFontMask]
1272 forKey:NSFontAttributeName];
1273 NSMutableDictionary *labelEndLineDict = [NSMutableDictionary dictionaryWithObject:[NSFont toolTipsFontOfSize:2]
1274 forKey:NSFontAttributeName];
1275 NSMutableDictionary *entryDict = [NSMutableDictionary dictionaryWithObject:toolTipsFont
1276 forKey:NSFontAttributeName];
1278 //Get the user's display name as an attributed string
1279 NSAttributedString *displayName = [[NSAttributedString alloc] initWithString:[object displayName]
1280 attributes:titleDict];
1281 NSAttributedString *filteredDisplayName = [[adium contentController] filterAttributedString:displayName
1282 usingFilterType:AIFilterTooltips
1283 direction:AIFilterIncoming
1286 //Append the user's display name
1287 if (filteredDisplayName) {
1288 [titleString appendAttributedString:filteredDisplayName];
1291 //Append the user's formatted UID if there is one that's different to the display name
1292 if (formattedUID && (!([[[displayName string] compactedString] isEqualToString:[formattedUID compactedString]]))) {
1293 [titleString appendString:[NSString stringWithFormat:@" (%@)", formattedUID] withAttributes:titleDict];
1295 [displayName release];
1297 if ([object isKindOfClass:[AIListContact class]]) {
1298 if ((![object isKindOfClass:[AIMetaContact class]] || [(AIMetaContact *)object containsOnlyOneService]) &&
1299 [object userIcon]) {
1300 NSImage *serviceIcon = [[AIServiceIcons serviceIconForObject:object type:AIServiceIconSmall direction:AIIconNormal]
1301 imageByScalingToSize:NSMakeSize(14,14)];
1303 NSTextAttachment *attachment;
1304 NSTextAttachmentCell *cell;
1306 cell = [[NSTextAttachmentCell alloc] init];
1307 [cell setImage:serviceIcon];
1309 attachment = [[NSTextAttachment alloc] init];
1310 [attachment setAttachmentCell:cell];
1313 [titleString appendString:@" " withAttributes:nil];
1314 [titleString appendAttributedString:[NSAttributedString attributedStringWithAttachment:attachment]];
1315 [attachment release];
1320 if ([object isKindOfClass:[AIListGroup class]]) {
1321 [titleString appendString:[NSString stringWithFormat:@" (%i/%i)",[(AIListGroup *)object visibleCount],[(AIListGroup *)object containedObjectsCount]]
1322 withAttributes:titleDict];
1325 //Entries from plugins
1327 //Calculate the widest label while loading the arrays
1328 enumerator = [contactListTooltipEntryArray objectEnumerator];
1330 while ((tooltipEntry = [enumerator nextObject])) {
1332 entryString = [[tooltipEntry entryForObject:object] mutableCopy];
1333 if (entryString && [entryString length]) {
1335 NSString *labelString = [tooltipEntry labelForObject:object];
1336 if (labelString && [labelString length]) {
1338 [entryArray addObject:entryString];
1339 [labelArray addObject:labelString];
1341 NSAttributedString * labelAttribString = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@:",labelString]
1342 attributes:labelDict];
1344 //The largest size should be the label's size plus the distance to the next tab at least a space past its end
1345 labelWidth = [labelAttribString size].width;
1346 [labelAttribString release];
1348 if (labelWidth > maxLabelWidth)
1349 maxLabelWidth = labelWidth;
1352 [entryString release];
1355 //Add labels plus entires to the toolTip
1356 enumerator = [entryArray objectEnumerator];
1357 labelEnumerator = [labelArray objectEnumerator];
1359 while ((entryString = [enumerator nextObject])) {
1360 NSAttributedString * labelAttribString = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"\t%@:\t",[labelEnumerator nextObject]]
1361 attributes:labelDict];
1363 //Add a carriage return
1364 [titleString appendString:@"\n" withAttributes:labelEndLineDict];
1368 [titleString appendString:@"\n" withAttributes:labelEndLineDict];
1372 //Add the label (with its spacing)
1373 [titleString appendAttributedString:labelAttribString];
1374 [labelAttribString release];
1376 [entryString addAttributes:entryDict range:NSMakeRange(0,[entryString length])];
1377 [titleString appendAttributedString:entryString];
1380 return [titleString autorelease];
1383 - (NSAttributedString *)_tooltipBodyForObject:(AIListObject *)object
1385 NSMutableAttributedString *tipString = [[NSMutableAttributedString alloc] init];
1387 //Configure fonts and attributes
1388 NSFontManager *fontManager = [NSFontManager sharedFontManager];
1389 NSFont *toolTipsFont = [NSFont toolTipsFontOfSize:10];
1390 NSMutableDictionary *labelDict = [NSMutableDictionary dictionaryWithObject:[fontManager convertFont:[NSFont toolTipsFontOfSize:9] toHaveTrait:NSBoldFontMask]
1391 forKey:NSFontAttributeName];
1392 NSMutableDictionary *labelEndLineDict = [NSMutableDictionary dictionaryWithObject:[NSFont toolTipsFontOfSize:1]
1393 forKey:NSFontAttributeName];
1394 NSMutableDictionary *entryDict = [NSMutableDictionary dictionaryWithObject:toolTipsFont
1395 forKey:NSFontAttributeName];
1397 //Entries from plugins
1398 id <AIContactListTooltipEntry> tooltipEntry;
1399 NSEnumerator *enumerator;
1400 NSEnumerator *labelEnumerator;
1401 NSMutableArray *labelArray = [NSMutableArray array];
1402 NSMutableArray *entryArray = [NSMutableArray array];
1403 NSMutableAttributedString *entryString;
1405 BOOL firstEntry = YES;
1407 //Calculate the widest label while loading the arrays
1408 enumerator = [contactListTooltipSecondaryEntryArray objectEnumerator];
1410 while ((tooltipEntry = [enumerator nextObject])) {
1412 entryString = [[tooltipEntry entryForObject:object] mutableCopy];
1413 if (entryString && [entryString length]) {
1415 NSString *labelString = [tooltipEntry labelForObject:object];
1416 if (labelString && [labelString length]) {
1418 [entryArray addObject:entryString];
1419 [labelArray addObject:labelString];
1421 NSAttributedString * labelAttribString = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@:",labelString]
1422 attributes:labelDict];
1424 //The largest size should be the label's size plus the distance to the next tab at least a space past its end
1425 labelWidth = [labelAttribString size].width;
1426 [labelAttribString release];
1428 if (labelWidth > maxLabelWidth)
1429 maxLabelWidth = labelWidth;
1432 [entryString release];
1435 //Add labels plus entires to the toolTip
1436 enumerator = [entryArray objectEnumerator];
1437 labelEnumerator = [labelArray objectEnumerator];
1438 while ((entryString = [enumerator nextObject])) {
1439 NSMutableAttributedString *labelString = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"\t%@:\t",[labelEnumerator nextObject]]
1440 attributes:labelDict];
1445 //Add a carriage return and skip a line
1446 [tipString appendString:@"\n\n" withAttributes:labelEndLineDict];
1449 //Add the label (with its spacing)
1450 [tipString appendAttributedString:labelString];
1451 [labelString release];
1453 NSRange fullLength = NSMakeRange(0, [entryString length]);
1455 //remove any background coloration
1456 [entryString removeAttribute:NSBackgroundColorAttributeName range:fullLength];
1458 //adjust foreground colors for the tooltip background
1459 [entryString adjustColorsToShowOnBackground:[NSColor colorWithCalibratedRed:1.000 green:1.000 blue:0.800 alpha:1.0]];
1461 //headIndent doesn't apply to the first line of a paragraph... so when new lines are in the entry, we need to tab over to the proper location
1462 if ([entryString replaceOccurrencesOfString:@"\r" withString:@"\r\t\t" options:NSLiteralSearch range:fullLength])
1463 fullLength = NSMakeRange(0, [entryString length]);
1464 if ([entryString replaceOccurrencesOfString:@"\n" withString:@"\n\t\t" options:NSLiteralSearch range:fullLength])
1465 fullLength = NSMakeRange(0, [entryString length]);
1467 //Run the entry through the filters and add it to tipString
1468 entryString = [[[adium contentController] filterAttributedString:entryString
1469 usingFilterType:AIFilterTooltips
1470 direction:AIFilterIncoming
1471 context:object] mutableCopy];
1473 [entryString addAttributes:entryDict range:NSMakeRange(0,[entryString length])];
1474 [tipString appendAttributedString:entryString];
1475 [entryString release];
1478 return [tipString autorelease];
1481 //Custom pasting ----------------------------------------------------------------------------------------------------
1482 #pragma mark Custom Pasting
1483 //Paste, stripping formatting
1484 - (IBAction)paste:(id)sender
1486 [self _pasteWithPreferredSelector:@selector(pasteAsPlainTextWithTraits:) sender:sender];
1489 //Paste with formatting
1490 - (IBAction)pasteAndMatchStyle:(id)sender
1492 [self _pasteWithPreferredSelector:@selector(pasteAsPlainText:) sender:sender];
1495 - (IBAction)pasteWithImagesAndColors:(id)sender
1497 [self _pasteWithPreferredSelector:@selector(pasteAsRichText:) sender:sender];
1501 * @brief Send a paste message, using preferredSelector if possible and paste: if not
1503 * Walks the responder chain looking for a responder which can handle pasting, skipping instances of
1504 * WebHTMLView. These are skipped because we can control what paste does to WebView (by using a custom subclass) but
1505 * have no control over what the WebHTMLView would do.
1507 * If no responder is found, repeats the process looking for the simpler paste: selector.
1509 - (void)_pasteWithPreferredSelector:(SEL)selector sender:(id)sender
1511 NSWindow *keyWindow = [[NSApplication sharedApplication] keyWindow];
1512 NSResponder *responder;
1514 //First, look for a responder which can handle the preferred selector
1515 if (!(responder = [keyWindow earliestResponderWhichRespondsToSelector:selector
1516 andIsNotOfClass:NSClassFromString(@"WebHTMLView")])) {
1517 //No responder found. Try again, looking for one which will respond to paste:
1518 selector = @selector(paste:);
1519 responder = [keyWindow earliestResponderWhichRespondsToSelector:selector
1520 andIsNotOfClass:NSClassFromString(@"WebHTMLView")];
1523 //Sending pasteAsRichText: to a non rich text NSTextView won't do anything; change it to a generic paste:
1524 if ([responder isKindOfClass:[NSTextView class]] && ![(NSTextView *)responder isRichText]) {
1525 selector = @selector(paste:);
1529 [keyWindow makeFirstResponder:responder];
1530 [responder performSelector:selector
1535 //Custom Printing ------------------------------------------------------------------------------------------------------
1536 #pragma mark Custom Printing
1537 - (IBAction)adiumPrint:(id)sender
1539 //Pass the print command to the window, which is responsible for routing it to the correct place or
1540 //creating a view and printing. Adium will not print from a window that does not respond to adiumPrint:
1541 NSWindow *keyWindowController = [[[NSApplication sharedApplication] keyWindow] windowController];
1542 if ([keyWindowController respondsToSelector:@selector(adiumPrint:)]) {
1543 [keyWindowController performSelector:@selector(adiumPrint:)
1548 #pragma mark Preferences Display
1549 - (IBAction)showPreferenceWindow:(id)sender
1551 [[adium preferenceController] showPreferenceWindow:sender];
1554 #pragma mark Font Panel
1555 - (IBAction)toggleFontPanel:(id)sender
1557 if ([NSFontPanel sharedFontPanelExists] &&
1558 [[NSFontPanel sharedFontPanel] isVisible]) {
1559 [[NSFontPanel sharedFontPanel] close];
1562 NSFontPanel *fontPanel = [NSFontPanel sharedFontPanel];
1564 if (!fontPanelAccessoryView) {
1565 [NSBundle loadNibNamed:@"FontPanelAccessoryView" owner:self];
1566 [fontPanel setAccessoryView:fontPanelAccessoryView];
1569 [fontPanel orderFront:self];
1573 - (IBAction)setFontPanelSettingsAsDefaultFont:(id)sender
1575 NSFont *selectedFont = [[NSFontManager sharedFontManager] selectedFont];
1577 [[adium preferenceController] setPreference:[selectedFont stringRepresentation]
1578 forKey:KEY_FORMATTING_FONT
1579 group:PREF_GROUP_FORMATTING];
1581 //We can't get foreground/background color from the font panel so far as I can tell... so we do the best we can.
1582 NSWindow *keyWindow = [[NSApplication sharedApplication] keyWindow];
1583 NSResponder *responder = [keyWindow firstResponder];
1584 if ([responder isKindOfClass:[NSTextView class]]) {
1585 NSDictionary *typingAttributes = [(NSTextView *)responder typingAttributes];
1586 NSColor *foregroundColor, *backgroundColor;
1588 if ((foregroundColor = [typingAttributes objectForKey:NSForegroundColorAttributeName])) {
1589 [[adium preferenceController] setPreference:[foregroundColor stringRepresentation]
1590 forKey:KEY_FORMATTING_TEXT_COLOR
1591 group:PREF_GROUP_FORMATTING];
1594 if ((backgroundColor = [typingAttributes objectForKey:AIBodyColorAttributeName])) {
1595 [[adium preferenceController] setPreference:[backgroundColor stringRepresentation]
1596 forKey:KEY_FORMATTING_BACKGROUND_COLOR
1597 group:PREF_GROUP_FORMATTING];
1602 //Custom Dimming menu items --------------------------------------------------------------------------------------------
1603 #pragma mark Custom Dimming menu items
1604 //The standard ones do not dim correctly when unavailable
1605 - (IBAction)toggleFontTrait:(id)sender
1607 NSFontManager *fontManager = [NSFontManager sharedFontManager];
1609 if ([fontManager traitsOfFont:[fontManager selectedFont]] & [sender tag]) {
1610 [fontManager removeFontTrait:sender];
1612 [fontManager addFontTrait:sender];
1616 - (void)toggleToolbarShown:(id)sender
1618 NSWindow *window = [[NSApplication sharedApplication] keyWindow];
1619 [window toggleToolbarShown:sender];
1622 - (void)runToolbarCustomizationPalette:(id)sender
1624 NSWindow *window = [[NSApplication sharedApplication] keyWindow];
1625 [window runToolbarCustomizationPalette:sender];
1628 //Menu item validation
1629 - (BOOL)validateMenuItem:(NSMenuItem *)menuItem
1632 NSWindow *keyWindow = [[NSApplication sharedApplication] keyWindow];
1633 NSResponder *responder = [keyWindow firstResponder];
1635 if (menuItem == menuItem_bold || menuItem == menuItem_italic) {
1636 NSFont *selectedFont = [[NSFontManager sharedFontManager] selectedFont];
1638 //We must be in a text view, have text on the pasteboard, and have a font that supports bold or italic
1639 if ([responder isKindOfClass:[NSTextView class]]) {
1640 return (menuItem == menuItem_bold ? [selectedFont supportsBold] : [selectedFont supportsItalics]);
1644 } else if (menuItem == menuItem_paste || menuItem == menuItem_pasteAndMatchStyle || menuItem == menuItem_pasteWithImagesAndColors) {
1646 //The user can paste if the pasteboard contains an image, some text, one or more files, or one or more URLs.
1647 NSPasteboard *pboard = [NSPasteboard generalPasteboard];
1648 NSArray *nonImageTypes = [NSArray arrayWithObjects:
1652 NSFilenamesPboardType,
1653 NSFilesPromisePboardType,
1656 return ([pboard availableTypeFromArray:nonImageTypes] != nil) || [NSImage canInitWithPasteboard:pboard];
1658 } else if (menuItem == menuItem_showToolbar) {
1659 [menuItem_showToolbar setTitle:([[keyWindow toolbar] isVisible] ?
1660 AILocalizedString(@"Hide Toolbar",nil) :
1661 AILocalizedString(@"Show Toolbar",nil))];
1662 return [keyWindow toolbar] != nil;
1664 } else if (menuItem == menuItem_customizeToolbar) {
1665 return ([keyWindow toolbar] != nil && [[keyWindow toolbar] isVisible] && [[keyWindow windowController] canCustomizeToolbar]);
1667 } else if (menuItem == menuItem_close) {
1668 return (keyWindow && ([[keyWindow standardWindowButton:NSWindowCloseButton] isEnabled] ||
1669 ([[keyWindow windowController] respondsToSelector:@selector(windowPermitsClose)] &&
1670 [[keyWindow windowController] windowPermitsClose])));
1672 } else if (menuItem == menuItem_closeChat) {
1673 return activeChat != nil;
1675 } else if( menuItem == menuItem_closeAllChats) {
1676 return [[self openChats] count] > 0;
1678 } else if (menuItem == menuItem_print) {
1679 NSWindowController *windowController = [keyWindow windowController];
1681 return ([windowController respondsToSelector:@selector(adiumPrint:)] &&
1682 (![windowController respondsToSelector:@selector(validatePrintMenuItem:)] ||
1683 [windowController validatePrintMenuItem:menuItem]));
1685 } else if (menuItem == menuItem_showFonts) {
1686 [menuItem_showFonts setTitle:(([NSFontPanel sharedFontPanelExists] && [[NSFontPanel sharedFontPanel] isVisible]) ?
1687 AILocalizedString(@"Hide Fonts",nil) :
1688 AILocalizedString(@"Show Fonts",nil))];
1690 } else if (menuItem == menuItem_toggleUserlist) {
1691 return [[self activeChat] isGroupChat];
1697 #pragma mark Window levels
1698 - (NSMenu *)menuForWindowLevelsNotifyingTarget:(id)target
1700 NSMenu *windowPositionMenu = [[NSMenu allocWithZone:[NSMenu zone]] init];
1701 NSMenuItem *menuItem;
1703 menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Above other windows",nil)
1705 action:@selector(selectedWindowLevel:)
1707 [menuItem setEnabled:YES];
1708 [menuItem setTag:AIFloatingWindowLevel];
1709 [windowPositionMenu addItem:menuItem];
1712 menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Normally",nil)
1714 action:@selector(selectedWindowLevel:)
1716 [menuItem setEnabled:YES];
1717 [menuItem setTag:AINormalWindowLevel];
1718 [windowPositionMenu addItem:menuItem];
1721 menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Below other windows",nil)
1723 action:@selector(selectedWindowLevel:)
1725 [menuItem setEnabled:YES];
1726 [menuItem setTag:AIDesktopWindowLevel];
1727 [windowPositionMenu addItem:menuItem];
1730 [windowPositionMenu setAutoenablesItems:NO];
1732 return [windowPositionMenu autorelease];
1735 -(void)toggleUserlist:(id)sender
1737 NSLog(@"toggle userlist in interfaceController");
1738 [[adium notificationCenter] postNotificationName:@"toggleUserlist" object:nil];