* Adium is the legal property of its developers, whose names are listed in the copyright file included * with this source distribution. * This program is free software; you can redistribute it and/or modify it under the terms of the GNU * General Public License as published by the Free Software Foundation; either version 2 of the License, * or (at your option) any later version. * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General * Public License for more details. * You should have received a copy of the GNU General Public License along with this program; if not, * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. #import "AIInterfaceController.h" #import <Adium/AIAccountControllerProtocol.h> #import <Adium/AIContactControllerProtocol.h> #import <Adium/AIChatControllerProtocol.h> #import <Adium/AIContentControllerProtocol.h> #import <Adium/AIMenuControllerProtocol.h> #import <Adium/AIAuthorizationRequestsWindowController.h> #import <AIUtilities/AIAttributedStringAdditions.h> #import <AIUtilities/AIColorAdditions.h> #import <AIUtilities/AIFontAdditions.h> #import <AIUtilities/AIImageDrawingAdditions.h> #import <AIUtilities/AIMenuAdditions.h> #import <AIUtilities/AIStringAdditions.h> #import <AIUtilities/AITooltipUtilities.h> #import <AIUtilities/AIWindowAdditions.h> #import <AIUtilities/AITextAttributes.h> #import <AIUtilities/AIWindowControllerAdditions.h> #import <Adium/AIListContact.h> #import <Adium/AIListGroup.h> #import <Adium/AIListObject.h> #import <Adium/AIMetaContact.h> #import <Adium/AIService.h> #import <Adium/AIServiceIcons.h> #import <Adium/AISortController.h> #import "AIMessageWindowController.h" #import "AIMessageTabViewItem.h" #import <Adium/AIContactList.h> #import "AIListOutlineView.h" #import "AIMessageViewController.h" #define ERROR_MESSAGE_WINDOW_TITLE AILocalizedString(@"Adium : Error","Error message window title") #define LABEL_ENTRY_SPACING 4.0f #define DISPLAY_IMAGE_ON_RIGHT NO #define PREF_GROUP_FORMATTING @"Formatting" #define KEY_FORMATTING_FONT @"Default Font" #define MESSAGES_WINDOW_MENU_TITLE AILocalizedString(@"Chats","Title for the messages window menu item") //#define LOG_RESPONDER_CHAIN @interface NSObject (AIInterfaceController_WindowPrefsTarget) - ( void ) selectedWindowLevel: ( id ) sender ; @interface AIInterfaceController () - ( void ) _resetOpenChatsCache ; - ( void ) _addItemToMainMenuAndDock: ( NSMenuItem * ) item ; - ( NSMutableAttributedString * ) _tooltipTitleForObject: ( AIListObject * ) object ; - ( NSMutableAttributedString * ) _tooltipBodyForObject: ( AIListObject * ) object ; - ( void ) _pasteWithPreferredSelector: ( SEL ) preferredSelector sender: ( id ) sender ; - ( void ) updateCloseMenuKeys ; - ( void ) restoreSavedContainers ; - ( void ) saveContainersOnQuit: ( NSNotification * ) notification ; - ( void ) toggleUserlist: ( id ) sender ; - ( void ) toggleUserlistSide: ( id ) sender ; - ( void ) clearDisplay: ( id ) sender ; - ( IBAction ) closeContextualChat: ( id ) sender ; - ( void ) openAuthorizationWindow: ( id ) sender ; - ( void ) didReceiveContent: ( NSNotification * ) notification ; - ( void ) adiumDidFinishLoading: ( NSNotification * ) inNotification ; - ( void ) flashTimer: ( NSTimer * ) inTimer ; - ( void ) updateActiveWindowMenuItem ; - ( AIChat * ) mostRecentActiveChat ; * @class AIInterfaceController * @brief Interface controller * Chat window related requests, such as opening and closing chats, are routed through the interface controller * to the appropriate component. The interface controller keeps track of the most recently active chat, handles chat * cycling (switching between chats), chat sorting, and so on. The interface controller also handles switching to * an appropriate window or chat when the dock icon is clicked for a 'reopen' event. * Contact list window requests, such as toggling window visibilty are routed to the contact list controller component. * Error messages are routed through the interface controller. * Tooltips, such as seen on hover in the contact list are generated and displayed here. Tooltip display components and * plugins register with the interface controller to be queried for contact information when a tooltip is displayed. * When displays in Adium flash, such as in the dock or the contact list for unviewed content, the interface controller * manages keeping the flashing synchronized. * Finally, the interface controller manages many menu items, providing better menu item validation and target routing * than the responder chain alone would do. @implementation AIInterfaceController if (( self = [ super init ])) { contactListViewArray = [[ NSMutableArray alloc ] init ]; messageViewArray = [[ NSMutableArray alloc ] init ]; contactListTooltipEntryArray = [[ NSMutableArray alloc ] init ]; contactListTooltipSecondaryEntryArray = [[ NSMutableArray alloc ] init ]; closeMenuConfiguredForChat = NO ; mostRecentActiveChat = nil ; flashObserverArray = nil ; recentlyClosedChats = [[ NSMutableArray alloc ] init ]; #ifdef LOG_RESPONDER_CHAIN [ NSTimer scheduledTimerWithTimeInterval : 2.0 target : self selector : @selector ( reportResponderChain : ) userInfo : nil repeats : YES ]; #ifdef LOG_RESPONDER_CHAIN //Can be called by a timer to periodically log the responder chain //[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(reportResponderChain:) userInfo:nil repeats:YES]; - ( void ) reportResponderChain: ( NSTimer * ) inTimer NSMutableString * responderChain = [ NSMutableString string ]; NSWindow * keyWin = [[ NSApplication sharedApplication ] keyWindow ]; #warning 64BIT: Check formatting arguments [ responderChain appendFormat : @"%@ (%i): " , keyWin ,[ keyWin respondsToSelector : @selector ( print : )]]; NSResponder * responder = [ keyWin firstResponder ]; //First, walk down the responder chain looking for a responder which can handle the preferred selector #warning 64BIT: Check formatting arguments [ responderChain appendFormat : @"%@ (%i)" , responder ,[ responder respondsToSelector : @selector ( print : )]]; responder = [ responder nextResponder ]; if ( responder ) [ responderChain appendString : @" -> " ]; - ( void ) controllerDidLoad [ interfacePlugin openInterface ]; //Open the contact list window [ self showContactList : nil ]; //Userlist show/hide item menuItem_toggleUserlist = [[ NSMenuItem allocWithZone : [ NSMenu menuZone ]] initWithTitle : AILocalizedString ( @"Toggle User List" , nil ) action : @selector ( toggleUserlist : ) [ menuItem_toggleUserlist setKeyEquivalentModifierMask : ( NSCommandKeyMask | NSAlternateKeyMask )]; [ adium . menuController addMenuItem : menuItem_toggleUserlist toLocation : LOC_Display_General ]; menuItem_toggleUserlistSide = [[ NSMenuItem allocWithZone : [ NSMenu menuZone ]] initWithTitle : AILocalizedString ( @"Toggle User List Side" , nil ) action : @selector ( toggleUserlistSide : ) [ adium . menuController addMenuItem : menuItem_toggleUserlistSide toLocation : LOC_Display_General ]; NSMenuItem * menuItem = [[[ NSMenuItem allocWithZone : [ NSMenu menuZone ]] initWithTitle : AILocalizedString ( @"Toggle User List" , nil ) action : @selector ( toggleUserlist : ) keyEquivalent : @"" ] autorelease ]; [ adium . menuController addContextualMenuItem : menuItem toLocation : Context_GroupChat_Action ]; menuItem_clearDisplay = [[ NSMenuItem allocWithZone : [ NSMenu menuZone ]] initWithTitle : AILocalizedString ( @"Clear Display" , nil ) action : @selector ( clearDisplay : ) [ adium . menuController addMenuItem : menuItem_clearDisplay toLocation : LOC_Display_MessageControl ]; menuItem = [[ NSMenuItem allocWithZone : [ NSMenu menuZone ]] initWithTitle : AILocalizedString ( @"Contact List" , "Name of the window which lists contacts" ) action : @selector ( toggleContactList : ) [ adium . menuController addMenuItem : menuItem toLocation : LOC_Window_Fixed ]; //Contact list menu item for the dock menu menuItem = [[ NSMenuItem allocWithZone : [ NSMenu menuZone ]] initWithTitle : AILocalizedString ( @"Contact List" , "Name of the window which lists contacts" ) action : @selector ( showContactListAndBringToFront : ) [ adium . menuController addMenuItem : menuItem toLocation : LOC_Dock_Status ]; menuItem = [[ NSMenuItem allocWithZone : [ NSMenu menuZone ]] initWithTitle : AILocalizedString ( @"Close Chat" , "Title for the close chat menu item" ) action : @selector ( closeContextualChat : ) [ adium . menuController addContextualMenuItem : menuItem toLocation : Context_Tab_Action ]; // Authorization requests menu item menuItem = [[ NSMenuItem alloc ] initWithTitle : AILocalizedStringFromTableInBundle ( @"Authorization Requests" , nil , [ NSBundle bundleForClass : [ AIAuthorizationRequestsWindowController class ]], nil ) action : @selector ( openAuthorizationWindow : ) [ adium . menuController addMenuItem : menuItem toLocation : LOC_Window_Auxiliary ]; //Observe content so we can open chats as necessary [[ NSNotificationCenter defaultCenter ] addObserver : self selector : @selector ( didReceiveContent : ) name : CONTENT_MESSAGE_RECEIVED object : nil ]; [[ NSNotificationCenter defaultCenter ] addObserver : self selector : @selector ( didReceiveContent : ) name : CONTENT_MESSAGE_RECEIVED_GROUP object : nil ]; //Observe Adium finishing loading so we can do things which may require other components or plugins [[ NSNotificationCenter defaultCenter ] addObserver : self selector : @selector ( adiumDidFinishLoading : ) name : AIApplicationDidFinishLoadingNotification //Observe quits so we can save containers. [[ NSNotificationCenter defaultCenter ] addObserver : self selector : @selector ( saveContainersOnQuit : ) name : AIAppWillTerminateNotification - ( void ) controllerWillClose [ contactListPlugin closeContactList ]; [ interfacePlugin closeInterface ]; [ contactListViewArray release ]; contactListViewArray = nil ; [ messageViewArray release ]; messageViewArray = nil ; [ interfaceArray release ]; interfaceArray = nil ; [ tooltipListObject release ]; tooltipListObject = nil ; [ tooltipTitle release ]; tooltipTitle = nil ; [ tooltipBody release ]; tooltipBody = nil ; [ tooltipImage release ]; tooltipImage = nil ; [[ NSNotificationCenter defaultCenter ] removeObserver : self ]; [ adium . preferenceController unregisterPreferenceObserver : self ]; [ recentlyClosedChats release ]; recentlyClosedChats = nil ; - ( void ) adiumDidFinishLoading: ( NSNotification * ) inNotification //Observe preference changes. This will also restore saved containers if appropriate. [ adium . preferenceController registerPreferenceObserver : self forGroup : PREF_GROUP_INTERFACE ]; [[ NSNotificationCenter defaultCenter ] removeObserver : self name : AIApplicationDidFinishLoadingNotification //Registers code to handle the interface - ( void ) registerInterfaceController: ( id < AIInterfaceComponent > ) inController if ( ! interfacePlugin ) interfacePlugin = [ inController retain ]; //Register code to handle the contact list - ( void ) registerContactListController: ( id < AIMultiContactListComponent > ) inController if ( ! contactListPlugin ) contactListPlugin = [ inController retain ]; - ( void ) preferencesChangedForGroup: ( NSString * ) group key: ( NSString * ) key object :( AIListObject * ) object preferenceDict: ( NSDictionary * ) prefDict firstTime: ( BOOL ) firstTime tabbedChatting = [[ prefDict objectForKey : KEY_TABBED_CHATTING ] boolValue ]; groupChatsByContactGroup = [[ prefDict objectForKey : KEY_GROUP_CHATS_BY_GROUP ] boolValue ]; saveContainers = [[ prefDict objectForKey : KEY_SAVE_CONTAINERS ] boolValue ]; //Restore saved containers [ self performSelector : @selector ( restoreSavedContainers ) withObject : nil afterDelay : 0.0 ]; } else if ([ prefDict objectForKey : KEY_CONTAINERS ]) { /* We've loaded without wanting to save containers; clear any saved * from a previous session. [ adium . preferenceController setPreference : nil group : PREF_GROUP_INTERFACE ]; //Handle a reopen/dock icon click - ( BOOL ) handleReopenWithVisibleWindows: ( BOOL ) visibleWindows if ( ! [ self contactListIsVisibleAndMain ] && [[ interfacePlugin openContainerIDs ] count ] == 0 ) { //The contact list is not visible, and there are no chat windows. Make the contact list visible. [ self showContactList : nil ]; AIChat * mostRecentUnviewedChat ; //If windows are open, try switching to a chat with unviewed content if (( mostRecentUnviewedChat = [ adium . chatController mostRecentUnviewedChat ])) { if ([ mostRecentActiveChat unviewedContentCount ]) { //If the most recently active chat has unviewed content, ensure it is in the front [ self setActiveChat : mostRecentActiveChat ]; //Otherwise, switch to the chat which most recently received content [ self setActiveChat : mostRecentUnviewedChat ]; NSWindow * targetWindow = nil ; BOOL unMinimizedWindows = 0 ; //If there was no unviewed content, ensure that atleast one of Adium's windows is unminimized for ( NSWindow * window in [ NSApp windows ]) { //Check stylemask to rule out the system menu's window (Which reports itself as visible like a real window) if (([ window styleMask ] & ( NSTitledWindowMask | NSClosableWindowMask | NSMiniaturizableWindowMask ))) { if ( ! targetWindow ) targetWindow = window ; if ( ! [ window isMiniaturized ]) unMinimizedWindows ++ ; //If there are no unminimized windows, unminimize the last one if ( unMinimizedWindows == 0 && targetWindow ) { [ targetWindow deminiaturize : nil ]; //Contact List --------------------------------------------------------------------------------------------------------- #pragma mark Contact list * @brief Toggles contact list between visible and hiden - ( IBAction ) toggleContactList: ( id ) sender if ([ self contactListIsVisibleAndMain ]) { [ self closeContactList : nil ]; [[ NSApplication sharedApplication ] activateIgnoringOtherApps : YES ]; [ self showContactList : nil ]; * @brief Brings contact list to the front - ( IBAction ) showContactList: ( id ) sender [ contactListPlugin showContactListAndBringToFront : YES ]; * @brief Show the contact list window and bring Adium to the front - ( IBAction ) showContactListAndBringToFront: ( id ) sender [ contactListPlugin showContactListAndBringToFront : YES ]; [[ NSApplication sharedApplication ] activateIgnoringOtherApps : YES ]; * @brief Close the contact list window - ( IBAction ) closeContactList: ( id ) sender [ contactListPlugin closeContactList ]; * @returns YES if contact list is visible and selected, otherwise NO - ( BOOL ) contactListIsVisibleAndMain return [ contactListPlugin contactListIsVisibleAndMain ]; * @returns YES if contact list is visible, otherwise NO - ( BOOL ) contactListIsVisible return [ contactListPlugin contactListIsVisible ]; //Detachable Contact List ---------------------------------------------------------------------------------------------- #pragma mark Detachable Contact List * @returns Created contact list controller for detached contact list - ( AIListWindowController * ) detachContactList: ( AIContactList * ) aContactList return [ contactListPlugin detachContactList : aContactList ]; #pragma mark Container Saving * @brief Restores containers saved from a previous session - ( void ) restoreSavedContainers NSData * savedData = [ adium . preferenceController preferenceForKey : KEY_CONTAINERS group : PREF_GROUP_INTERFACE ]; // If there's no data, we can't restore anything. [[ AIContactObserverManager sharedManager ] delayListObjectNotifications ]; for ( NSDictionary * dict in [ NSKeyedUnarchiver unarchiveObjectWithData : savedData ]) { AIMessageWindowController * windowController = [ self openContainerWithID : [ dict objectForKey : @"ID" ] name :[ dict objectForKey : @"Name" ]]; AIChat * containerActiveChat = nil ; // Position the container where it was last saved (using -savedFrameFromString: to prevent going offscreen) [[ windowController window ] setFrame : [ windowController savedFrameFromString : [ dict objectForKey : @"Frame" ]] display : YES ]; for ( NSDictionary * chatDict in [ dict objectForKey : @"Content" ]) { AIService * service = [ adium . accountController firstServiceWithServiceID : [ chatDict objectForKey : @"serviceID" ]]; AIAccount * account = [ adium . accountController accountWithInternalObjectID : [ chatDict objectForKey : @"AccountID" ]]; if ([[ chatDict objectForKey : @"IsGroupChat" ] boolValue ]) { chat = [ adium . chatController chatWithName : [ chatDict objectForKey : @"Name" ] chatCreationInfo :[ chatDict objectForKey : @"ChatCreationInfo" ]]; AIListContact * contact = [ adium . contactController contactWithService : service UID :[ chatDict objectForKey : @"UID" ]]; chat = [ adium . chatController chatWithContact : contact ]; // Tag the chat as restored. [ chat setValue : [ NSNumber numberWithBool : YES ] forProperty : @"Restored Chat" if ([[ chatDict objectForKey : @"ActiveChat" ] boolValue ]) { containerActiveChat = chat ; // Open the chat into the container we've created above. [ self openChat : chat inContainerWithID : [ dict objectForKey : @"ID" ] atIndex : -1 ]; [ self setActiveChat : containerActiveChat ]; [[ AIContactObserverManager sharedManager ] endListObjectNotificationsDelay ]; * @brief Saves open container information with their content when Adium quits - ( void ) saveContainersOnQuit: ( NSNotification * ) notification * @brief Save opened containers and windows * @param withContent Save the current buffer of the window to restore at a later point // Don't save anything if we're not set to. // Save active containers. NSMutableArray * savedContainers = [ NSMutableArray array ]; for ( NSDictionary * dict in [ interfacePlugin openContainersAndChats ]) { NSMutableArray * containerContents = [ NSMutableArray array ]; for ( AIChat * chat in [ dict objectForKey : @"Content" ]) { NSMutableDictionary * newContainerDict = [ NSMutableDictionary dictionary ]; [ newContainerDict setObject : chat . account . internalObjectID forKey : @"AccountID" ]; // Save chat-specific information. // -chatCreationDictionary may be nil, so put it last. [ newContainerDict addEntriesFromDictionary : [ NSDictionary dictionaryWithObjectsAndKeys : [ NSNumber numberWithBool : YES ], @"IsGroupChat" , [ NSNumber numberWithBool : ([ dict objectForKey : @"ActiveChat" ] == chat )], @"ActiveChat" , [ chat chatCreationDictionary ], @"ChatCreationInfo" , nil ]]; [ newContainerDict addEntriesFromDictionary : [ NSDictionary dictionaryWithObjectsAndKeys : [ NSNumber numberWithBool : ([ dict objectForKey : @"ActiveChat" ] == chat )], @"ActiveChat" , chat . listObject . UID , @"UID" , chat . account . service . serviceID , @"serviceID" , chat . account . internalObjectID , @"AccountID" , nil ]]; [ containerContents addObject : newContainerDict ]; // Replace the "Content" key in -openContainersAndChats with our version of the content. // Remove the ActiveChat reference // We use the same keys otherwise that -openContainersAndChats provides (Name, ID, Frame) NSMutableDictionary * saveDict = [[ dict mutableCopy ] autorelease ]; [ saveDict removeObjectForKey : @"ActiveChat" ]; [ saveDict setObject : containerContents [ savedContainers addObject : saveDict ]; [ adium . preferenceController setPreference : [ NSKeyedArchiver archivedDataWithRootObject : savedContainers ] group : PREF_GROUP_INTERFACE ]; //Messaging ------------------------------------------------------------------------------------------------------------ //Methods for instructing the interface to provide a representation of chats, and to determine which chat has user focus * @brief Opens window for chat - ( void ) openChat: ( AIChat * ) inChat NSArray * containerIDs = [ interfacePlugin openContainerIDs ]; NSString * containerID = nil ; NSString * containerName = nil ; //Determine the correct container for this chat //We're not using tabs; each chat starts in its own container, based on the destination object or the chat name if ([ inChat listObject ]) { containerID = inChat . listObject . internalObjectID ; containerID = inChat . name ; } else if ( groupChatsByContactGroup ) { if ( inChat . isGroupChat ) { containerID = AILocalizedString ( @"Group Chats" , nil ); //XXX multiple containers: this is "correct" but maybe not desirable, as it is non-deterministic AIListGroup * group = inChat . listObject . parentContact . groups . anyObject ; //If the contact is in the contact list root, we don't have a group if ( group && ! [ group isKindOfClass : [ AIContactList class ]]) { containerID = group . displayName ; containerName = containerID ; //Open new chats into the first container (if not available, create a new one) if ([ containerIDs count ] > 0 ) { containerID = [ containerIDs objectAtIndex : 0 ]; //Determine the correct placement for this chat within the container [ interfacePlugin openChat : inChat inContainerWithID : containerID withName : containerName atIndex : -1 ]; //Post the notification last, so observers receive a chat whose isOpen flag is yes. [[ NSNotificationCenter defaultCenter ] postNotificationName : Chat_DidOpen object : inChat userInfo : nil ]; - ( id ) openChat: ( AIChat * ) inChat inContainerWithID: ( NSString * ) containerID atIndex: ( NSUInteger ) idx NSArray * openContainerIDs = [ interfacePlugin openContainerIDs ]; //Open new chats into the first container (if not available, create a new one) if ([ openContainerIDs count ] > 0 ) { containerID = [ openContainerIDs objectAtIndex : 0 ]; containerID = AILocalizedString ( @"Chats" , nil ); //Determine the correct placement for this chat within the container id tabViewItem = [ interfacePlugin openChat : inChat inContainerWithID : containerID withName : nil atIndex : idx ]; //Post the notification last, so observers receive a chat whose isOpen flag is yes. [[ NSNotificationCenter defaultCenter ] postNotificationName : Chat_DidOpen object : inChat userInfo : nil ]; * @brief Opens a container with a specific ID * Asks the interfacePlugin to openContainerWithID: - ( AIMessageWindowController * ) openContainerWithID: ( NSString * ) containerID name: ( NSString * ) containerName return [ interfacePlugin openContainerWithID : containerID name : containerName ]; * @brief Close the interface for a chat * Tell the interface plugin to close the chat. - ( void ) closeChat: ( AIChat * ) inChat if ([ adium . chatController closeChat : inChat ]) { NSMutableDictionary * newRecentlyClosedChat = [ NSMutableDictionary dictionary ]; [ newRecentlyClosedChat setObject : inChat . account . internalObjectID forKey : @"AccountID" ]; if ( inChat . isGroupChat ) { // -chatCreationDictionary may be nil, so put it last. [ newRecentlyClosedChat addEntriesFromDictionary : [ NSDictionary dictionaryWithObjectsAndKeys : [ NSNumber numberWithBool : YES ], @"IsGroupChat" , [ inChat chatCreationDictionary ], @"ChatCreationInfo" , nil ]]; [ newRecentlyClosedChat addEntriesFromDictionary : [ NSDictionary dictionaryWithObjectsAndKeys : inChat . listObject . UID , @"UID" , inChat . account . service . serviceID , @"serviceID" , inChat . account . internalObjectID , @"AccountID" , nil ]]; [ recentlyClosedChats insertObject : newRecentlyClosedChat atIndex : 0 ]; // this sounds like a sensible limit: no-one will remember what chat they had in the closed tab beyond these while ( recentlyClosedChats . count > 16 ) { [ recentlyClosedChats removeLastObject ]; [ interfacePlugin closeChat : inChat ]; * @brief Consolidate chats into a single container //We work with copies of these arrays, since moving chats may change their contents NSArray * openContainerIDs = [[ interfacePlugin openContainerIDs ] copy ]; NSEnumerator * containerEnumerator = [ openContainerIDs objectEnumerator ]; NSString * firstContainerID = [ containerEnumerator nextObject ]; //For all containers but the first, move the chats they contain to the first container while (( containerID = [ containerEnumerator nextObject ])) { NSArray * openChats = [[ interfacePlugin openChatsInContainerWithID : containerID ] copy ]; //Move all the chats, providing a target index if chat sorting is enabled for ( AIChat * chat in openChats ) { [ interfacePlugin moveChat : chat toContainerWithID : firstContainerID [ self chatOrderDidChange ]; [ openContainerIDs release ]; - ( void ) moveChatToNewContainer: ( AIChat * ) inChat [ interfacePlugin moveChatToNewContainer : inChat ]; * @brief Set the active chat window - ( void ) setActiveChat: ( AIChat * ) inChat [ interfacePlugin setActiveChat : inChat ]; * @returns Last chat to be active, nil if not chat is open - ( AIChat * ) mostRecentActiveChat return mostRecentActiveChat ; * @brief Sets active chat window based on chat - ( void ) setMostRecentActiveChat: ( AIChat * ) inChat [ self setActiveChat : inChat ]; * @returns Array of open chats (cached, so call as frequently as desired) _cachedOpenChats = [[ interfacePlugin openChats ] retain ]; - ( NSArray * ) openContainerIDs return [ interfacePlugin openContainerIDs ]; * @param containerID ID for chat window * @returns Array of all chats in chat window - ( NSArray * ) openChatsInContainerWithID: ( NSString * ) containerID return [ interfacePlugin openChatsInContainerWithID : containerID ]; * @brief The container ID for a chat * @param chat The chat to look up * @returns The container ID for the container the chat is in. - ( NSString * ) containerIDForChat: ( AIChat * ) chat return [ interfacePlugin containerIDForChat : chat ]; * @brief Resets the cache of open chats - ( void ) _resetOpenChatsCache [ _cachedOpenChats release ]; _cachedOpenChats = nil ; - ( IBAction ) reopenChat: ( id ) sender if ( recentlyClosedChats . count == 0 ) { AILogWithSignature ( @"Can't open recently closed tab: no recently closed tabs!" ); NSDictionary * chatDict = [[[ recentlyClosedChats objectAtIndex : 0 ] retain ] autorelease ]; [ recentlyClosedChats removeObjectAtIndex : 0 ]; AIService * service = [ adium . accountController firstServiceWithServiceID : [ chatDict objectForKey : @"serviceID" ]]; AIAccount * account = [ adium . accountController accountWithInternalObjectID : [ chatDict objectForKey : @"AccountID" ]]; if ([[ chatDict objectForKey : @"IsGroupChat" ] boolValue ]) { chat = [ adium . chatController chatWithName : [ chatDict objectForKey : @"Name" ] chatCreationInfo :[ chatDict objectForKey : @"ChatCreationInfo" ]]; AIListContact * contact = [ adium . contactController contactWithService : service UID :[ chatDict objectForKey : @"UID" ]]; if ( contact ) chat = [ adium . chatController chatWithContact : contact ]; NSRunAlertPanel ( AILocalizedString ( @"Restoring chat failed" , nil ), AILocalizedString ( @"Restoring the last closed tab failed. Perhaps the account not exist anymore?" , nil ), AILocalizedString ( @"OK" , nil ), // Tag the chat as restored. [ chat setValue : [ NSNumber numberWithBool : YES ] forProperty : @"Restored Chat" [ self openChat : chat inContainerWithID : nil atIndex : -1 ]; [ self setActiveChat : chat ]; //Interface plugin callbacks ------------------------------------------------------------------------------------------- //These methods are called by the interface to let us know what's going on. We're informed of chats opening, closing, #pragma mark Interface plugin callbacks * @brief A chat window did open: rebuild our window menu to show the new chat * This should be called by the interface plugin (e.g. AIDualWindowInterfacePlugin) after a chat opens * @param inChat Newly created chat - ( void ) chatDidOpen: ( AIChat * ) inChat [ self _resetOpenChatsCache ]; * @brief A chat has become active: update our chat closing keys and flag this chat as selected in the window menu * @param inChat Chat which has become active - ( void ) chatDidBecomeActive: ( AIChat * ) inChat AIChat * previouslyActiveChat = activeChat ; activeChat = [ inChat retain ]; [ self updateCloseMenuKeys ]; [ self updateActiveWindowMenuItem ]; if ( inChat && ( inChat != mostRecentActiveChat )) { [ mostRecentActiveChat release ]; mostRecentActiveChat = nil ; mostRecentActiveChat = [ inChat retain ]; [[ NSNotificationCenter defaultCenter ] postNotificationName : Chat_BecameActive userInfo :( previouslyActiveChat ? [ NSDictionary dictionaryWithObject : previouslyActiveChat forKey : @"PreviouslyActiveChat" ] : /* Clear the unviewed content on the next event loop so other methods have a chance to react to the chat becoming * active. Specifically, this lets the handleReopenWithVisibleWindows: method have a chance to know that this chat [ inChat performSelector : @selector ( clearUnviewedContentCount ) [ previouslyActiveChat release ]; * @brief A chat has become visible: send out a notification for components and plugins to take action * @param inChat Chat that has become active * @param nWindow Containing chat window - ( void ) chatDidBecomeVisible: ( AIChat * ) inChat inWindow: ( NSWindow * ) inWindow [[ NSNotificationCenter defaultCenter ] postNotificationName : @"AIChatDidBecomeVisible" userInfo :[ NSDictionary dictionaryWithObject : inWindow * @brief Find the window currently displaying a chat * @returns Window for chat otherwise if the chat is not in any window, or is not visible in any window, returns nil - ( NSWindow * ) windowForChat: ( AIChat * ) inChat return [ interfacePlugin windowForChat : inChat ]; * @brief Find the chat active in a window * If the window does not have an active chat, nil is returned - ( AIChat * ) activeChatInWindow: ( NSWindow * ) window return [ interfacePlugin activeChatInWindow : window ]; * @brief A chat window did close: rebuild our window menu to remove the chat * @param inChat Chat that closed - ( void ) chatDidClose: ( AIChat * ) inChat [ self _resetOpenChatsCache ]; [ inChat clearUnviewedContentCount ]; // Don't save containers when the chats are closed while quitting if ( inChat == activeChat ) { [ activeChat release ]; activeChat = nil ; if ( inChat == mostRecentActiveChat ) { [ mostRecentActiveChat release ]; mostRecentActiveChat = nil ; * @brief The order of chats has changed: rebuild our window menu to reflect the new order - ( void ) chatOrderDidChange [ self _resetOpenChatsCache ]; // Don't save containers when the chats are closed while quitting [[ NSNotificationCenter defaultCenter ] postNotificationName : Chat_OrderDidChange object : nil userInfo : nil ]; #pragma mark Unviewed content * @breif Content was received, increase the unviewed content count of the chat (if it's not currently active) - ( void ) didReceiveContent: ( NSNotification * ) notification AIChat * chat = [[ notification userInfo ] objectForKey : @"AIChat" ]; if ( chat != activeChat ) { [ chat incrementUnviewedContentCount ]; //Chat close menus ----------------------------------------------------------------------------------------------------- #pragma mark Chat close menus * @brief Closes currently active window - ( IBAction ) closeMenu: ( id ) sender [[[ NSApplication sharedApplication ] keyWindow ] performClose : nil ]; * @brief Closes currently active chat (if there is an active chat) - ( IBAction ) closeChatMenu: ( id ) sender if ( activeChat ) [ self closeChat : activeChat ]; * @brief Closes currently selected chat based on current chat contextual menu - ( IBAction ) closeContextualChat: ( id ) sender [ self closeChat : [ adium . menuController currentContextMenuChat ]]; * @brief Loop through open chats and close them - ( IBAction ) closeAllChats: ( id ) sender for ( AIChat * chatToClose in [[ interfacePlugin . openChats copy ] autorelease ]) { [ self closeChat : chatToClose ]; * @brief Updates the key equivalents on 'close' and 'close chat' (dynamically changed to make cmd-w less destructive) - ( void ) updateCloseMenuKeys if ( activeChat && ! closeMenuConfiguredForChat ) { [ menuItem_close setKeyEquivalent : @"W" ]; [ menuItem_closeChat setKeyEquivalent : @"w" ]; closeMenuConfiguredForChat = YES ; } else if ( ! activeChat && closeMenuConfiguredForChat ) { [ menuItem_close setKeyEquivalent : @"w" ]; [ menuItem_closeChat removeKeyEquivalent ]; closeMenuConfiguredForChat = NO ; //Window Menu ---------------------------------------------------------------------------------------------------------- * @brief Open the authorization requests window. - ( void ) openAuthorizationWindow: ( id ) sender [[ AIAuthorizationRequestsWindowController sharedController ] showWindow : nil ]; * @brief Make a chat window active * Invoked by a selection in the window menu - ( IBAction ) showChatWindow: ( id ) sender [ self setActiveChat : [ sender representedObject ]]; [[ NSApplication sharedApplication ] activateIgnoringOtherApps : YES ]; * @brief Updates the 'check' icon so it's next to the active window - ( void ) updateActiveWindowMenuItem for ( item in windowMenuArray ) { if ([ item representedObject ]) [ item setState : ([ item representedObject ] == activeChat ? NSOnState : NSOffState )]; * @brief Builds the window menu * This function gets called whenever chats are opened, closed, or re-ordered - so improvements and optimizations here * would probably be helpful //Remove any existing menus for ( item in windowMenuArray ) { [ adium . menuController removeMenuItem : item ]; [ windowMenuArray release ]; windowMenuArray = [[ NSMutableArray alloc ] init ]; //Messages window and any open messasges for ( NSDictionary * containerDict in [ interfacePlugin openContainersAndChats ]) { NSString * containerName = [ containerDict objectForKey : @"Name" ]; NSArray * contentArray = [ containerDict objectForKey : @"Content" ]; //Add a menu item for the container if ( contentArray . count > 1 ) { item = [[ NSMenuItem allocWithZone : [ NSMenu menuZone ]] initWithTitle : ([ containerName length ] ? containerName : AILocalizedString ( @"Chats" , nil )) [ self _addItemToMainMenuAndDock : item ]; //Add items for the chats it contains for ( AIChat * chat in [ contentArray objectEnumerator ]) { NSString * windowKeyString ; //Prepare a key equivalent for the controller windowKeyString = [ NSString stringWithFormat : @"%ld" , ( windowKey )]; } else if ( windowKey == 10 ) { item = [[ NSMenuItem allocWithZone : [ NSMenu menuZone ]] initWithTitle : chat . displayName action : @selector ( showChatWindow : ) keyEquivalent : windowKeyString ]; if ([ contentArray count ] > 1 ) [ item setIndentationLevel : 1 ]; [ item setRepresentedObject : chat ]; [ item setImage : chat . chatMenuImage ]; [ self _addItemToMainMenuAndDock : item ]; [ self updateActiveWindowMenuItem ]; * brief Adds a menu item to the internal array, dock menu, and main menu * Should be used for adding a new window to the window menu (and dock menu) - ( void ) _addItemToMainMenuAndDock: ( NSMenuItem * ) item [ adium . menuController addMenuItem : item toLocation : LOC_Window_Fixed ]; [ windowMenuArray addObject : item ]; //Make a copy, and add to the dock [ item setKeyEquivalent : @"" ]; [ adium . menuController addMenuItem : item toLocation : LOC_Dock_Status ]; [ windowMenuArray addObject : item ]; //Chat Cycling --------------------------------------------------------------------------------------------------------- #pragma mark Chat Cycling * @brief Cycles to the next active chat - ( void ) nextChat: ( id ) sender NSString * containerID = [ self containerIDForChat : activeChat ]; NSArray * chats = [ self openChatsInContainerWithID : containerID ]; NSInteger nextChat = [ chats indexOfObject : activeChat ] + 1 ; if ( nextChat >= chats . count ) [ self setActiveChat : [ chats objectAtIndex : nextChat ]]; * @brief Cycles to the previus active chat - ( void ) previousChat: ( id ) sender NSString * containerID = [ self containerIDForChat : activeChat ]; NSArray * chats = [ self openChatsInContainerWithID : containerID ]; NSInteger nextChat = [ chats indexOfObject : activeChat ] - 1 ; nextChat = chats . count - 1 ; [ self setActiveChat : [ chats objectAtIndex : nextChat ]]; //Selected contact ------------------------------------------------ #pragma mark Selected contact - ( id ) _performSelectorOnFirstAvailableResponder: ( SEL ) selector NSResponder * responder = [[[ NSApplication sharedApplication ] mainWindow ] firstResponder ]; //Check the first responder if ([ responder respondsToSelector : selector ]) { return [ responder performSelector : selector ]; //Search the responder chain responder = [ responder nextResponder ]; if ([ responder respondsToSelector : selector ]) { return [ responder performSelector : selector ]; } while ( responder != nil ); - ( id ) _performSelectorOnFirstAvailableResponder: ( SEL ) selector conformingToProtocol: ( Protocol * ) protocol NSResponder * responder = [[[ NSApplication sharedApplication ] mainWindow ] firstResponder ]; //Check the first responder if ([ responder conformsToProtocol : protocol ] && [ responder respondsToSelector : selector ]) { return [ responder performSelector : selector ]; //Search the responder chain responder = [ responder nextResponder ]; if ([ responder conformsToProtocol : protocol ] && [ responder respondsToSelector : selector ]) { return [ responder performSelector : selector ]; } while ( responder != nil ); * @returns The "selected"(represented) contact (By finding the first responder that returns a contact) * If no listObject is found, try to find a list object selected in a group chat - ( AIListObject * ) selectedListObject AIListObject * listObject = [ self _performSelectorOnFirstAvailableResponder : @selector ( listObject )]; listObject = [ self _performSelectorOnFirstAvailableResponder : @selector ( preferredListObject )]; - ( AIListObject * ) selectedListObjectInContactList return [ self _performSelectorOnFirstAvailableResponder : @selector ( listObject ) conformingToProtocol : @ protocol ( ContactListOutlineView )]; - ( NSArray * ) arrayOfSelectedListObjectsInContactList return [ self _performSelectorOnFirstAvailableResponder : @selector ( arrayOfListObjects ) conformingToProtocol : @ protocol ( ContactListOutlineView )]; - ( NSArray * ) arrayOfSelectedListObjectsWithGroupsInContactList return [ self _performSelectorOnFirstAvailableResponder : @selector ( arrayOfListObjectsWithGroups ) conformingToProtocol : @ protocol ( ContactListOutlineView )]; //Message View --------------------------------------------------------------------------------------------------------- //Message view is abstracted from the containing interface, since they're not directly related to eachother #pragma mark Message View //Registers a view to handle the contact list - ( void ) registerMessageDisplayPlugin: ( id < AIMessageDisplayPlugin > ) inPlugin [ messageViewArray addObject : inPlugin ]; - ( void ) unregisterMessageDisplayPlugin: ( id < AIMessageDisplayPlugin > ) inPlugin [ messageViewArray removeObject : inPlugin ]; - ( id < AIMessageDisplayController > ) messageDisplayControllerForChat: ( AIChat * ) inChat //Sometimes our users find it amusing to disable plugins that are located within the Adium bundle. This error //trap prevents us from crashing if they happen to disable all the available message view plugins. //PUT THAT PLUGIN BACK IT WAS IMPORTANT! if ([ messageViewArray count ] == 0 ) { AILogWithSignature ( @"WARNING: Called for %@ without a mesage display controller." , inChat ); return [[ messageViewArray objectAtIndex : 0 ] messageDisplayControllerForChat : inChat ]; //Error Display -------------------------------------------------------------------------------------------------------- #pragma mark Error Display - ( void ) handleErrorMessage: ( NSString * ) inTitle withDescription: ( NSString * ) inDesc [ self handleMessage : inTitle withDescription : inDesc withWindowTitle : ERROR_MESSAGE_WINDOW_TITLE ]; - ( void ) handleMessage: ( NSString * ) inTitle withDescription: ( NSString * ) inDesc withWindowTitle: ( NSString * ) inWindowTitle ; //Post a notification that an error was recieved errorDict = [ NSDictionary dictionaryWithObjectsAndKeys : inTitle , @"Title" , inDesc , @"Description" , inWindowTitle , @"Window Title" , nil ]; [[ NSNotificationCenter defaultCenter ] postNotificationName : Interface_ShouldDisplayErrorMessage object : nil userInfo : errorDict ]; //Display then clear the last disconnection error - ( void ) account : ( AIAccount * ) inAccount disconnectedWithError : ( NSString * ) disconnectionError //Question Display ----------------------------------------------------------------------------------------------------- #pragma mark Question Display - ( void ) displayQuestion : ( NSString * ) inTitle withAttributedDescription : ( NSAttributedString * ) inDesc withWindowTitle : ( NSString * ) inWindowTitle defaultButton :( NSString * ) inDefaultButton alternateButton : ( NSString * ) inAlternateButton otherButton : ( NSString * ) inOtherButton suppression : ( NSString * ) inSuppression target :( id ) inTarget selector : ( SEL ) inSelector userInfo : ( id ) inUserInfo [ self displayQuestion : inTitle withAttributedDescription : inDesc withWindowTitle : inWindowTitle defaultButton : inDefaultButton alternateButton : inAlternateButton otherButton : inOtherButton suppression : inSuppression - ( void ) displayQuestion : ( NSString * ) inTitle withAttributedDescription : ( NSAttributedString * ) inDesc withWindowTitle : ( NSString * ) inWindowTitle defaultButton :( NSString * ) inDefaultButton alternateButton : ( NSString * ) inAlternateButton otherButton : ( NSString * ) inOtherButton suppression : ( NSString * ) inSuppression makeKey :( BOOL ) key target : ( id ) inTarget selector : ( SEL ) inSelector userInfo : ( id ) inUserInfo NSMutableDictionary * questionDict = [ NSMutableDictionary dictionary ]; [ questionDict setObject : inTitle forKey : @"Title" ]; [ questionDict setObject : inDesc forKey : @"Description" ]; [ questionDict setObject : inWindowTitle forKey : @"Window Title" ]; if ( inDefaultButton != nil ) [ questionDict setObject : inDefaultButton forKey : @"Default Button" ]; if ( inAlternateButton != nil ) [ questionDict setObject : inAlternateButton forKey : @"Alternate Button" ]; [ questionDict setObject : inOtherButton forKey : @"Other Button" ]; [ questionDict setObject : inSuppression forKey : @"Suppression Checkbox" ]; [ questionDict setObject : inTarget forKey : @"Target" ]; [ questionDict setObject : NSStringFromSelector ( inSelector ) forKey : @"Selector" ]; [ questionDict setObject : inUserInfo forKey : @"Userinfo" ]; [ questionDict setObject : @( key ) forKey : @"Make Key" ]; [[ NSNotificationCenter defaultCenter ] postNotificationName : Interface_ShouldDisplayQuestion object : nil userInfo : questionDict ]; - ( void ) displayQuestion : ( NSString * ) inTitle withDescription : ( NSString * ) inDesc withWindowTitle : ( NSString * ) inWindowTitle defaultButton :( NSString * ) inDefaultButton alternateButton : ( NSString * ) inAlternateButton otherButton : ( NSString * ) inOtherButton suppression : ( NSString * ) inSuppression target :( id ) inTarget selector : ( SEL ) inSelector userInfo : ( id ) inUserInfo [ self displayQuestion : inTitle withAttributedDescription :[[[ NSAttributedString alloc ] initWithString : inDesc attributes :[ NSDictionary dictionaryWithObject : [ NSFont systemFontOfSize : 0 ] forKey : NSFontAttributeName ]] autorelease ] withWindowTitle : inWindowTitle defaultButton : inDefaultButton alternateButton : inAlternateButton otherButton : inOtherButton suppression : inSuppression - ( void ) displayQuestion : ( NSString * ) inTitle withDescription : ( NSString * ) inDesc withWindowTitle : ( NSString * ) inWindowTitle defaultButton :( NSString * ) inDefaultButton alternateButton : ( NSString * ) inAlternateButton otherButton : ( NSString * ) inOtherButton suppression : ( NSString * ) inSuppression makeKey :( BOOL ) key target : ( id ) inTarget selector : ( SEL ) inSelector userInfo : ( id ) inUserInfo [ self displayQuestion : inTitle withAttributedDescription :[[[ NSAttributedString alloc ] initWithString : inDesc attributes :[ NSDictionary dictionaryWithObject : [ NSFont systemFontOfSize : 0 ] forKey : NSFontAttributeName ]] autorelease ] withWindowTitle : inWindowTitle defaultButton : inDefaultButton alternateButton : inAlternateButton otherButton : inOtherButton suppression : inSuppression //Synchronized Flashing ------------------------------------------------------------------------------------------------ #pragma mark Synchronized Flashing //Register to observe the synchronized flashing - ( void ) registerFlashObserver : ( id < AIFlashObserver > ) inObserver //Setup the timer if we don't have one yet if ( ! flashObserverArray ) { flashObserverArray = [[ NSMutableArray alloc ] init ]; flashTimer = [[ NSTimer scheduledTimerWithTimeInterval : ( 1.0 / 2.0 ) selector : @selector ( flashTimer : ) //Add the new observer to the array [ flashObserverArray addObject : inObserver ]; //Unregister from observing flashing - ( void ) unregisterFlashObserver : ( id < AIFlashObserver > ) inObserver //Remove the observer from our array [ flashObserverArray removeObject : inObserver ]; //Release the observer array and uninstall the timer if ([ flashObserverArray count ] == 0 ) { [ flashObserverArray release ]; flashObserverArray = nil ; [ flashTimer release ]; flashTimer = nil ; - ( void ) flashTimer : ( NSTimer * ) inTimer for ( id < AIFlashObserver > observer in [[ flashObserverArray copy ] autorelease ]) { [ observer flash : flashState ]; //Current state of flashing. This is an integer the increases by 1 with every flash. Mod to whatever range is desired //Tooltips ------------------------------------------------------------------------------------------------------------- //Registers code to display tooltip info about a contact - ( void ) registerContactListTooltipEntry : ( id < AIContactListTooltipEntry > ) inEntry secondaryEntry : ( BOOL ) isSecondary [ contactListTooltipSecondaryEntryArray addObject : inEntry ]; [ contactListTooltipEntryArray addObject : inEntry ]; //Unregisters code to display tooltip info about a contact - ( void ) unregisterContactListTooltipEntry : ( id < AIContactListTooltipEntry > ) inEntry secondaryEntry : ( BOOL ) isSecondary [ contactListTooltipSecondaryEntryArray removeObject : inEntry ]; [ contactListTooltipEntryArray removeObject : inEntry ]; - ( NSArray * ) contactListTooltipPrimaryEntries return contactListTooltipEntryArray ; - ( NSArray * ) contactListTooltipSecondaryEntries return contactListTooltipSecondaryEntryArray ; - ( void ) showTooltipForListObject : ( AIListObject * ) object atScreenPoint : ( NSPoint ) point onWindow : ( NSWindow * ) inWindow if ( object == tooltipListObject ) { //If we already have this tooltip open //Move the existing tooltip [ AITooltipUtilities showTooltipWithTitle : tooltipTitle imageOnRight : DISPLAY_IMAGE_ON_RIGHT orientation : TooltipBelow ]; } else { //This is a new tooltip NSMutableParagraphStyle * paragraphStyleTitle = [[ NSParagraphStyle defaultParagraphStyle ] mutableCopy ]; NSMutableParagraphStyle * paragraphStyle = [[ NSParagraphStyle defaultParagraphStyle ] mutableCopy ]; //Hold onto the new object [ tooltipListObject release ]; tooltipListObject = [ object retain ]; tooltipImage = [[ tooltipListObject userIcon ] retain ]; if ( ! tooltipImage ) tooltipImage = [[ AIServiceIcons serviceIconForObject : tooltipListObject direction : AIIconNormal ] retain ]; //Reset the maxLabelWidth for the tooltip generation //Build a tooltip string for the primary information [ tooltipTitle release ]; tooltipTitle = [[ self _tooltipTitleForObject : object ] retain ]; //If there is an image, set the title tab and indentation settings independently //Set a right-align tab at the maximum label width and a left-align just past it tabArray = [[ NSArray alloc ] initWithObjects : [[[ NSTextTab alloc ] initWithType : NSRightTabStopType location : maxLabelWidth ] autorelease ] ,[[[ NSTextTab alloc ] initWithType : NSLeftTabStopType location : maxLabelWidth + LABEL_ENTRY_SPACING ] autorelease ] [ paragraphStyleTitle setTabStops : tabArray ]; [ paragraphStyleTitle setHeadIndent : ( maxLabelWidth + LABEL_ENTRY_SPACING )]; [ tooltipTitle addAttribute : NSParagraphStyleAttributeName value : paragraphStyleTitle range : NSMakeRange ( 0 ,[ tooltipTitle length ])]; //Reset the max label width since the body will be independent //Build a tooltip string for the secondary information [ tooltipBody release ]; tooltipBody = nil ; tooltipBody = [[ self _tooltipBodyForObject : object ] retain ]; //Set a right-align tab at the maximum label width for the body and a left-align just past it tabArray = [[ NSArray alloc ] initWithObjects : [[[ NSTextTab alloc ] initWithType : NSRightTabStopType location : maxLabelWidth ] autorelease ] ,[[[ NSTextTab alloc ] initWithType : NSLeftTabStopType location : maxLabelWidth + LABEL_ENTRY_SPACING ] autorelease ] [ paragraphStyle setTabStops : tabArray ]; [ paragraphStyle setHeadIndent : ( maxLabelWidth + LABEL_ENTRY_SPACING )]; [ tooltipBody addAttribute : NSParagraphStyleAttributeName value : paragraphStyle range : NSMakeRange ( 0 ,[ tooltipBody length ])]; //If there is no image, also use these settings for the top part [ tooltipTitle addAttribute : NSParagraphStyleAttributeName value : paragraphStyle range : NSMakeRange ( 0 ,[ tooltipTitle length ])]; //Display the new tooltip [ AITooltipUtilities showTooltipWithTitle : tooltipTitle imageOnRight : DISPLAY_IMAGE_ON_RIGHT orientation : TooltipBelow ]; [ paragraphStyleTitle release ]; [ paragraphStyle release ]; //Hide the existing tooltip [ AITooltipUtilities showTooltipWithTitle : nil orientation : TooltipBelow ]; [ tooltipListObject release ]; tooltipListObject = nil ; [ tooltipTitle release ]; tooltipTitle = nil ; [ tooltipBody release ]; tooltipBody = nil ; [ tooltipImage release ]; tooltipImage = nil ; - ( NSMutableAttributedString * ) _tooltipTitleForObject : ( AIListObject * ) object NSMutableAttributedString * titleString = [[ NSMutableAttributedString alloc ] init ]; id < AIContactListTooltipEntry > tooltipEntry ; NSEnumerator * labelEnumerator ; NSMutableArray * labelArray = [ NSMutableArray array ]; NSMutableArray * entryArray = [ NSMutableArray array ]; NSMutableAttributedString * entryString ; NSString * formattedUID = object . formattedUID ; //Configure fonts and attributes NSFontManager * fontManager = [ NSFontManager sharedFontManager ]; NSFont * toolTipsFont = [ NSFont toolTipsFontOfSize : 10 ]; NSMutableDictionary * titleDict = [ NSMutableDictionary dictionaryWithObject : [ fontManager convertFont : [ NSFont toolTipsFontOfSize : 12 ] toHaveTrait : NSBoldFontMask ] forKey : NSFontAttributeName ]; NSMutableDictionary * labelDict = [ NSMutableDictionary dictionaryWithObject : [ fontManager convertFont : [ NSFont toolTipsFontOfSize : 9 ] toHaveTrait : NSBoldFontMask ] forKey : NSFontAttributeName ]; NSMutableDictionary * labelEndLineDict = [ NSMutableDictionary dictionaryWithObject : [ NSFont toolTipsFontOfSize : 2 ] forKey : NSFontAttributeName ]; NSMutableDictionary * entryDict = [ NSMutableDictionary dictionaryWithObject : toolTipsFont forKey : NSFontAttributeName ]; //Get the user's display name as an attributed string NSAttributedString * displayName = [[ NSAttributedString alloc ] initWithString : object . displayName NSAttributedString * filteredDisplayName = [ adium . contentController filterAttributedString : displayName usingFilterType : AIFilterTooltips direction : AIFilterIncoming //Append the user's display name if ( filteredDisplayName ) { [ titleString appendAttributedString : filteredDisplayName ]; //Append the user's formatted UID if there is one that's different to the display name if ( formattedUID && ( ! ([[[ displayName string ] compactedString ] isEqualToString : [ formattedUID compactedString ]]))) { [ titleString appendString : [ NSString stringWithFormat : @" (%@)" , formattedUID ] withAttributes : titleDict ]; if ([ object isKindOfClass : [ AIListGroup class ]]) { [ titleString appendString : [ NSString stringWithFormat : @" (%ld/%ld)" ,[( AIListGroup * ) object visibleCount ],[( AIListGroup * ) object countOfContainedObjects ]] withAttributes : titleDict ]; //Calculate the widest label while loading the arrays for ( tooltipEntry in contactListTooltipEntryArray ) { entryString = [[ tooltipEntry entryForObject : object ] mutableCopy ]; if ( entryString && [ entryString length ]) { NSString * labelString = [ tooltipEntry labelForObject : object ]; if ( labelString && [ labelString length ]) { [ entryArray addObject : entryString ]; [ labelArray addObject : labelString ]; NSAttributedString * labelAttribString = [[ NSAttributedString alloc ] initWithString : [ NSString stringWithFormat : @"%@:" , labelString ] //The largest size should be the label's size plus the distance to the next tab at least a space past its end labelWidth = [ labelAttribString size ]. width ; [ labelAttribString release ]; if ( labelWidth > maxLabelWidth ) maxLabelWidth = labelWidth ; //Add labels plus entires to the toolTip labelEnumerator = [ labelArray objectEnumerator ]; for ( entryString in entryArray ) { NSAttributedString * labelAttribString = [[ NSAttributedString alloc ] initWithString : [ NSString stringWithFormat : @" \t %@: \t " ,[ labelEnumerator nextObject ]] [ titleString appendString : @" \n " withAttributes : labelEndLineDict ]; [ titleString appendString : @" \n " withAttributes : labelEndLineDict ]; //Add the label (with its spacing) [ titleString appendAttributedString : labelAttribString ]; [ labelAttribString release ]; [ entryString addAttributes : entryDict range : NSMakeRange ( 0 ,[ entryString length ])]; [ titleString appendAttributedString : entryString ]; return [ titleString autorelease ]; - ( NSMutableAttributedString * ) _tooltipBodyForObject : ( AIListObject * ) object NSMutableAttributedString * tipString = [[ NSMutableAttributedString alloc ] init ]; //Configure fonts and attributes NSFontManager * fontManager = [ NSFontManager sharedFontManager ]; NSFont * toolTipsFont = [ NSFont toolTipsFontOfSize : 10 ]; NSMutableDictionary * labelDict = [ NSMutableDictionary dictionaryWithObject : [ fontManager convertFont : [ NSFont toolTipsFontOfSize : 9 ] toHaveTrait : NSBoldFontMask ] forKey : NSFontAttributeName ]; NSMutableDictionary * labelEndLineDict = [ NSMutableDictionary dictionaryWithObject : [ NSFont toolTipsFontOfSize : 1 ] forKey : NSFontAttributeName ]; NSMutableDictionary * entryDict = [ NSMutableDictionary dictionaryWithObject : toolTipsFont forKey : NSFontAttributeName ]; NSEnumerator * labelEnumerator ; NSMutableArray * labelArray = [ NSMutableArray array ]; //Array of NSStrings NSMutableArray * entryArray = [ NSMutableArray array ]; //Array of NSMutableStrings //Calculate the widest label while loading the arrays for ( id < AIContactListTooltipEntry > tooltipEntry in contactListTooltipSecondaryEntryArray ) { NSMutableAttributedString * entryString = [[ tooltipEntry entryForObject : object ] mutableCopy ]; if ( entryString && entryString . length ) { NSString * labelString = [ tooltipEntry labelForObject : object ]; if ( labelString && labelString . length ) { [ entryArray addObject : entryString ]; [ labelArray addObject : labelString ]; NSAttributedString * labelAttribString = [[ NSAttributedString alloc ] initWithString : [ NSString stringWithFormat : @"%@:" , labelString ] //The largest size should be the label's size plus the distance to the next tab at least a space past its end labelWidth = labelAttribString . size . width ; [ labelAttribString release ]; if ( labelWidth > maxLabelWidth ) maxLabelWidth = labelWidth ; //Add labels plus entires to the toolTip labelEnumerator = [ labelArray objectEnumerator ]; for ( NSMutableAttributedString * entryString in entryArray ) { NSMutableAttributedString * labelString = [[ NSMutableAttributedString alloc ] initWithString : [ NSString stringWithFormat : @" \t %@: \t " ,[ labelEnumerator nextObject ]] //Add a carriage return and skip a line [ tipString appendString : @" \n\n " withAttributes : labelEndLineDict ]; //Add the label (with its spacing) [ tipString appendAttributedString : labelString ]; NSRange fullLength = NSMakeRange ( 0 , [ entryString length ]); //remove any background coloration [ entryString removeAttribute : NSBackgroundColorAttributeName range : fullLength ]; //adjust foreground colors for the tooltip background [ entryString adjustColorsToShowOnBackground : [ NSColor colorWithCalibratedRed : 1.000f green : 1.000f blue : 0.800f alpha : 1.0f ]]; //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 if ([ entryString replaceOccurrencesOfString : @" \r " withString : @" \r\t\t " options : NSLiteralSearch range : fullLength ]) { fullLength = NSMakeRange ( 0 , [ entryString length ]); [ entryString replaceOccurrencesOfString : @" \n " withString : @" \n\t\t " options : NSLiteralSearch range : fullLength ]; //Run the entry through the filters and add it to tipString entryString = [[ adium . contentController filterAttributedString : entryString usingFilterType : AIFilterTooltips direction : AIFilterIncoming context : object ] mutableCopy ]; [ entryString addAttributes : entryDict range : NSMakeRange ( 0 ,[ entryString length ])]; [ tipString appendAttributedString : entryString ]; return [ tipString autorelease ]; //Custom pasting ---------------------------------------------------------------------------------------------------- #pragma mark Custom Pasting //Paste, stripping formatting - ( IBAction ) paste : ( id ) sender [ self _pasteWithPreferredSelector : @selector ( pasteAsPlainTextWithTraits : ) sender : sender ]; - ( IBAction ) pasteAndMatchStyle : ( id ) sender [ self _pasteWithPreferredSelector : @selector ( pasteAsPlainText : ) sender : sender ]; - ( IBAction ) pasteWithImagesAndColors : ( id ) sender [ self _pasteWithPreferredSelector : @selector ( pasteAsRichText : ) sender : sender ]; * @brief Send a paste message, using preferredSelector if possible and paste: if not * Walks the responder chain looking for a responder which can handle pasting, skipping instances of * WebHTMLView. These are skipped because we can control what paste does to WebView (by using a custom subclass) but * have no control over what the WebHTMLView would do. * If no responder is found, repeats the process looking for the simpler paste: selector. - ( void ) _pasteWithPreferredSelector : ( SEL ) selector sender : ( id ) sender NSWindow * keyWin = [[ NSApplication sharedApplication ] keyWindow ]; //First, look for a responder which can handle the preferred selector if ( ! ( responder = [ keyWin earliestResponderWhichRespondsToSelector : selector andIsNotOfClass : NSClassFromString ( @"WebHTMLView" )])) { //No responder found. Try again, looking for one which will respond to paste: selector = @selector ( paste : ); responder = [ keyWin earliestResponderWhichRespondsToSelector : selector andIsNotOfClass : NSClassFromString ( @"WebHTMLView" )]; //Sending pasteAsRichText: to a non rich text NSTextView won't do anything; change it to a generic paste: if ([ responder isKindOfClass : [ NSTextView class ]] && ! [( NSTextView * ) responder isRichText ]) { selector = @selector ( paste : ); [ keyWin makeFirstResponder : responder ]; [ responder performSelector : selector //Custom Printing ------------------------------------------------------------------------------------------------------ #pragma mark Custom Printing - ( IBAction ) adiumPrint : ( id ) sender //Pass the print command to the window, which is responsible for routing it to the correct place or //creating a view and printing. Adium will not print from a window that does not respond to adiumPrint: NSWindow * keyWindowController = [[[ NSApplication sharedApplication ] keyWindow ] windowController ]; if ([ keyWindowController respondsToSelector : @selector ( adiumPrint : )]) { [ keyWindowController performSelector : @selector ( adiumPrint : ) #pragma mark Preferences Display - ( IBAction ) showPreferenceWindow : ( id ) sender [ adium . preferenceController showPreferenceWindow : sender ]; - ( IBAction ) toggleFontPanel : ( id ) sender if ([ NSFontPanel sharedFontPanelExists ] && [[ NSFontPanel sharedFontPanel ] isVisible ]) { [[ NSFontPanel sharedFontPanel ] close ]; NSFontPanel * fontPanel = [ NSFontPanel sharedFontPanel ]; if ( ! fontPanelAccessoryView ) { [ NSBundle loadNibNamed : @"FontPanelAccessoryView" owner : self ]; [ fontPanel setAccessoryView : fontPanelAccessoryView ]; [ button_fontPanelSetAsDefault setLocalizedString : AILocalizedString ( @"Save This Setting As My Default Font" , "Appears in the Format > Show Fonts window. You are limited for horizontal space, so try to keep it at most the length of the English string." )]; [ fontPanel orderFront : self ]; - ( IBAction ) setFontPanelSettingsAsDefaultFont : ( id ) sender NSFont * selectedFont = [[ NSFontManager sharedFontManager ] selectedFont ]; [ adium . preferenceController setPreference : [ selectedFont stringRepresentation ] forKey : KEY_FORMATTING_FONT group : PREF_GROUP_FORMATTING ]; //We can't get foreground/background color from the font panel so far as I can tell... so we do the best we can. NSWindow * keyWin = [[ NSApplication sharedApplication ] keyWindow ]; NSResponder * responder = [ keyWin firstResponder ]; if ([ responder isKindOfClass : [ NSTextView class ]]) { NSDictionary * typingAttributes = [( NSTextView * ) responder typingAttributes ]; NSColor * foregroundColor , * backgroundColor ; if (( foregroundColor = [ typingAttributes objectForKey : NSForegroundColorAttributeName ])) { [ adium . preferenceController setPreference : [ foregroundColor stringRepresentation ] forKey : KEY_FORMATTING_TEXT_COLOR group : PREF_GROUP_FORMATTING ]; if (( backgroundColor = [ typingAttributes objectForKey : AIBodyColorAttributeName ])) { [ adium . preferenceController setPreference : [ backgroundColor stringRepresentation ] forKey : KEY_FORMATTING_BACKGROUND_COLOR group : PREF_GROUP_FORMATTING ]; //Custom Dimming menu items -------------------------------------------------------------------------------------------- #pragma mark Custom Dimming menu items //The standard ones do not dim correctly when unavailable - ( IBAction ) toggleFontTrait : ( id ) sender NSFontManager * fontManager = [ NSFontManager sharedFontManager ]; if ([ fontManager traitsOfFont : [ fontManager selectedFont ]] & [ sender tag ]) { [ fontManager removeFontTrait : sender ]; [ fontManager addFontTrait : sender ]; - ( void ) toggleToolbarShown : ( id ) sender NSWindow * window = [[ NSApplication sharedApplication ] keyWindow ]; [ window toggleToolbarShown : sender ]; - ( void ) runToolbarCustomizationPalette : ( id ) sender NSWindow * window = [[ NSApplication sharedApplication ] keyWindow ]; [ window runToolbarCustomizationPalette : sender ]; - ( BOOL ) validateMenuItem : ( NSMenuItem * ) menuItem NSWindow * keyWin = [[ NSApplication sharedApplication ] keyWindow ]; NSResponder * responder = [ keyWin firstResponder ]; if ( menuItem == menuItem_bold || menuItem == menuItem_italic ) { NSFont * selectedFont = [[ NSFontManager sharedFontManager ] selectedFont ]; //We must be in a text view, have text on the pasteboard, and have a font that supports bold or italic if ([ responder isKindOfClass : [ NSTextView class ]]) { return ( menuItem == menuItem_bold ? [ selectedFont supportsBold ] : [ selectedFont supportsItalics ]); } else if ( menuItem == menuItem_paste || menuItem == menuItem_pasteAndMatchStyle || menuItem == menuItem_pasteWithImagesAndColors ) { //The user can paste if the pasteboard contains an image, some text, one or more files, or one or more URLs. NSPasteboard * pboard = [ NSPasteboard generalPasteboard ]; NSArray * nonImageTypes = [ NSArray arrayWithObjects : NSFilesPromisePboardType , return ([ pboard availableTypeFromArray : nonImageTypes ] != nil ) || [ NSImage canInitWithPasteboard : pboard ]; } else if ( menuItem == menuItem_showToolbar ) { [ menuItem_showToolbar setTitle : ([[ keyWin toolbar ] isVisible ] ? AILocalizedString ( @"Hide Toolbar" , nil ) : AILocalizedString ( @"Show Toolbar" , nil ))]; return [ keyWin toolbar ] != nil ; } else if ( menuItem == menuItem_customizeToolbar ) { return ([ keyWin toolbar ] != nil && [[ keyWin toolbar ] isVisible ] && [[ keyWin windowController ] canCustomizeToolbar ]); } else if ( menuItem == menuItem_close ) { return ( keyWin && ([[ keyWin standardWindowButton : NSWindowCloseButton ] isEnabled ] || ([[ keyWin windowController ] respondsToSelector : @selector ( windowPermitsClose )] && [[ keyWin windowController ] windowPermitsClose ]))); } else if ( menuItem == menuItem_closeChat || menuItem == menuItem_clearDisplay ) { return activeChat != nil ; } else if ( menuItem == menuItem_closeAllChats ) { return [[ self openChats ] count ] > 0 ; } else if ( menuItem == menuItem_print ) { NSWindowController * windowController = [ keyWin windowController ]; return ([ windowController respondsToSelector : @selector ( adiumPrint : )] && ( ! [ windowController respondsToSelector : @selector ( validatePrintMenuItem : )] || [ windowController validatePrintMenuItem : menuItem ])); } else if ( menuItem == menuItem_showFonts ) { [ menuItem_showFonts setTitle : (([ NSFontPanel sharedFontPanelExists ] && [[ NSFontPanel sharedFontPanel ] isVisible ]) ? AILocalizedString ( @"Hide Fonts" , nil ) : AILocalizedString ( @"Show Fonts" , nil ))]; } else if ( menuItem == menuItem_toggleUserlist || menuItem == menuItem_toggleUserlistSide ) { return self . activeChat . isGroupChat ; } else if ( menuItem == menuItem_reopenTab ) { return recentlyClosedChats . count > 0 ; #pragma mark Window levels - ( NSMenu * ) menuForWindowLevelsNotifyingTarget : ( id ) target NSMenu * windowPositionMenu = [[ NSMenu allocWithZone : [ NSMenu zone ]] init ]; menuItem = [[ NSMenuItem allocWithZone : [ NSMenu menuZone ]] initWithTitle : AILocalizedString ( @"Above other windows" , nil ) action : @selector ( selectedWindowLevel : ) [ menuItem setEnabled : YES ]; [ menuItem setTag : AIFloatingWindowLevel ]; [ windowPositionMenu addItem : menuItem ]; menuItem = [[ NSMenuItem allocWithZone : [ NSMenu menuZone ]] initWithTitle : AILocalizedString ( @"Normally" , nil ) action : @selector ( selectedWindowLevel : ) [ menuItem setEnabled : YES ]; [ menuItem setTag : AINormalWindowLevel ]; [ windowPositionMenu addItem : menuItem ]; menuItem = [[ NSMenuItem allocWithZone : [ NSMenu menuZone ]] initWithTitle : AILocalizedString ( @"Below other windows" , nil ) action : @selector ( selectedWindowLevel : ) [ menuItem setEnabled : YES ]; [ menuItem setTag : AIDesktopWindowLevel ]; [ windowPositionMenu addItem : menuItem ]; [ windowPositionMenu setAutoenablesItems : NO ]; return [ windowPositionMenu autorelease ]; - ( void ) toggleUserlist : ( id ) sender [ self . activeChat . chatContainer . chatViewController toggleUserList ]; - ( void ) toggleUserlistSide : ( id ) sender [ self . activeChat . chatContainer . chatViewController toggleUserListSide ]; - ( void ) clearDisplay : ( id ) sender [ self . activeChat . chatContainer . messageViewController . messageDisplayController clearView ];