Source/AIInterfaceController.m
author Thijs Alkemade <thijsalkemade@gmail.com>
Sat Aug 28 22:01:12 2010 +0200 (21 months ago)
changeset 3277 21ab21e877e0
parent 3092 ffb42621b742
child 3310 72ccf4b32d4d
permissions -rw-r--r--
Add a Reopen Closed Tab menu item to the File menu that will restore the most recently closed tab, similar to Chrome. Fixes #12537

Does not work with MSN group chats (and probably other protocols that have unnamed MUCs).

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