Source/AIInterfaceController.m
author Zachary West <zacw@adium.im>
Fri Oct 30 21:28:39 2009 -0400 (2009-10-30)
changeset 2693 5a28417276f5
parent 2131 0846c7e2b617
child 2736 d25c420e8e66
permissions -rw-r--r--
Grab the "Authorization Requests" localized string from the right bundle. Refs #13275.
     1 /* 
     2  * Adium is the legal property of its developers, whose names are listed in the copyright file included
     3  * with this source distribution.
     4  * 
     5  * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
     6  * General Public License as published by the Free Software Foundation; either version 2 of the License,
     7  * or (at your option) any later version.
     8  * 
     9  * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
    10  * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
    11  * Public License for more details.
    12  * 
    13  * You should have received a copy of the GNU General Public License along with this program; if not,
    14  * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
    15  */
    16 
    17 // $Id$
    18 
    19 #import "AIInterfaceController.h"
    20 
    21 #import <Adium/AIAccountControllerProtocol.h>
    22 #import <Adium/AIContactControllerProtocol.h>
    23 #import <Adium/AIChatControllerProtocol.h>
    24 #import <Adium/AIContentControllerProtocol.h>
    25 #import <Adium/AIMenuControllerProtocol.h>
    26 #import <Adium/AIAuthorizationRequestsWindowController.h>
    27 #import <AIUtilities/AIAttributedStringAdditions.h>
    28 #import <AIUtilities/AIColorAdditions.h>
    29 #import <AIUtilities/AIFontAdditions.h>
    30 #import <AIUtilities/AIImageDrawingAdditions.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 "AIMessageTabViewItem.h"
    46 #import "KNShelfSplitview.h"
    47 #import <Adium/AIContactList.h>
    48 
    49 #import "AIMessageViewController.h"
    50 
    51 #define ERROR_MESSAGE_WINDOW_TITLE		AILocalizedString(@"Adium : Error","Error message window title")
    52 #define LABEL_ENTRY_SPACING				4.0
    53 #define DISPLAY_IMAGE_ON_RIGHT			NO
    54 
    55 #define PREF_GROUP_FORMATTING			@"Formatting"
    56 #define KEY_FORMATTING_FONT				@"Default Font"
    57 
    58 #define MESSAGES_WINDOW_MENU_TITLE		AILocalizedString(@"Chats","Title for the messages window menu item")
    59 
    60 //#define	LOG_RESPONDER_CHAIN
    61 
    62 @interface AIInterfaceController ()
    63 - (void)_resetOpenChatsCache;
    64 - (void)_addItemToMainMenuAndDock:(NSMenuItem *)item;
    65 - (NSAttributedString *)_tooltipTitleForObject:(AIListObject *)object;
    66 - (NSAttributedString *)_tooltipBodyForObject:(AIListObject *)object;
    67 - (void)_pasteWithPreferredSelector:(SEL)preferredSelector sender:(id)sender;
    68 - (void)updateCloseMenuKeys;
    69 
    70 - (void)saveContainers;
    71 - (void)restoreSavedContainers;
    72 
    73 //Window Menu
    74 - (void)updateActiveWindowMenuItem;
    75 - (void)buildWindowMenu;
    76 
    77 - (AIChat *)mostRecentActiveChat;
    78 @end
    79 
    80 /*!
    81  * @class AIInterfaceController
    82  * @brief Interface controller
    83  *
    84  * Chat window related requests, such as opening and closing chats, are routed through the interface controller
    85  * to the appropriate component. The interface controller keeps track of the most recently active chat, handles chat
    86  * cycling (switching between chats), chat sorting, and so on.  The interface controller also handles switching to
    87  * an appropriate window or chat when the dock icon is clicked for a 'reopen' event.
    88  *
    89  * Contact list window requests, such as toggling window visibilty are routed to the contact list controller component.
    90  *
    91  * Error messages are routed through the interface controller.
    92  *
    93  * Tooltips, such as seen on hover in the contact list are generated and displayed here.  Tooltip display components and
    94  * plugins register with the interface controller to be queried for contact information when a tooltip is displayed.
    95  *
    96  * When displays in Adium flash, such as in the dock or the contact list for unviewed content, the interface controller
    97  * manages keeping the flashing synchronized.
    98  *
    99  * Finally, the interface controller manages many menu items, providing better menu item validation and target routing
   100  * than the responder chain alone would do.
   101  */
   102 @implementation AIInterfaceController
   103 
   104 - (id)init
   105 {
   106 	if ((self = [super init])) {
   107 		contactListViewArray = [[NSMutableArray alloc] init];
   108 		messageViewArray = [[NSMutableArray alloc] init];
   109 		contactListTooltipEntryArray = [[NSMutableArray alloc] init];
   110 		contactListTooltipSecondaryEntryArray = [[NSMutableArray alloc] init];
   111 		closeMenuConfiguredForChat = NO;
   112 		_cachedOpenChats = nil;
   113 		mostRecentActiveChat = nil;
   114 		activeChat = nil;
   115 		
   116 		tooltipListObject = nil;
   117 		tooltipTitle = nil;
   118 		tooltipBody = nil;
   119 		tooltipImage = nil;
   120 		flashObserverArray = nil;
   121 		flashTimer = nil;
   122 		flashState = 0;
   123 		
   124 		windowMenuArray = nil;
   125 		
   126 #ifdef LOG_RESPONDER_CHAIN
   127 		[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(reportResponderChain:) userInfo:nil repeats:YES];
   128 #endif
   129 	}
   130 	
   131 	return self;
   132 }
   133 
   134 #ifdef LOG_RESPONDER_CHAIN
   135 //Can be called by a timer to periodically log the responder chain
   136 //[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(reportResponderChain:) userInfo:nil repeats:YES];
   137 - (void)reportResponderChain:(NSTimer *)inTimer
   138 {
   139 	NSMutableString	*responderChain = [NSMutableString string];
   140 	
   141 	NSWindow	*keyWindow = [[NSApplication sharedApplication] keyWindow];
   142 #warning 64BIT: Check formatting arguments
   143 	[responderChain appendFormat:@"%@ (%i): ",keyWindow,[keyWindow respondsToSelector:@selector(print:)]];
   144 	
   145 	NSResponder	*responder = [keyWindow firstResponder];
   146 	
   147 	//First, walk down the responder chain looking for a responder which can handle the preferred selector
   148 	while (responder) {
   149 #warning 64BIT: Check formatting arguments
   150 		[responderChain appendFormat:@"%@ (%i)",responder,[responder respondsToSelector:@selector(print:)]];
   151 		responder = [responder nextResponder];
   152 		if (responder) [responderChain appendString:@" -> "];
   153 	}
   154 
   155 	NSLog(responderChain);
   156 }
   157 #endif
   158 
   159 - (void)controllerDidLoad
   160 {
   161     //Load the interface
   162     [interfacePlugin openInterface];
   163 
   164 	//Open the contact list window
   165     [self showContactList:nil];
   166 	
   167 	//Userlist show/hide item
   168 	menuItem_toggleUserlist = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Toggle User List", nil)
   169 																							 target:self
   170 																							 action:@selector(toggleUserlist:)
   171 																					  keyEquivalent:@"/"];
   172 	[menuItem_toggleUserlist setKeyEquivalentModifierMask:(NSCommandKeyMask | NSAlternateKeyMask)];
   173 	
   174 	[adium.menuController addMenuItem:menuItem_toggleUserlist toLocation:LOC_Display_General];
   175 	
   176 	menuItem_toggleUserlistSide = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Toggle User List Side", nil)
   177 																				   target:self
   178 																				   action:@selector(toggleUserlistSide:)
   179 																			keyEquivalent:@""];
   180 	
   181 	[adium.menuController addMenuItem:menuItem_toggleUserlistSide toLocation:LOC_Display_General];
   182 
   183 	NSMenuItem *menuItem = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Toggle User List", nil)
   184 																				target:self
   185 																				action:@selector(toggleUserlist:)
   186 																		 keyEquivalent:@""] autorelease];
   187 	
   188 	[adium.menuController addContextualMenuItem:menuItem toLocation:Context_GroupChat_Action];
   189 	
   190 	// Clear display
   191 	menuItem_clearDisplay = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Clear Display", nil)
   192 																				 target:self
   193 																				 action:@selector(clearDisplay:)
   194 																		  keyEquivalent:@""];
   195 	[adium.menuController addMenuItem:menuItem_clearDisplay toLocation:LOC_Display_MessageControl];
   196 																			  
   197 	//Contact list menu item
   198 	menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Contact List","Name of the window which lists contacts")
   199 																				target:self
   200 																				action:@selector(toggleContactList:)
   201 																		 keyEquivalent:@"/"];
   202 	[adium.menuController addMenuItem:menuItem toLocation:LOC_Window_Fixed];
   203 	[adium.menuController addMenuItem:[[menuItem copy] autorelease] toLocation:LOC_Dock_Status];
   204 	[menuItem release];
   205 	
   206 	menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Close Chat","Title for the close chat menu item")
   207 																	target:self
   208 																	action:@selector(closeContextualChat:)
   209 															 keyEquivalent:@""];
   210 	[adium.menuController addContextualMenuItem:menuItem toLocation:Context_Tab_Action];
   211 	[menuItem release];
   212 	
   213 	// Authorization requests menu item
   214 	menuItem = [[NSMenuItem alloc] initWithTitle:AILocalizedStringFromTableInBundle(@"Authorization Requests",nil, [NSBundle bundleForClass:[AIAuthorizationRequestsWindowController class]], nil)
   215 										  target:self
   216 										  action:@selector(openAuthorizationWindow:)
   217 								   keyEquivalent:@""];
   218 	
   219 	[adium.menuController addMenuItem:menuItem toLocation:LOC_Window_Auxiliary];
   220 	[menuItem release];
   221 
   222     //Observe content so we can open chats as necessary
   223     [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveContent:) 
   224 									   name:CONTENT_MESSAGE_RECEIVED object:nil];
   225     [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveContent:) 
   226 									   name:CONTENT_MESSAGE_RECEIVED_GROUP object:nil];
   227 	
   228 	//Observe Adium finishing loading so we can do things which may require other components or plugins
   229 	[[NSNotificationCenter defaultCenter] addObserver:self
   230 								   selector:@selector(adiumDidFinishLoading:)
   231 									   name:AIApplicationDidFinishLoadingNotification
   232 									 object:nil];
   233 	
   234 	//Observe quits so we can save containers.
   235 	[[NSNotificationCenter defaultCenter] addObserver:self
   236 								   selector:@selector(saveContainersOnQuit:)
   237 									   name:AIAppWillTerminateNotification
   238 									 object:nil];
   239 }
   240 
   241 - (void)controllerWillClose
   242 {
   243     [contactListPlugin closeContactList];
   244     [interfacePlugin closeInterface];
   245 }
   246 
   247 // Dealloc
   248 - (void)dealloc
   249 {
   250     [contactListViewArray release]; contactListViewArray = nil;
   251     [messageViewArray release]; messageViewArray = nil;
   252     [interfaceArray release]; interfaceArray = nil;
   253 	
   254     [tooltipListObject release]; tooltipListObject = nil;
   255 	[tooltipTitle release]; tooltipTitle = nil;
   256 	[tooltipBody release]; tooltipBody = nil;
   257 	[tooltipImage release]; tooltipImage = nil;
   258 	
   259 	[[NSNotificationCenter defaultCenter] removeObserver:self];
   260 	[adium.preferenceController unregisterPreferenceObserver:self];
   261 	
   262     [super dealloc];
   263 }
   264 
   265 - (void)adiumDidFinishLoading:(NSNotification *)inNotification
   266 {
   267 	//Observe preference changes. This will also restore saved containers if appropriate.
   268 	[adium.preferenceController registerPreferenceObserver:self forGroup:PREF_GROUP_INTERFACE];
   269 	
   270 	[[NSNotificationCenter defaultCenter] removeObserver:self
   271 										  name:AIApplicationDidFinishLoadingNotification
   272 										object:nil];
   273 }
   274 
   275 //Registers code to handle the interface
   276 - (void)registerInterfaceController:(id <AIInterfaceComponent>)inController
   277 {
   278 	if (!interfacePlugin) interfacePlugin = [inController retain];
   279 }
   280 
   281 //Register code to handle the contact list
   282 - (void)registerContactListController:(id <AIContactListComponent>)inController
   283 {
   284 	if (!contactListPlugin) contactListPlugin = [inController retain];
   285 }
   286 
   287 //Preferences changed
   288 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key
   289 							object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
   290 {
   291 	if (!object) {
   292 		//Update prefs
   293 		tabbedChatting = [[prefDict objectForKey:KEY_TABBED_CHATTING] boolValue];
   294 		groupChatsByContactGroup = [[prefDict objectForKey:KEY_GROUP_CHATS_BY_GROUP] boolValue];
   295 		saveContainers = [[prefDict objectForKey:KEY_SAVE_CONTAINERS] boolValue];
   296 	
   297 		if (firstTime) {
   298 			if (saveContainers) {
   299 				//Restore saved containers
   300 				[self restoreSavedContainers];	
   301 			} else if ([prefDict objectForKey:KEY_CONTAINERS]) {
   302 				/* We've loaded without wanting to save containers; clear any saved
   303 				 * from a previous session.
   304 				 */
   305 				[adium.preferenceController setPreference:nil
   306 													 forKey:KEY_CONTAINERS
   307 													  group:PREF_GROUP_INTERFACE];
   308 			}
   309 		}
   310 	}
   311 }
   312 
   313 //Handle a reopen/dock icon click
   314 - (BOOL)handleReopenWithVisibleWindows:(BOOL)visibleWindows
   315 {
   316 	if (![self contactListIsVisibleAndMain] && [[interfacePlugin openContainerIDs] count] == 0) {
   317 		//The contact list is not visible, and there are no chat windows. Make the contact list visible.
   318 		[self showContactList:nil];
   319 
   320 	} else {
   321 		AIChat	*mostRecentUnviewedChat;
   322 
   323 		//If windows are open, try switching to a chat with unviewed content
   324 		if ((mostRecentUnviewedChat = [adium.chatController mostRecentUnviewedChat])) {
   325 			if ([mostRecentActiveChat unviewedContentCount]) {
   326 				//If the most recently active chat has unviewed content, ensure it is in the front
   327 				[self setActiveChat:mostRecentActiveChat];
   328 			} else {
   329 				//Otherwise, switch to the chat which most recently received content
   330 				[self setActiveChat:mostRecentUnviewedChat];
   331 			}
   332 
   333 		} else {
   334 			NSWindow *targetWindow = nil;
   335 			BOOL	 unMinimizedWindows = 0;
   336 			
   337 			//If there was no unviewed content, ensure that atleast one of Adium's windows is unminimized
   338 			for (NSWindow *window in [NSApp windows]) {
   339 				//Check stylemask to rule out the system menu's window (Which reports itself as visible like a real window)
   340 				if (([window styleMask] & (NSTitledWindowMask | NSClosableWindowMask | NSMiniaturizableWindowMask))) {
   341 					if (!targetWindow) targetWindow = window;
   342 					if (![window isMiniaturized]) unMinimizedWindows++;
   343 				}
   344 			}
   345 			
   346 			//If there are no unminimized windows, unminimize the last one
   347 			if (unMinimizedWindows == 0 && targetWindow) {
   348 				[targetWindow deminiaturize:nil];
   349 			}
   350 		}
   351 	}
   352 
   353 	return YES; 
   354 }
   355 
   356 //Contact List ---------------------------------------------------------------------------------------------------------
   357 #pragma mark Contact list
   358 /*!
   359  * @brief Toggles contact list between visible and hiden
   360  */
   361 - (IBAction)toggleContactList:(id)sender
   362 {
   363     if ([self contactListIsVisibleAndMain]) {
   364 		[self closeContactList:nil];
   365     } else {
   366 		[[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
   367 		[self showContactList:nil];
   368     } 
   369 }
   370 
   371 /*!
   372  * @brief Brings contact list to the front
   373  */
   374 - (IBAction)showContactList:(id)sender
   375 {
   376 	[contactListPlugin showContactListAndBringToFront:YES];
   377 }
   378 
   379 /*!
   380  * @brief Show the contact list window and bring Adium to the front
   381  */
   382 - (IBAction)showContactListAndBringToFront:(id)sender
   383 {
   384 	[contactListPlugin showContactListAndBringToFront:YES];
   385 	[[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
   386 }
   387 
   388 /*!
   389  * @brief Close the contact list window
   390  */
   391 - (IBAction)closeContactList:(id)sender
   392 {
   393 	[contactListPlugin closeContactList];
   394 }
   395 
   396 /*!
   397  * @returns YES if contact list is visible and selected, otherwise NO
   398  */
   399 - (BOOL)contactListIsVisibleAndMain
   400 {
   401 	return [contactListPlugin contactListIsVisibleAndMain];
   402 }
   403 
   404 /*!
   405 * @returns YES if contact list is visible, otherwise NO
   406  */
   407 - (BOOL)contactListIsVisible
   408 {
   409 	return [contactListPlugin contactListIsVisible];
   410 }
   411 
   412 //Detachable Contact List ----------------------------------------------------------------------------------------------
   413 #pragma mark Detachable Contact List
   414 
   415 /*!
   416  * @returns Created contact list controller for detached contact list
   417  */
   418 - (AIListWindowController *)detachContactList:(AIContactList *)aContactList
   419 {
   420 	return [contactListPlugin detachContactList:aContactList];
   421 }
   422 
   423 
   424 #pragma mark Container Saving
   425 /*!
   426  * @brief Restores containers saved from a previous session
   427  */
   428 - (void)restoreSavedContainers
   429 {
   430 	NSData				*savedData = [adium.preferenceController preferenceForKey:KEY_CONTAINERS
   431 																	group:PREF_GROUP_INTERFACE];
   432 	
   433 	// If there's no data, we can't restore anything.
   434 	if (!savedData)
   435 		return;
   436 
   437 	for (NSDictionary *dict in [NSKeyedUnarchiver unarchiveObjectWithData:savedData]) {
   438 		AIMessageWindowController *windowController = [self openContainerWithID:[dict objectForKey:@"ID"]
   439 																 name:[dict objectForKey:@"Name"]];
   440 		AIChat *containerActiveChat = nil;
   441 		
   442 		// Position the container where it was last saved (using -savedFrameFromString: to prevent going offscreen)
   443 		[[windowController window] setFrame:[windowController savedFrameFromString:[dict objectForKey:@"Frame"]] display:YES];
   444 		
   445 		for (NSDictionary *chatDict in [dict objectForKey:@"Content"]) {
   446 			AIChat			*chat = nil;
   447 			AIService		*service = [adium.accountController firstServiceWithServiceID:[chatDict objectForKey:@"serviceID"]];
   448 			AIAccount		*account = [adium.accountController accountWithInternalObjectID:[chatDict objectForKey:@"AccountID"]];
   449 					
   450 			if ([[chatDict objectForKey:@"IsGroupChat"] boolValue]) {
   451 				chat = [adium.chatController chatWithName:[chatDict objectForKey:@"Name"]
   452 												 identifier:nil
   453 												  onAccount:account
   454 										   chatCreationInfo:[chatDict objectForKey:@"ChatCreationInfo"]];
   455 			} else {
   456 				AIListContact		*contact = [adium.contactController contactWithService:service
   457 																					account:account
   458 																						UID:[chatDict objectForKey:@"UID"]];
   459 				
   460 				chat = [adium.chatController chatWithContact:contact];
   461 			}
   462 			
   463 			// Tag the chat as restored.
   464 			[chat setValue:[NSNumber numberWithBool:YES]
   465 			   forProperty:@"Restored Chat"
   466 					notify:NotifyNow];
   467 			
   468 			if ([[chatDict objectForKey:@"ActiveChat"] boolValue]) {
   469 				containerActiveChat = chat;
   470 			}
   471 					
   472 			// Open the chat into the container we've created above.
   473 			[self openChat:chat inContainerWithID:[dict objectForKey:@"ID"] atIndex:-1];
   474 		}
   475 		
   476 		if (containerActiveChat)
   477 			[self setActiveChat:containerActiveChat];
   478 	}
   479 }
   480 
   481 /*!
   482  * @brief Saves open container information with their content when Adium quits
   483  */
   484 - (void)saveContainersOnQuit:(NSNotification *)notification
   485 {
   486 	[self saveContainers];
   487 }
   488 
   489 /*!
   490  * @brief Save opened containers and windows
   491  *
   492  * @param withContent Save the current buffer of the window to restore at a later point
   493  */
   494 - (void)saveContainers
   495 {
   496 	if (!saveContainers) {
   497 		// Don't save anything if we're not set to.
   498 		return;
   499 	}
   500 
   501 	// Save active containers.
   502 	NSMutableArray		*savedContainers = [NSMutableArray array];
   503 	
   504 	for (NSDictionary *dict in [interfacePlugin openContainersAndChats]) {
   505 		NSMutableArray		*containerContents = [NSMutableArray array];
   506 		
   507 		for (AIChat *chat in [dict objectForKey:@"Content"]) {
   508 			NSMutableDictionary		*newContainerDict = [NSMutableDictionary dictionary];
   509 
   510 			[newContainerDict setObject:chat.account.internalObjectID forKey:@"AccountID"];
   511 			
   512 			// Save chat-specific information.
   513 			if (chat.isGroupChat) {
   514 				// -chatCreationDictionary may be nil, so put it last.
   515 				[newContainerDict addEntriesFromDictionary:[NSDictionary dictionaryWithObjectsAndKeys:
   516 															[NSNumber numberWithBool:YES], @"IsGroupChat",
   517 															[NSNumber numberWithBool:([dict objectForKey:@"ActiveChat"] == chat)], @"ActiveChat",
   518 															chat.name, @"Name",
   519 															[chat chatCreationDictionary], @"ChatCreationInfo",nil]];
   520 			} else {
   521 				[newContainerDict addEntriesFromDictionary:[NSDictionary dictionaryWithObjectsAndKeys:
   522 															[NSNumber numberWithBool:([dict objectForKey:@"ActiveChat"] == chat)], @"ActiveChat",
   523 															chat.listObject.UID, @"UID",
   524 															chat.account.service.serviceID, @"serviceID",
   525 															chat.account.internalObjectID, @"AccountID",nil]];
   526 			}
   527 					
   528 			[containerContents addObject:newContainerDict];
   529 		}
   530 		
   531 		// Replace the "Content" key in -openContainersAndChats with our version of the content.
   532 		// Remove the ActiveChat reference
   533 		// We use the same keys otherwise that -openContainersAndChats provides (Name, ID, Frame)
   534 		NSMutableDictionary *saveDict = [[dict mutableCopy] autorelease];
   535 
   536 		[saveDict removeObjectForKey:@"ActiveChat"];
   537 		
   538 		[saveDict setObject:containerContents
   539 					 forKey:@"Content"];
   540 		
   541 		[savedContainers addObject:saveDict];
   542 	}
   543 	
   544 	[adium.preferenceController setPreference:[NSKeyedArchiver archivedDataWithRootObject:savedContainers]
   545 										 forKey:KEY_CONTAINERS
   546 										  group:PREF_GROUP_INTERFACE];
   547 }
   548 
   549 //Messaging ------------------------------------------------------------------------------------------------------------
   550 //Methods for instructing the interface to provide a representation of chats, and to determine which chat has user focus
   551 #pragma mark Messaging
   552 
   553 /*!
   554  * @brief Opens window for chat
   555  */
   556 - (void)openChat:(AIChat *)inChat
   557 {
   558 	NSArray		*containerIDs = [interfacePlugin openContainerIDs];
   559 	NSString	*containerID = nil;
   560 	NSString	*containerName = nil;
   561 	
   562 	//Determine the correct container for this chat
   563 	
   564 	if (!tabbedChatting) {
   565 		//We're not using tabs; each chat starts in its own container, based on the destination object or the chat name
   566 		if ([inChat listObject]) {
   567 			containerID = inChat.listObject.internalObjectID;
   568 		} else {
   569 			containerID = inChat.name;
   570 		}
   571 		
   572 	} else if (groupChatsByContactGroup) {
   573 		if (inChat.isGroupChat) {
   574 			containerID = AILocalizedString(@"Group Chats",nil);
   575 			
   576 		} else {
   577 			//XXX multiple containers: this is "correct" but maybe not desirable, as it is non-deterministic
   578 			AIListGroup	*group = inChat.listObject.parentContact.groups.anyObject;
   579 			
   580 			//If the contact is in the contact list root, we don't have a group
   581 			if (group && ![group isKindOfClass:[AIContactList class]]) {
   582 				containerID = group.displayName;
   583 			}
   584 		}
   585 		
   586 		containerName = containerID;
   587 	}
   588 	
   589 	if (!containerID) {
   590 		//Open new chats into the first container (if not available, create a new one)
   591 		if ([containerIDs count] > 0) {
   592 			containerID = [containerIDs objectAtIndex:0];
   593 		} else {
   594 			containerID = nil;
   595 		}
   596 	}
   597 
   598 	//Determine the correct placement for this chat within the container
   599 	[interfacePlugin openChat:inChat inContainerWithID:containerID withName:containerName atIndex:-1];
   600 	if (![inChat isOpen]) {
   601 		[inChat setIsOpen:YES];
   602 		
   603 		//Post the notification last, so observers receive a chat whose isOpen flag is yes.
   604 		[[NSNotificationCenter defaultCenter] postNotificationName:Chat_DidOpen object:inChat userInfo:nil];
   605 	}
   606 }
   607 
   608 - (id)openChat:(AIChat *)inChat inContainerWithID:(NSString *)containerID atIndex:(NSUInteger)index
   609 {	
   610 	NSArray		*openContainerIDs = [interfacePlugin openContainerIDs];
   611 
   612 	if (!containerID) {
   613 		//Open new chats into the first container (if not available, create a new one)
   614 		if ([openContainerIDs count] > 0) {
   615 			containerID = [openContainerIDs objectAtIndex:0];
   616 		} else {
   617 			containerID = AILocalizedString(@"Chats",nil);
   618 		}
   619 	}
   620 
   621 	//Determine the correct placement for this chat within the container
   622 	id tabViewItem = [interfacePlugin openChat:inChat inContainerWithID:containerID withName:nil atIndex:index];
   623 	if (![inChat isOpen]) {
   624 		[inChat setIsOpen:YES];
   625 		
   626 		//Post the notification last, so observers receive a chat whose isOpen flag is yes.
   627 		[[NSNotificationCenter defaultCenter] postNotificationName:Chat_DidOpen object:inChat userInfo:nil];
   628 	}
   629 	return tabViewItem;
   630 }
   631 
   632 /**
   633  * @brief Opens a container with a specific ID
   634  *
   635  * Asks the interfacePlugin to openContainerWithID:
   636  */
   637 - (AIMessageWindowController *)openContainerWithID:(NSString *)containerID name:(NSString *)containerName
   638 {
   639 	return [interfacePlugin openContainerWithID:containerID name:containerName];
   640 }
   641 
   642 /*!
   643  * @brief Close the interface for a chat
   644  *
   645  * Tell the interface plugin to close the chat.
   646  */
   647 - (void)closeChat:(AIChat *)inChat
   648 {
   649 	if (inChat) {
   650 		if ([adium.chatController closeChat:inChat]) {
   651 			[interfacePlugin closeChat:inChat];
   652 		}
   653 	}
   654 }
   655 
   656 /*!
   657  * @brief Consolidate chats into a single container
   658  */
   659 - (void)consolidateChats
   660 {
   661 	//We work with copies of these arrays, since moving chats may change their contents
   662 	NSArray			*openContainerIDs = [[interfacePlugin openContainerIDs] copy];
   663 	NSEnumerator	*containerEnumerator = [openContainerIDs objectEnumerator];
   664 	NSString		*firstContainerID = [containerEnumerator nextObject];
   665 	NSString		*containerID;
   666 	
   667 	//For all containers but the first, move the chats they contain to the first container
   668 	while ((containerID = [containerEnumerator nextObject])) {
   669 		NSArray			*openChats = [[interfacePlugin openChatsInContainerWithID:containerID] copy];
   670 		NSEnumerator	*chatEnumerator = [openChats objectEnumerator];
   671 		AIChat			*chat;
   672 
   673 		//Move all the chats, providing a target index if chat sorting is enabled
   674 		while ((chat = [chatEnumerator nextObject])) {
   675 			[interfacePlugin moveChat:chat
   676 					toContainerWithID:firstContainerID
   677 								index:-1];
   678 		}
   679 		
   680 		[openChats release];
   681 	}
   682 	
   683 	[self chatOrderDidChange];
   684 	
   685 	[openContainerIDs release];
   686 }
   687 
   688 - (void)moveChatToNewContainer:(AIChat *)inChat
   689 {
   690 	[interfacePlugin moveChatToNewContainer:inChat];
   691 }
   692 
   693 /*!
   694  * @returns Active chat
   695  */
   696 - (AIChat *)activeChat
   697 {
   698 	return activeChat;
   699 }
   700 
   701 /*!
   702  * @brief Set the active chat window
   703  */
   704 - (void)setActiveChat:(AIChat *)inChat
   705 {
   706 	[interfacePlugin setActiveChat:inChat];
   707 }
   708 
   709 /*!
   710  * @returns Last chat to be active, nil if not chat is open
   711  */
   712 - (AIChat *)mostRecentActiveChat
   713 {
   714 	return mostRecentActiveChat;
   715 }
   716 
   717 /*!
   718  * @brief Sets active chat window based on chat
   719  */
   720 - (void)setMostRecentActiveChat:(AIChat *)inChat
   721 {
   722 	[self setActiveChat:inChat];
   723 }
   724 
   725 /*!
   726  * @returns Array of open chats (cached, so call as frequently as desired)
   727  */
   728 - (NSArray *)openChats
   729 {
   730 	if (!_cachedOpenChats) {
   731 		_cachedOpenChats = [[interfacePlugin openChats] retain];
   732 	}
   733 	
   734 	return _cachedOpenChats;
   735 }
   736 
   737 - (NSArray *)openContainerIDs
   738 {
   739 	return [interfacePlugin openContainerIDs];
   740 }
   741 
   742 /*!
   743  * @param containerID ID for chat window
   744  *
   745  * @returns Array of all chats in chat window
   746  */
   747 - (NSArray *)openChatsInContainerWithID:(NSString *)containerID
   748 {
   749 	return [interfacePlugin openChatsInContainerWithID:containerID];
   750 }
   751 
   752 /*!
   753  * @brief Resets the cache of open chats
   754  */
   755 - (void)_resetOpenChatsCache
   756 {
   757 	[_cachedOpenChats release]; _cachedOpenChats = nil;
   758 }
   759 
   760 
   761 
   762 //Interface plugin callbacks -------------------------------------------------------------------------------------------
   763 //These methods are called by the interface to let us know what's going on.  We're informed of chats opening, closing,
   764 //changing order, etc.
   765 #pragma mark Interface plugin callbacks
   766 /*!
   767  * @brief A chat window did open: rebuild our window menu to show the new chat
   768  *
   769  * This should be called by the interface plugin (e.g. AIDualWindowInterfacePlugin) after a chat opens
   770  *
   771  * @param inChat Newly created chat 
   772  */
   773 - (void)chatDidOpen:(AIChat *)inChat
   774 {
   775 	[self _resetOpenChatsCache];
   776 	[self buildWindowMenu];
   777 	[self saveContainers];
   778 }
   779 
   780 /*!
   781  * @brief A chat has become active: update our chat closing keys and flag this chat as selected in the window menu
   782  *
   783  * @param inChat Chat which has become active
   784  */
   785 - (void)chatDidBecomeActive:(AIChat *)inChat
   786 {
   787 	AIChat	*previouslyActiveChat = activeChat;
   788 	
   789 	activeChat = [inChat retain];
   790 	
   791 	[self updateCloseMenuKeys];
   792 	[self updateActiveWindowMenuItem];
   793 	
   794 	if (inChat && (inChat != mostRecentActiveChat)) {
   795 		[mostRecentActiveChat release]; mostRecentActiveChat = nil;
   796 		mostRecentActiveChat = [inChat retain];
   797 	}
   798 	
   799 	[[NSNotificationCenter defaultCenter] postNotificationName:Chat_BecameActive
   800 											  object:inChat 
   801 											userInfo:(previouslyActiveChat ?
   802 													  [NSDictionary dictionaryWithObject:previouslyActiveChat
   803 																				  forKey:@"PreviouslyActiveChat"] :
   804 													  nil)];
   805 	
   806 	if (inChat) {
   807 		/* Clear the unviewed content on the next event loop so other methods have a chance to react to the chat becoming
   808 		* active. Specifically, this lets the handleReopenWithVisibleWindows: method have a chance to know that this chat
   809 		* had unviewed content.
   810 		*/
   811 		[inChat performSelector:@selector(clearUnviewedContentCount)
   812 					 withObject:nil
   813 					 afterDelay:0];
   814 	}
   815 	
   816 	[previouslyActiveChat release];	
   817 }
   818 
   819 /*!
   820  * @brief A chat has become visible: send out a notification for components and plugins to take action
   821  *
   822  * @param inChat Chat that has become active
   823  * @param nWindow Containing chat window
   824  */
   825 - (void)chatDidBecomeVisible:(AIChat *)inChat inWindow:(NSWindow *)inWindow
   826 {
   827 	[[NSNotificationCenter defaultCenter] postNotificationName:@"AIChatDidBecomeVisible"
   828 											  object:inChat
   829 											userInfo:[NSDictionary dictionaryWithObject:inWindow
   830 																				 forKey:@"NSWindow"]];
   831 }
   832 
   833 /*!
   834  * @brief Find the window currently displaying a chat
   835  *
   836  * @returns Window for chat otherwise if the chat is not in any window, or is not visible in any window, returns nil
   837  */
   838 - (NSWindow *)windowForChat:(AIChat *)inChat
   839 {
   840 	return [interfacePlugin windowForChat:inChat];
   841 }
   842 
   843 /*!
   844  * @brief Find the chat active in a window
   845  *
   846  * If the window does not have an active chat, nil is returned
   847  */
   848 - (AIChat *)activeChatInWindow:(NSWindow *)window
   849 {
   850 	return [interfacePlugin activeChatInWindow:window];
   851 }
   852 
   853 /*!
   854  * @brief A chat window did close: rebuild our window menu to remove the chat
   855  * 
   856  * @param inChat Chat that closed
   857  */
   858 - (void)chatDidClose:(AIChat *)inChat
   859 {
   860 	[self _resetOpenChatsCache];
   861 	[inChat clearUnviewedContentCount];
   862 	[self buildWindowMenu];
   863 	
   864 	if (!adium.isQuitting) {
   865 		// Don't save containers when the chats are closed while quitting
   866 		[self saveContainers];
   867 	}
   868 	
   869 	if (inChat == activeChat) {
   870 		[activeChat release]; activeChat = nil;
   871 	}
   872 	
   873 	if (inChat == mostRecentActiveChat) {
   874 		[mostRecentActiveChat release]; mostRecentActiveChat = nil;
   875 	}
   876 }
   877 
   878 /*!
   879  * @brief The order of chats has changed: rebuild our window menu to reflect the new order
   880  */
   881 - (void)chatOrderDidChange
   882 {
   883 	[self _resetOpenChatsCache];
   884 	[self buildWindowMenu];
   885 
   886 	if (!adium.isQuitting) {
   887 		// Don't save containers when the chats are closed while quitting
   888 		[self saveContainers];
   889 	}
   890 	
   891 	[[NSNotificationCenter defaultCenter] postNotificationName:Chat_OrderDidChange object:nil userInfo:nil];
   892 	
   893 }
   894 
   895 #pragma mark Unviewed content
   896 
   897 /*!
   898  * @breif Content was received, increase the unviewed content count of the chat (if it's not currently active)
   899  */
   900 - (void)didReceiveContent:(NSNotification *)notification
   901 {
   902 	AIChat		*chat = [[notification userInfo] objectForKey:@"AIChat"];
   903 	
   904 	if (chat != activeChat) {
   905 		[chat incrementUnviewedContentCount];
   906 	}
   907 }
   908 
   909 
   910 //Chat close menus -----------------------------------------------------------------------------------------------------
   911 #pragma mark Chat close menus
   912 
   913 /*!
   914  * @brief Closes currently active window
   915  */
   916 - (IBAction)closeMenu:(id)sender
   917 {
   918     [[[NSApplication sharedApplication] keyWindow] performClose:nil];
   919 }
   920 
   921 /*!
   922  * @brief Closes currently active chat (if there is an active chat)
   923  */
   924 - (IBAction)closeChatMenu:(id)sender
   925 {
   926 	if (activeChat) [self closeChat:activeChat];
   927 }
   928 
   929 /*!
   930  * @brief Closes currently selected chat based on current chat contextual menu
   931  */
   932 - (IBAction)closeContextualChat:(id)sender
   933 {
   934 	[self closeChat:[adium.menuController currentContextMenuChat]];
   935 }
   936 
   937 /*!
   938  * @brief Loop through open chats and close them
   939  */
   940 - (IBAction)closeAllChats:(id)sender
   941 {
   942 	for (AIChat *chatToClose in [[interfacePlugin.openChats copy] autorelease]) {
   943 		[self closeChat:chatToClose];
   944 	}
   945 }
   946 
   947 /*!
   948  * @brief Updates the key equivalents on 'close' and 'close chat' (dynamically changed to make cmd-w less destructive)
   949  */
   950 - (void)updateCloseMenuKeys
   951 {
   952 	if (activeChat && !closeMenuConfiguredForChat) {
   953         [menuItem_close setKeyEquivalent:@"W"];
   954         [menuItem_closeChat setKeyEquivalent:@"w"];
   955 		closeMenuConfiguredForChat = YES;
   956 	} else if (!activeChat && closeMenuConfiguredForChat) {
   957         [menuItem_close setKeyEquivalent:@"w"];
   958 		[menuItem_closeChat removeKeyEquivalent];		
   959 		closeMenuConfiguredForChat = NO;
   960 	}
   961 }
   962 
   963 
   964 //Window Menu ----------------------------------------------------------------------------------------------------------
   965 #pragma mark Window Menu
   966 
   967 /*!
   968  * @brief Open the authorization requests window.
   969  */
   970 - (void)openAuthorizationWindow:(id)sender
   971 {
   972 	[[AIAuthorizationRequestsWindowController sharedController] showWindow:nil];
   973 }
   974 
   975 /*!
   976  * @brief Make a chat window active
   977  * 
   978  * Invoked by a selection in the window menu
   979  */
   980 - (IBAction)showChatWindow:(id)sender
   981 {
   982 	[self setActiveChat:[sender representedObject]];
   983     [[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
   984 }
   985 
   986 /*!
   987  * @brief Updates the 'check' icon so it's next to the active window
   988  */
   989 - (void)updateActiveWindowMenuItem
   990 {
   991     NSMenuItem		*item;
   992 
   993     for (item in windowMenuArray) {
   994 		if ([item representedObject]) [item setState:([item representedObject] == activeChat ? NSOnState : NSOffState)];
   995     }
   996 }
   997 
   998 /*!
   999  * @brief Builds the window menu
  1000  * 
  1001  * This function gets called whenever chats are opened, closed, or re-ordered - so improvements and optimizations here
  1002  * would probably be helpful
  1003  */
  1004 - (void)buildWindowMenu
  1005 {	
  1006     NSMenuItem				*item;
  1007     NSInteger						windowKey = 1;
  1008 	
  1009     //Remove any existing menus
  1010     for (item in windowMenuArray) {
  1011         [adium.menuController removeMenuItem:item];
  1012     }
  1013     [windowMenuArray release]; windowMenuArray = [[NSMutableArray alloc] init];
  1014 	
  1015     //Messages window and any open messasges	
  1016 	for (NSDictionary *containerDict in [interfacePlugin openContainersAndChats]) {
  1017 		NSString		*containerName = [containerDict objectForKey:@"Name"];
  1018 		NSArray			*contentArray = [containerDict objectForKey:@"Content"];
  1019 		
  1020 		//Add a menu item for the container
  1021 		if (contentArray.count > 1) {
  1022 			item = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:([containerName length] ? containerName : AILocalizedString(@"Chats", nil))
  1023 																		target:nil
  1024 																		action:nil
  1025 																 keyEquivalent:@""];
  1026 			[self _addItemToMainMenuAndDock:item];
  1027 			[item release];
  1028 		}
  1029 		
  1030 		//Add items for the chats it contains
  1031 		for (AIChat *chat in [contentArray objectEnumerator]) {
  1032 			NSString		*windowKeyString;
  1033 			
  1034 			//Prepare a key equivalent for the controller
  1035 			if (windowKey < 10) {
  1036 				windowKeyString = [NSString stringWithFormat:@"%ld", (windowKey)];
  1037 			} else if (windowKey == 10) {
  1038 				windowKeyString = [NSString stringWithString:@"0"];
  1039 			} else {
  1040 				windowKeyString = [NSString stringWithString:@""];
  1041 			}
  1042 			
  1043 			item = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:chat.displayName
  1044 																		target:self
  1045 																		action:@selector(showChatWindow:)
  1046 																 keyEquivalent:windowKeyString];
  1047 			if ([contentArray count] > 1) [item setIndentationLevel:1];
  1048 			[item setRepresentedObject:chat];
  1049 			[item setImage:chat.chatMenuImage];
  1050 			[self _addItemToMainMenuAndDock:item];
  1051 			[item release];
  1052 
  1053 			windowKey++;
  1054 		}
  1055 	}
  1056 
  1057 	[self updateActiveWindowMenuItem];
  1058 }
  1059 
  1060 /*!
  1061  * brief Adds a menu item to the internal array, dock menu, and main menu
  1062  *
  1063  * Should be used for adding a new window to the window menu (and dock menu)
  1064  */
  1065 - (void)_addItemToMainMenuAndDock:(NSMenuItem *)item
  1066 {
  1067 	//Add to main menu first
  1068 	[adium.menuController addMenuItem:item toLocation:LOC_Window_Fixed];
  1069 	[windowMenuArray addObject:item];
  1070 	
  1071 	//Make a copy, and add to the dock
  1072 	item = [item copy];
  1073 	[item setKeyEquivalent:@""];
  1074 	[adium.menuController addMenuItem:item toLocation:LOC_Dock_Status];
  1075 	[windowMenuArray addObject:item];
  1076 	[item release];
  1077 }
  1078 
  1079 
  1080 //Chat Cycling ---------------------------------------------------------------------------------------------------------
  1081 #pragma mark Chat Cycling
  1082 
  1083 /*!
  1084  * @brief Cycles to the next active chat
  1085  */
  1086 - (void)nextChat:(id)sender
  1087 {
  1088 	NSArray	*openChats = [self openChats];
  1089 
  1090 	if ([openChats count]) {
  1091 		if (activeChat) {
  1092 			NSInteger chatIndex = [openChats indexOfObject:activeChat]+1;
  1093 			[self setActiveChat:[openChats objectAtIndex:(chatIndex < [openChats count] ? chatIndex : 0)]];
  1094 		} else {
  1095 			[self setActiveChat:[openChats objectAtIndex:0]];
  1096 		}
  1097 	}
  1098 }
  1099 
  1100 /*!
  1101  * @brief Cycles to the previus active chat
  1102  */
  1103 - (void)previousChat:(id)sender
  1104 {
  1105 	NSArray	*openChats = [self openChats];
  1106 	
  1107 	if ([openChats count]) {
  1108 		if (activeChat) {
  1109 			NSInteger chatIndex = [openChats indexOfObject:activeChat]-1;
  1110 			[self setActiveChat:[openChats objectAtIndex:(chatIndex >= 0 ? chatIndex : [openChats count]-1)]];
  1111 		} else {
  1112 			[self setActiveChat:[openChats lastObject]];
  1113 		}
  1114 	}
  1115 }
  1116 
  1117 //Selected contact ------------------------------------------------
  1118 #pragma mark Selected contact
  1119 - (id)_performSelectorOnFirstAvailableResponder:(SEL)selector
  1120 {
  1121     NSResponder	*responder = [[[NSApplication sharedApplication] mainWindow] firstResponder];
  1122     //Check the first responder
  1123     if ([responder respondsToSelector:selector]) {
  1124         return [responder performSelector:selector];
  1125     }
  1126 	
  1127     //Search the responder chain
  1128     do{
  1129         responder = [responder nextResponder];
  1130         if ([responder respondsToSelector:selector]) {
  1131             return [responder performSelector:selector];
  1132         }
  1133 		
  1134     } while (responder != nil);
  1135 	
  1136     //None found, return nil
  1137     return nil;
  1138 }
  1139 - (id)_performSelectorOnFirstAvailableResponder:(SEL)selector conformingToProtocol:(Protocol *)protocol
  1140 {
  1141 	NSResponder *responder = [[[NSApplication sharedApplication] mainWindow] firstResponder];
  1142 	//Check the first responder
  1143 	if ([responder conformsToProtocol:protocol] && [responder respondsToSelector:selector]) {
  1144 		return [responder performSelector:selector];
  1145 	}
  1146 	
  1147     //Search the responder chain
  1148     do{
  1149         responder = [responder nextResponder];
  1150         if ([responder conformsToProtocol:protocol] && [responder respondsToSelector:selector]) {
  1151             return [responder performSelector:selector];
  1152         }
  1153 		
  1154     } while (responder != nil);
  1155 	
  1156     //None found, return nil
  1157     return nil;
  1158 }
  1159 
  1160 /*!
  1161  * @returns The "selected"(represented) contact (By finding the first responder that returns a contact)
  1162  * If no listObject is found, try to find a list object selected in a group chat
  1163  */
  1164 - (AIListObject *)selectedListObject
  1165 {
  1166 	AIListObject *listObject = [self _performSelectorOnFirstAvailableResponder:@selector(listObject)];
  1167 	if ( !listObject) {
  1168 		listObject = [self _performSelectorOnFirstAvailableResponder:@selector(preferredListObject)];
  1169 	}
  1170 	return listObject;
  1171 }
  1172 
  1173 - (AIListObject *)selectedListObjectInContactList
  1174 {
  1175 	return [self _performSelectorOnFirstAvailableResponder:@selector(listObject) conformingToProtocol:@protocol(ContactListOutlineView)];
  1176 }
  1177 - (NSArray *)arrayOfSelectedListObjectsInContactList
  1178 {
  1179 	return [self _performSelectorOnFirstAvailableResponder:@selector(arrayOfListObjects) conformingToProtocol:@protocol(ContactListOutlineView)];
  1180 }
  1181 - (NSArray *)arrayOfSelectedListObjectsWithGroupsInContactList
  1182 {
  1183 	return [self _performSelectorOnFirstAvailableResponder:@selector(arrayOfListObjectsWithGroups) conformingToProtocol:@protocol(ContactListOutlineView)];
  1184 }
  1185 
  1186 //Message View ---------------------------------------------------------------------------------------------------------
  1187 //Message view is abstracted from the containing interface, since they're not directly related to eachother
  1188 #pragma mark Message View
  1189 //Registers a view to handle the contact list
  1190 - (void)registerMessageDisplayPlugin:(id <AIMessageDisplayPlugin>)inPlugin
  1191 {
  1192     [messageViewArray addObject:inPlugin];
  1193 }
  1194 - (void)unregisterMessageDisplayPlugin:(id <AIMessageDisplayPlugin>)inPlugin
  1195 {
  1196     [messageViewArray removeObject:inPlugin];
  1197 }
  1198 - (id <AIMessageDisplayController>)messageDisplayControllerForChat:(AIChat *)inChat
  1199 {
  1200 	//Sometimes our users find it amusing to disable plugins that are located within the Adium bundle.  This error
  1201 	//trap prevents us from crashing if they happen to disable all the available message view plugins.
  1202 	//PUT THAT PLUGIN BACK IT WAS IMPORTANT!
  1203 	if ([messageViewArray count] == 0) {
  1204 		NSRunCriticalAlertPanel(@"No Message View Plugin Installed",
  1205 								@"Adium cannot find its message view plugin. Please re-install.  If you've manually disabled Adium's message view plugin, please re-enable it.",
  1206 								@"Quit",
  1207 								nil,
  1208 								nil);
  1209 		[NSApp terminate:nil];
  1210 	}
  1211 	
  1212 	return [[messageViewArray objectAtIndex:0] messageDisplayControllerForChat:inChat];
  1213 }
  1214 
  1215 
  1216 //Error Display --------------------------------------------------------------------------------------------------------
  1217 #pragma mark Error Display
  1218 - (void)handleErrorMessage:(NSString *)inTitle withDescription:(NSString *)inDesc
  1219 {
  1220     [self handleMessage:inTitle withDescription:inDesc withWindowTitle:ERROR_MESSAGE_WINDOW_TITLE];
  1221 }
  1222 
  1223 - (void)handleMessage:(NSString *)inTitle withDescription:(NSString *)inDesc withWindowTitle:(NSString *)inWindowTitle;
  1224 {
  1225     NSDictionary	*errorDict;
  1226     
  1227     //Post a notification that an error was recieved
  1228     errorDict = [NSDictionary dictionaryWithObjectsAndKeys:inTitle,@"Title",inDesc,@"Description",inWindowTitle,@"Window Title",nil];
  1229     [[NSNotificationCenter defaultCenter] postNotificationName:Interface_ShouldDisplayErrorMessage object:nil userInfo:errorDict];
  1230 }
  1231 
  1232 //Display then clear the last disconnection error
  1233 - (void)account:(AIAccount *)inAccount disconnectedWithError:(NSString *)disconnectionError
  1234 {
  1235 
  1236 }
  1237 
  1238 //Question Display -----------------------------------------------------------------------------------------------------
  1239 #pragma mark Question Display
  1240 - (void)displayQuestion:(NSString *)inTitle withAttributedDescription:(NSAttributedString *)inDesc withWindowTitle:(NSString *)inWindowTitle
  1241 		  defaultButton:(NSString *)inDefaultButton alternateButton:(NSString *)inAlternateButton otherButton:(NSString *)inOtherButton suppression:(NSString *)inSuppression
  1242 				 target:(id)inTarget selector:(SEL)inSelector userInfo:(id)inUserInfo
  1243 {
  1244 	NSMutableDictionary *questionDict = [NSMutableDictionary dictionary];
  1245 	
  1246 	if(inTitle != nil)
  1247 		[questionDict setObject:inTitle forKey:@"Title"];
  1248 	if(inDesc != nil)
  1249 		[questionDict setObject:inDesc forKey:@"Description"];
  1250 	if(inWindowTitle != nil)
  1251 		[questionDict setObject:inWindowTitle forKey:@"Window Title"];
  1252 	if(inDefaultButton != nil)
  1253 		[questionDict setObject:inDefaultButton forKey:@"Default Button"];
  1254 	if(inAlternateButton != nil)
  1255 		[questionDict setObject:inAlternateButton forKey:@"Alternate Button"];
  1256 	if(inOtherButton != nil)
  1257 		[questionDict setObject:inOtherButton forKey:@"Other Button"];
  1258 	if(inSuppression != nil)
  1259 		[questionDict setObject:inSuppression forKey:@"Suppression Checkbox"];
  1260 	if(inTarget != nil)
  1261 		[questionDict setObject:inTarget forKey:@"Target"];
  1262 	if(inSelector != NULL)
  1263 		[questionDict setObject:NSStringFromSelector(inSelector) forKey:@"Selector"];
  1264 	if(inUserInfo != nil)
  1265 		[questionDict setObject:inUserInfo forKey:@"Userinfo"];
  1266 	
  1267 	[[NSNotificationCenter defaultCenter] postNotificationName:Interface_ShouldDisplayQuestion object:nil userInfo:questionDict];
  1268 }
  1269 
  1270 - (void)displayQuestion:(NSString *)inTitle withDescription:(NSString *)inDesc withWindowTitle:(NSString *)inWindowTitle
  1271 		  defaultButton:(NSString *)inDefaultButton alternateButton:(NSString *)inAlternateButton otherButton:(NSString *)inOtherButton suppression:(NSString *)inSuppression
  1272 				 target:(id)inTarget selector:(SEL)inSelector userInfo:(id)inUserInfo
  1273 {
  1274 	[self displayQuestion:inTitle
  1275 withAttributedDescription:[[[NSAttributedString alloc] initWithString:inDesc
  1276 														   attributes:[NSDictionary dictionaryWithObject:[NSFont systemFontOfSize:0]
  1277 																								  forKey:NSFontAttributeName]] autorelease]
  1278 		  withWindowTitle:inWindowTitle
  1279 			defaultButton:inDefaultButton
  1280 		  alternateButton:inAlternateButton
  1281 			  otherButton:inOtherButton
  1282 			  suppression:inSuppression
  1283 				   target:inTarget
  1284 				 selector:inSelector
  1285 				 userInfo:inUserInfo];
  1286 }
  1287 //Synchronized Flashing ------------------------------------------------------------------------------------------------
  1288 #pragma mark Synchronized Flashing
  1289 //Register to observe the synchronized flashing
  1290 - (void)registerFlashObserver:(id <AIFlashObserver>)inObserver
  1291 {
  1292     //Setup the timer if we don't have one yet
  1293     if (!flashObserverArray) {
  1294         flashObserverArray = [[NSMutableArray alloc] init];
  1295         flashTimer = [[NSTimer scheduledTimerWithTimeInterval:(1.0/2.0) 
  1296                                                        target:self 
  1297                                                      selector:@selector(flashTimer:) 
  1298                                                      userInfo:nil
  1299                                                       repeats:YES] retain];
  1300     }
  1301     
  1302     //Add the new observer to the array
  1303     [flashObserverArray addObject:inObserver];
  1304 }
  1305 
  1306 //Unregister from observing flashing
  1307 - (void)unregisterFlashObserver:(id <AIFlashObserver>)inObserver
  1308 {
  1309     //Remove the observer from our array
  1310     [flashObserverArray removeObject:inObserver];
  1311     
  1312     //Release the observer array and uninstall the timer
  1313     if ([flashObserverArray count] == 0) {
  1314         [flashObserverArray release]; flashObserverArray = nil;
  1315         [flashTimer invalidate];
  1316         [flashTimer release]; flashTimer = nil;
  1317     }
  1318 }
  1319 
  1320 //Timer, invoke a flash
  1321 - (void)flashTimer:(NSTimer *)inTimer
  1322 {
  1323 	flashState++;
  1324 
  1325 	for (id<AIFlashObserver>observer in [[flashObserverArray copy] autorelease]) {
  1326 		[observer flash:flashState];
  1327 	}
  1328 }
  1329 
  1330 //Current state of flashing.  This is an integer the increases by 1 with every flash.  Mod to whatever range is desired
  1331 - (int)flashState
  1332 {
  1333     return flashState;
  1334 }
  1335 
  1336 
  1337 //Tooltips -------------------------------------------------------------------------------------------------------------
  1338 #pragma mark Tooltips
  1339 //Registers code to display tooltip info about a contact
  1340 - (void)registerContactListTooltipEntry:(id <AIContactListTooltipEntry>)inEntry secondaryEntry:(BOOL)isSecondary
  1341 {
  1342     if (isSecondary)
  1343         [contactListTooltipSecondaryEntryArray addObject:inEntry];
  1344     else
  1345         [contactListTooltipEntryArray addObject:inEntry];
  1346 }
  1347 
  1348 //Unregisters code to display tooltip info about a contact
  1349 - (void)unregisterContactListTooltipEntry:(id <AIContactListTooltipEntry>)inEntry secondaryEntry:(BOOL)isSecondary
  1350 {
  1351     if (isSecondary)
  1352         [contactListTooltipSecondaryEntryArray removeObject:inEntry];
  1353     else
  1354         [contactListTooltipEntryArray removeObject:inEntry];
  1355 }
  1356 
  1357 - (NSArray *)contactListTooltipPrimaryEntries
  1358 {
  1359 	return contactListTooltipEntryArray;
  1360 }
  1361 
  1362 - (NSArray *)contactListTooltipSecondaryEntries
  1363 {
  1364 	return contactListTooltipSecondaryEntryArray;
  1365 }
  1366 
  1367 //list object tooltips
  1368 - (void)showTooltipForListObject:(AIListObject *)object atScreenPoint:(NSPoint)point onWindow:(NSWindow *)inWindow 
  1369 {
  1370     if (object) {
  1371         if (object == tooltipListObject) { //If we already have this tooltip open
  1372                                          //Move the existing tooltip
  1373             [AITooltipUtilities showTooltipWithTitle:tooltipTitle
  1374 												body:tooltipBody
  1375 											   image:tooltipImage 
  1376 										imageOnRight:DISPLAY_IMAGE_ON_RIGHT 
  1377 											onWindow:inWindow
  1378 											 atPoint:point 
  1379 										 orientation:TooltipBelow];
  1380             
  1381         } else { //This is a new tooltip
  1382             NSArray                     *tabArray;
  1383             NSMutableParagraphStyle     *paragraphStyleTitle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
  1384             NSMutableParagraphStyle     *paragraphStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
  1385             
  1386             //Hold onto the new object
  1387             [tooltipListObject release]; tooltipListObject = [object retain];
  1388             
  1389             //Buddy Icon
  1390             [tooltipImage release];
  1391 			tooltipImage = [[tooltipListObject userIcon] retain];
  1392 			if (!tooltipImage) tooltipImage = [[AIServiceIcons serviceIconForObject:tooltipListObject
  1393 																			 type:AIServiceIconLarge
  1394 																		direction:AIIconNormal] retain];
  1395             
  1396             //Reset the maxLabelWidth for the tooltip generation
  1397             maxLabelWidth = 0;
  1398             
  1399             //Build a tooltip string for the primary information
  1400             [tooltipTitle release]; tooltipTitle = [[self _tooltipTitleForObject:object] retain];
  1401             
  1402             //If there is an image, set the title tab and indentation settings independently
  1403             if (tooltipImage) {
  1404                 //Set a right-align tab at the maximum label width and a left-align just past it
  1405                 tabArray = [[NSArray alloc] initWithObjects:[[[NSTextTab alloc] initWithType:NSRightTabStopType 
  1406 																					location:maxLabelWidth] autorelease]
  1407                                                             ,[[[NSTextTab alloc] initWithType:NSLeftTabStopType 
  1408                                                                                    location:maxLabelWidth + LABEL_ENTRY_SPACING] autorelease]
  1409                                                             ,nil];
  1410                 
  1411                 [paragraphStyleTitle setTabStops:tabArray];
  1412                 [tabArray release];
  1413                 tabArray = nil;
  1414                 [paragraphStyleTitle setHeadIndent:(maxLabelWidth + LABEL_ENTRY_SPACING)];
  1415                 
  1416                 [tooltipTitle addAttribute:NSParagraphStyleAttributeName 
  1417                                      value:paragraphStyleTitle
  1418                                      range:NSMakeRange(0,[tooltipTitle length])];
  1419                 
  1420                 //Reset the max label width since the body will be independent
  1421                 maxLabelWidth = 0;
  1422             }
  1423             
  1424             //Build a tooltip string for the secondary information
  1425             [tooltipBody release]; tooltipBody = nil;
  1426             tooltipBody = [[self _tooltipBodyForObject:object] retain];
  1427             
  1428             //Set a right-align tab at the maximum label width for the body and a left-align just past it
  1429             tabArray = [[NSArray alloc] initWithObjects:[[[NSTextTab alloc] initWithType:NSRightTabStopType 
  1430                                                                                  location:maxLabelWidth] autorelease]
  1431                                                         ,[[[NSTextTab alloc] initWithType:NSLeftTabStopType 
  1432                                                                                 location:maxLabelWidth + LABEL_ENTRY_SPACING] autorelease]
  1433                                                         ,nil];
  1434             [paragraphStyle setTabStops:tabArray];
  1435             [tabArray release];
  1436             [paragraphStyle setHeadIndent:(maxLabelWidth + LABEL_ENTRY_SPACING)];
  1437             
  1438             [tooltipBody addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:NSMakeRange(0,[tooltipBody length])];
  1439             //If there is no image, also use these settings for the top part
  1440             if (!tooltipImage) {
  1441                 [tooltipTitle addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:NSMakeRange(0,[tooltipTitle length])];
  1442             }
  1443             
  1444             //Display the new tooltip
  1445             [AITooltipUtilities showTooltipWithTitle:tooltipTitle
  1446                                                 body:tooltipBody 
  1447                                                image:tooltipImage
  1448                                         imageOnRight:DISPLAY_IMAGE_ON_RIGHT
  1449                                             onWindow:inWindow
  1450                                              atPoint:point 
  1451                                          orientation:TooltipBelow];
  1452 			
  1453 			[paragraphStyleTitle release];
  1454 			[paragraphStyle release];
  1455         }
  1456         
  1457     } else {
  1458         //Hide the existing tooltip
  1459         if (tooltipListObject) {
  1460             [AITooltipUtilities showTooltipWithTitle:nil 
  1461                                                 body:nil
  1462                                                image:nil 
  1463                                             onWindow:nil
  1464                                              atPoint:point
  1465                                          orientation:TooltipBelow];
  1466             [tooltipListObject release]; tooltipListObject = nil;
  1467 			
  1468 			[tooltipTitle release]; tooltipTitle = nil;
  1469 			[tooltipBody release]; tooltipBody = nil;
  1470 			[tooltipImage release]; tooltipImage = nil;
  1471         }
  1472     }
  1473 }
  1474 
  1475 - (NSAttributedString *)_tooltipTitleForObject:(AIListObject *)object
  1476 {
  1477     NSMutableAttributedString           *titleString = [[NSMutableAttributedString alloc] init];
  1478     
  1479     id <AIContactListTooltipEntry>		tooltipEntry;
  1480     NSEnumerator                        *labelEnumerator;
  1481     NSMutableArray                      *labelArray = [NSMutableArray array];
  1482     NSMutableArray                      *entryArray = [NSMutableArray array];
  1483     NSMutableAttributedString           *entryString;
  1484     CGFloat                               labelWidth;
  1485     BOOL                                isFirst = YES;
  1486     
  1487     NSString                            *formattedUID = object.formattedUID;
  1488     
  1489     //Configure fonts and attributes
  1490     NSFontManager                       *fontManager = [NSFontManager sharedFontManager];
  1491     NSFont                              *toolTipsFont = [NSFont toolTipsFontOfSize:10];
  1492     NSMutableDictionary                 *titleDict = [NSMutableDictionary dictionaryWithObject:[fontManager convertFont:[NSFont toolTipsFontOfSize:12] toHaveTrait:NSBoldFontMask]
  1493 	                                                                                    forKey:NSFontAttributeName];
  1494     NSMutableDictionary                 *labelDict = [NSMutableDictionary dictionaryWithObject:[fontManager convertFont:[NSFont toolTipsFontOfSize:9] toHaveTrait:NSBoldFontMask]
  1495 	                                                                                    forKey:NSFontAttributeName];
  1496     NSMutableDictionary                 *labelEndLineDict = [NSMutableDictionary dictionaryWithObject:[NSFont toolTipsFontOfSize:2]
  1497 	                                                                                           forKey:NSFontAttributeName];
  1498     NSMutableDictionary                 *entryDict = [NSMutableDictionary dictionaryWithObject:toolTipsFont
  1499 	                                                                                    forKey:NSFontAttributeName];
  1500 	
  1501 	//Get the user's display name as an attributed string
  1502     NSAttributedString                  *displayName = [[NSAttributedString alloc] initWithString:object.displayName
  1503 																					   attributes:titleDict];
  1504 	NSAttributedString					*filteredDisplayName = [adium.contentController filterAttributedString:displayName
  1505 																								 usingFilterType:AIFilterTooltips
  1506 																									   direction:AIFilterIncoming
  1507 																										 context:nil];
  1508 	
  1509 	//Append the user's display name
  1510 	if (filteredDisplayName) {
  1511 		[titleString appendAttributedString:filteredDisplayName];
  1512 	}
  1513 	
  1514 	//Append the user's formatted UID if there is one that's different to the display name
  1515 	if (formattedUID && (!([[[displayName string] compactedString] isEqualToString:[formattedUID compactedString]]))) {
  1516 		[titleString appendString:[NSString stringWithFormat:@" (%@)", formattedUID] withAttributes:titleDict];
  1517 	}
  1518 	[displayName release];
  1519     
  1520     if ([object isKindOfClass:[AIListContact class]]) {
  1521 		if ((![object isKindOfClass:[AIMetaContact class]] || [(AIMetaContact *)object containsOnlyOneService]) &&
  1522 			[object userIcon]) {
  1523 			NSImage *serviceIcon = [[AIServiceIcons serviceIconForObject:object type:AIServiceIconSmall direction:AIIconNormal]
  1524 									imageByScalingToSize:NSMakeSize(14,14)];
  1525 			if (serviceIcon) {
  1526 				NSTextAttachment		*attachment;
  1527 				NSTextAttachmentCell	*cell;
  1528 				
  1529 				cell = [[NSTextAttachmentCell alloc] init];
  1530 				[cell setImage:serviceIcon];
  1531 				
  1532 				attachment = [[NSTextAttachment alloc] init];
  1533 				[attachment setAttachmentCell:cell];
  1534 				[cell release];
  1535 	
  1536 				[titleString appendString:@" " withAttributes:nil];
  1537 				[titleString appendAttributedString:[NSAttributedString attributedStringWithAttachment:attachment]];
  1538 				[attachment release];
  1539 			}
  1540 		}
  1541 	}
  1542 		
  1543     if ([object isKindOfClass:[AIListGroup class]]) {
  1544         [titleString appendString:[NSString stringWithFormat:@" (%ld/%ld)",[(AIListGroup *)object visibleCount],[(AIListGroup *)object countOfContainedObjects]] 
  1545                    withAttributes:titleDict];
  1546     }
  1547     
  1548     //Entries from plugins
  1549     
  1550     //Calculate the widest label while loading the arrays
  1551     
  1552     for (tooltipEntry in contactListTooltipEntryArray) {
  1553         
  1554         entryString = [[tooltipEntry entryForObject:object] mutableCopy];
  1555         if (entryString && [entryString length]) {
  1556             
  1557             NSString        *labelString = [tooltipEntry labelForObject:object];
  1558             if (labelString && [labelString length]) {
  1559                 
  1560                 [entryArray addObject:entryString];
  1561                 [labelArray addObject:labelString];
  1562                 
  1563                 NSAttributedString * labelAttribString = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@:",labelString] 
  1564 																						 attributes:labelDict];
  1565                 
  1566                 //The largest size should be the label's size plus the distance to the next tab at least a space past its end
  1567                 labelWidth = [labelAttribString size].width;
  1568                 [labelAttribString release];
  1569                 
  1570                 if (labelWidth > maxLabelWidth)
  1571                     maxLabelWidth = labelWidth;
  1572             }
  1573         }
  1574         [entryString release];
  1575     }
  1576     
  1577     //Add labels plus entires to the toolTip
  1578     labelEnumerator = [labelArray objectEnumerator];
  1579     
  1580     for (entryString in entryArray) {        
  1581         NSAttributedString * labelAttribString = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"\t%@:\t",[labelEnumerator nextObject]]
  1582 																				 attributes:labelDict];
  1583         
  1584         //Add a carriage return
  1585         [titleString appendString:@"\n" withAttributes:labelEndLineDict];
  1586         
  1587         if (isFirst) {
  1588             //skip a line
  1589             [titleString appendString:@"\n" withAttributes:labelEndLineDict];
  1590             isFirst = NO;
  1591         }
  1592         
  1593         //Add the label (with its spacing)
  1594         [titleString appendAttributedString:labelAttribString];
  1595 		[labelAttribString release];
  1596 
  1597 		[entryString addAttributes:entryDict range:NSMakeRange(0,[entryString length])];
  1598         [titleString appendAttributedString:entryString];
  1599     }
  1600 
  1601     return [titleString autorelease];
  1602 }
  1603 
  1604 - (NSAttributedString *)_tooltipBodyForObject:(AIListObject *)object
  1605 {
  1606     NSMutableAttributedString       *tipString = [[NSMutableAttributedString alloc] init];
  1607     
  1608     //Configure fonts and attributes
  1609     NSFontManager                   *fontManager = [NSFontManager sharedFontManager];
  1610     NSFont                          *toolTipsFont = [NSFont toolTipsFontOfSize:10];
  1611     NSMutableDictionary             *labelDict = [NSMutableDictionary dictionaryWithObject:[fontManager convertFont:[NSFont toolTipsFontOfSize:9] toHaveTrait:NSBoldFontMask]
  1612 	                                                                                forKey:NSFontAttributeName];
  1613     NSMutableDictionary             *labelEndLineDict = [NSMutableDictionary dictionaryWithObject:[NSFont toolTipsFontOfSize:1]
  1614 	                                                                                       forKey:NSFontAttributeName];
  1615     NSMutableDictionary             *entryDict = [NSMutableDictionary dictionaryWithObject:toolTipsFont
  1616 	                                                                                forKey:NSFontAttributeName];
  1617     
  1618     //Entries from plugins
  1619     NSEnumerator                    *labelEnumerator; 
  1620     NSMutableArray                  *labelArray = [NSMutableArray array]; //Array of NSStrings
  1621     NSMutableArray                  *entryArray = [NSMutableArray array]; //Array of NSMutableStrings   
  1622     CGFloat                         labelWidth;
  1623     BOOL                            firstEntry = YES;
  1624     
  1625     //Calculate the widest label while loading the arrays
  1626 	for (id <AIContactListTooltipEntry>tooltipEntry in contactListTooltipSecondaryEntryArray) {
  1627 		NSMutableAttributedString *entryString = [[tooltipEntry entryForObject:object] mutableCopy];
  1628 		if (entryString && entryString.length) {
  1629 			NSString        *labelString = [tooltipEntry labelForObject:object];
  1630 
  1631 			if (labelString && labelString.length) {
  1632 				[entryArray addObject:entryString];
  1633 				[labelArray addObject:labelString];
  1634 				
  1635 				NSAttributedString *labelAttribString = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@:",labelString] 
  1636 																						attributes:labelDict];
  1637 				
  1638 				//The largest size should be the label's size plus the distance to the next tab at least a space past its end
  1639 				labelWidth = labelAttribString.size.width;
  1640 				[labelAttribString release];
  1641 				
  1642 				if (labelWidth > maxLabelWidth)
  1643 					maxLabelWidth = labelWidth;
  1644 			}
  1645 		}
  1646 		[entryString release];
  1647 	}
  1648 		
  1649     //Add labels plus entires to the toolTip
  1650     labelEnumerator = [labelArray objectEnumerator];
  1651     for (NSMutableAttributedString *entryString in entryArray) {
  1652         NSMutableAttributedString *labelString = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"\t%@:\t",[labelEnumerator nextObject]]
  1653 																						attributes:labelDict];
  1654         
  1655         if (firstEntry) {
  1656             firstEntry = NO;
  1657         } else {
  1658             //Add a carriage return and skip a line
  1659             [tipString appendString:@"\n\n" withAttributes:labelEndLineDict];
  1660         }
  1661         
  1662         //Add the label (with its spacing)
  1663         [tipString appendAttributedString:labelString];
  1664         [labelString release];
  1665 
  1666         NSRange fullLength = NSMakeRange(0, [entryString length]);
  1667         
  1668         //remove any background coloration
  1669         [entryString removeAttribute:NSBackgroundColorAttributeName range:fullLength];
  1670         
  1671         //adjust foreground colors for the tooltip background
  1672         [entryString adjustColorsToShowOnBackground:[NSColor colorWithCalibratedRed:1.000 green:1.000 blue:0.800 alpha:1.0]];
  1673 
  1674         //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
  1675 		if ([entryString replaceOccurrencesOfString:@"\r" withString:@"\r\t\t" options:NSLiteralSearch range:fullLength])
  1676             fullLength = NSMakeRange(0, [entryString length]);
  1677         if ([entryString replaceOccurrencesOfString:@"\n" withString:@"\n\t\t" options:NSLiteralSearch range:fullLength])
  1678             fullLength = NSMakeRange(0, [entryString length]);
  1679 		
  1680         //Run the entry through the filters and add it to tipString
  1681 		entryString = [[adium.contentController filterAttributedString:entryString
  1682 														 usingFilterType:AIFilterTooltips
  1683 															   direction:AIFilterIncoming
  1684 																 context:object] mutableCopy];
  1685 		
  1686 		[entryString addAttributes:entryDict range:NSMakeRange(0,[entryString length])];
  1687         [tipString appendAttributedString:entryString];
  1688 		[entryString release];
  1689     }
  1690 
  1691     return [tipString autorelease];
  1692 }
  1693 
  1694 //Custom pasting ----------------------------------------------------------------------------------------------------
  1695 #pragma mark Custom Pasting
  1696 //Paste, stripping formatting
  1697 - (IBAction)paste:(id)sender
  1698 {
  1699 	[self _pasteWithPreferredSelector:@selector(pasteAsPlainTextWithTraits:) sender:sender];
  1700 }
  1701 
  1702 //Paste with formatting
  1703 - (IBAction)pasteAndMatchStyle:(id)sender
  1704 {
  1705 	[self _pasteWithPreferredSelector:@selector(pasteAsPlainText:) sender:sender];
  1706 }
  1707 
  1708 - (IBAction)pasteWithImagesAndColors:(id)sender
  1709 {
  1710 	[self _pasteWithPreferredSelector:@selector(pasteAsRichText:) sender:sender];	
  1711 }
  1712 
  1713 /*!
  1714  * @brief Send a paste message, using preferredSelector if possible and paste: if not
  1715  *
  1716  * Walks the responder chain looking for a responder which can handle pasting, skipping instances of
  1717  * WebHTMLView.  These are skipped because we can control what paste does to WebView (by using a custom subclass) but
  1718  * have no control over what the WebHTMLView would do.
  1719  *
  1720  * If no responder is found, repeats the process looking for the simpler paste: selector.
  1721  */
  1722 - (void)_pasteWithPreferredSelector:(SEL)selector sender:(id)sender
  1723 {
  1724 	NSWindow	*keyWindow = [[NSApplication sharedApplication] keyWindow];
  1725 	NSResponder	*responder;
  1726 
  1727 	//First, look for a responder which can handle the preferred selector
  1728 	if (!(responder = [keyWindow earliestResponderWhichRespondsToSelector:selector
  1729 														  andIsNotOfClass:NSClassFromString(@"WebHTMLView")])) {		
  1730 		//No responder found.  Try again, looking for one which will respond to paste:
  1731 		selector = @selector(paste:);
  1732 		responder = [keyWindow earliestResponderWhichRespondsToSelector:selector
  1733 														andIsNotOfClass:NSClassFromString(@"WebHTMLView")];
  1734 	}
  1735 
  1736 	//Sending pasteAsRichText: to a non rich text NSTextView won't do anything; change it to a generic paste:
  1737 	if ([responder isKindOfClass:[NSTextView class]] && ![(NSTextView *)responder isRichText]) {
  1738 		selector = @selector(paste:);
  1739 	}
  1740 
  1741 	if (selector) {
  1742 		[keyWindow makeFirstResponder:responder];
  1743 		[responder performSelector:selector
  1744 						withObject:sender];
  1745 	}
  1746 }
  1747 
  1748 //Custom Printing ------------------------------------------------------------------------------------------------------
  1749 #pragma mark Custom Printing
  1750 - (IBAction)adiumPrint:(id)sender
  1751 {
  1752 	//Pass the print command to the window, which is responsible for routing it to the correct place or
  1753 	//creating a view and printing.  Adium will not print from a window that does not respond to adiumPrint:
  1754 	NSWindow	*keyWindowController = [[[NSApplication sharedApplication] keyWindow] windowController];
  1755 	if ([keyWindowController respondsToSelector:@selector(adiumPrint:)]) {
  1756 		[keyWindowController performSelector:@selector(adiumPrint:)
  1757 								  withObject:sender];
  1758 	}
  1759 }
  1760 
  1761 #pragma mark Preferences Display
  1762 - (IBAction)showPreferenceWindow:(id)sender
  1763 {
  1764 	[adium.preferenceController showPreferenceWindow:sender];
  1765 }
  1766 
  1767 #pragma mark Font Panel
  1768 - (IBAction)toggleFontPanel:(id)sender
  1769 {
  1770 	if ([NSFontPanel sharedFontPanelExists] &&
  1771 		[[NSFontPanel sharedFontPanel] isVisible]) {
  1772 		[[NSFontPanel sharedFontPanel] close];
  1773 
  1774 	} else {
  1775 		NSFontPanel	*fontPanel = [NSFontPanel sharedFontPanel];
  1776 		
  1777 		if (!fontPanelAccessoryView) {
  1778 			[NSBundle loadNibNamed:@"FontPanelAccessoryView" owner:self];
  1779 			[fontPanel setAccessoryView:fontPanelAccessoryView];
  1780 		}
  1781 		
  1782 		[fontPanel orderFront:self]; 
  1783 	}
  1784 }
  1785 
  1786 - (IBAction)setFontPanelSettingsAsDefaultFont:(id)sender
  1787 {
  1788 	NSFont	*selectedFont = [[NSFontManager sharedFontManager] selectedFont];
  1789 
  1790 	[adium.preferenceController setPreference:[selectedFont stringRepresentation]
  1791 										 forKey:KEY_FORMATTING_FONT
  1792 										  group:PREF_GROUP_FORMATTING];
  1793 	
  1794 	//We can't get foreground/background color from the font panel so far as I can tell... so we do the best we can.
  1795 	NSWindow	*keyWindow = [[NSApplication sharedApplication] keyWindow];
  1796 	NSResponder *responder = [keyWindow firstResponder]; 
  1797 	if ([responder isKindOfClass:[NSTextView class]]) {
  1798 		NSDictionary	*typingAttributes = [(NSTextView *)responder typingAttributes];
  1799 		NSColor			*foregroundColor, *backgroundColor;
  1800 
  1801 		if ((foregroundColor = [typingAttributes objectForKey:NSForegroundColorAttributeName])) {
  1802 			[adium.preferenceController setPreference:[foregroundColor stringRepresentation]
  1803 												 forKey:KEY_FORMATTING_TEXT_COLOR
  1804 												  group:PREF_GROUP_FORMATTING];
  1805 		}
  1806 
  1807 		if ((backgroundColor = [typingAttributes objectForKey:AIBodyColorAttributeName])) {
  1808 			[adium.preferenceController setPreference:[backgroundColor stringRepresentation]
  1809 												 forKey:KEY_FORMATTING_BACKGROUND_COLOR
  1810 												  group:PREF_GROUP_FORMATTING];
  1811 		}
  1812 	}
  1813 }
  1814 
  1815 //Custom Dimming menu items --------------------------------------------------------------------------------------------
  1816 #pragma mark Custom Dimming menu items
  1817 //The standard ones do not dim correctly when unavailable
  1818 - (IBAction)toggleFontTrait:(id)sender
  1819 {
  1820     NSFontManager	*fontManager = [NSFontManager sharedFontManager];
  1821     
  1822     if ([fontManager traitsOfFont:[fontManager selectedFont]] & [sender tag]) {
  1823         [fontManager removeFontTrait:sender];
  1824     } else {
  1825         [fontManager addFontTrait:sender];
  1826     }
  1827 }
  1828 
  1829 - (void)toggleToolbarShown:(id)sender
  1830 {
  1831 	NSWindow	*window = [[NSApplication sharedApplication] keyWindow]; 	
  1832 	[window toggleToolbarShown:sender];
  1833 }
  1834 
  1835 - (void)runToolbarCustomizationPalette:(id)sender
  1836 {
  1837 	NSWindow	*window = [[NSApplication sharedApplication] keyWindow]; 	
  1838 	[window runToolbarCustomizationPalette:sender];
  1839 }
  1840 
  1841 //Menu item validation
  1842 - (BOOL)validateMenuItem:(NSMenuItem *)menuItem
  1843 {
  1844 	
  1845 	NSWindow	*keyWindow = [[NSApplication sharedApplication] keyWindow];
  1846 	NSResponder *responder = [keyWindow firstResponder]; 
  1847 	
  1848     if (menuItem == menuItem_bold || menuItem == menuItem_italic) {
  1849 		NSFont			*selectedFont = [[NSFontManager sharedFontManager] selectedFont];
  1850 		
  1851 		//We must be in a text view, have text on the pasteboard, and have a font that supports bold or italic
  1852 		if ([responder isKindOfClass:[NSTextView class]]) {
  1853 			return (menuItem == menuItem_bold ? [selectedFont supportsBold] : [selectedFont supportsItalics]);
  1854 		}
  1855 		return NO;
  1856 		
  1857 	} else if (menuItem == menuItem_paste || menuItem == menuItem_pasteAndMatchStyle || menuItem == menuItem_pasteWithImagesAndColors) {
  1858 
  1859 		//The user can paste if the pasteboard contains an image, some text, one or more files, or one or more URLs.
  1860 		NSPasteboard *pboard = [NSPasteboard generalPasteboard];
  1861 		NSArray *nonImageTypes = [NSArray arrayWithObjects:
  1862 			NSStringPboardType,
  1863 			NSRTFPboardType,
  1864 			NSURLPboardType,
  1865 			NSFilenamesPboardType,
  1866 			NSFilesPromisePboardType,
  1867 			NSRTFDPboardType,
  1868 			nil];
  1869 		return ([pboard availableTypeFromArray:nonImageTypes] != nil) || [NSImage canInitWithPasteboard:pboard];
  1870 	
  1871 	} else if (menuItem == menuItem_showToolbar) {
  1872 		[menuItem_showToolbar setTitle:([[keyWindow toolbar] isVisible] ? 
  1873 										AILocalizedString(@"Hide Toolbar",nil) : 
  1874 										AILocalizedString(@"Show Toolbar",nil))];
  1875 		return [keyWindow toolbar] != nil;
  1876 	
  1877 	} else if (menuItem == menuItem_customizeToolbar) {
  1878 		return ([keyWindow toolbar] != nil && [[keyWindow toolbar] isVisible] && [[keyWindow windowController] canCustomizeToolbar]);
  1879 
  1880 	} else if (menuItem == menuItem_close) {
  1881 		return (keyWindow && ([[keyWindow standardWindowButton:NSWindowCloseButton] isEnabled] ||
  1882 							  ([[keyWindow windowController] respondsToSelector:@selector(windowPermitsClose)] &&
  1883 							   [[keyWindow windowController] windowPermitsClose])));
  1884 		
  1885 	} else if (menuItem == menuItem_closeChat || menuItem == menuItem_clearDisplay) {
  1886 		return activeChat != nil;
  1887 		
  1888 	} else if( menuItem == menuItem_closeAllChats) {
  1889 		return [[self openChats] count] > 0;
  1890 
  1891 	} else if (menuItem == menuItem_print) {
  1892 		NSWindowController *windowController = [keyWindow windowController];
  1893 
  1894 		return ([windowController respondsToSelector:@selector(adiumPrint:)] &&
  1895 				(![windowController respondsToSelector:@selector(validatePrintMenuItem:)] ||
  1896 				 [windowController validatePrintMenuItem:menuItem]));
  1897 		
  1898 	} else if (menuItem == menuItem_showFonts) {
  1899 		[menuItem_showFonts setTitle:(([NSFontPanel sharedFontPanelExists] && [[NSFontPanel sharedFontPanel] isVisible]) ?
  1900 									  AILocalizedString(@"Hide Fonts",nil) :
  1901 									  AILocalizedString(@"Show Fonts",nil))];
  1902 		return YES;
  1903 	} else if (menuItem == menuItem_toggleUserlist || menuItem == menuItem_toggleUserlistSide) {
  1904 			return self.activeChat.isGroupChat;
  1905 	} else {
  1906 		return YES;
  1907 	}
  1908 }
  1909 
  1910 #pragma mark Window levels
  1911 - (NSMenu *)menuForWindowLevelsNotifyingTarget:(id)target
  1912 {
  1913 	NSMenu		*windowPositionMenu = [[NSMenu allocWithZone:[NSMenu zone]] init];
  1914 	NSMenuItem	*menuItem;
  1915 	
  1916 	menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Above other windows",nil)
  1917 																	target:target
  1918 																	action:@selector(selectedWindowLevel:)
  1919 															 keyEquivalent:@""];
  1920 	[menuItem setEnabled:YES];
  1921 	[menuItem setTag:AIFloatingWindowLevel];
  1922 	[windowPositionMenu addItem:menuItem];
  1923 	[menuItem release];
  1924 	
  1925 	menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Normally",nil)
  1926 																	target:target
  1927 																	action:@selector(selectedWindowLevel:)
  1928 															 keyEquivalent:@""];
  1929 	[menuItem setEnabled:YES];
  1930 	[menuItem setTag:AINormalWindowLevel];
  1931 	[windowPositionMenu addItem:menuItem];
  1932 	[menuItem release];
  1933 	
  1934 	menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:AILocalizedString(@"Below other windows",nil)
  1935 																	target:target
  1936 																	action:@selector(selectedWindowLevel:)
  1937 															 keyEquivalent:@""];
  1938 	[menuItem setEnabled:YES];
  1939 	[menuItem setTag:AIDesktopWindowLevel];
  1940 	[windowPositionMenu addItem:menuItem];
  1941 	[menuItem release];
  1942 	
  1943 	[windowPositionMenu setAutoenablesItems:NO];
  1944 
  1945 	return [windowPositionMenu autorelease];
  1946 }
  1947 
  1948 -(void)toggleUserlist:(id)sender
  1949 {
  1950 	[self.activeChat.chatContainer.chatViewController toggleUserList];
  1951 }
  1952 
  1953 -(void)toggleUserlistSide:(id)sender
  1954 {
  1955 	[self.activeChat.chatContainer.chatViewController toggleUserListSide];
  1956 }
  1957 
  1958 -(void)clearDisplay:(id)sender
  1959 {
  1960 	[self.activeChat.chatContainer.messageViewController.messageDisplayController clearView];
  1961 }
  1962 
  1963 @end