Plugins/Dual Window Interface/AIMessageViewController.m
author Zachary West <zacw@adium.im>
Sun Oct 25 21:21:42 2009 -0400 (2009-10-25)
changeset 2776 862f4e3f05e1
parent 2118 9fbe80565319
child 2838 2d3f7242a158
permissions -rw-r--r--
Control more directly the completion range for group chats. Fixes #13237.

We now go back to the nearest whitespace and see if it would autocomplete any nicks at that point. If it would, we perform the autocompletion starting at that point. If it doesn't, we let the OS do its normal thing.

This fixes autocompleting things like "[mynick]" or the channel name (such as "#adium") since they start with non-word characters, but are perfectly valid.
     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 #import "AIMessageViewController.h"
    18 #import "AIAccountSelectionView.h"
    19 #import "AIMessageWindowController.h"
    20 #import "ESGeneralPreferencesPlugin.h"
    21 #import "AIDualWindowInterfacePlugin.h"
    22 #import "AIContactInfoWindowController.h"
    23 #import "AIMessageTabSplitView.h"
    24 #import "AIMessageWindowOutgoingScrollView.h"
    25 #import "KNShelfSplitView.h"
    26 #import "ESChatUserListController.h"
    27 
    28 #import <Adium/AIChatControllerProtocol.h>
    29 #import <Adium/AIContactAlertsControllerProtocol.h>
    30 #import <Adium/AIContactControllerProtocol.h>
    31 #import <Adium/AIContentControllerProtocol.h>
    32 #import <Adium/AIContentControllerProtocol.h>
    33 #import <Adium/AIInterfaceControllerProtocol.h>
    34 #import <Adium/AIMenuControllerProtocol.h>
    35 #import <Adium/AIToolbarControllerProtocol.h>
    36 #import <Adium/AIAccount.h>
    37 #import <Adium/AIChat.h>
    38 #import <Adium/AIContentMessage.h>
    39 #import <Adium/AIListContact.h>
    40 #import <Adium/AIMetaContact.h>
    41 #import <Adium/AIListObject.h>
    42 #import <Adium/AIListOutlineView.h>
    43 #import <Adium/AIServiceIcons.h>
    44 
    45 #import <AIUtilities/AIApplicationAdditions.h>
    46 #import <AIUtilities/AIAttributedStringAdditions.h>
    47 #import <AIUtilities/AIAutoScrollView.h>
    48 #import <AIUtilities/AIDictionaryAdditions.h>
    49 #import <AIUtilities/AISplitView.h>
    50 
    51 #import <PSMTabBarControl/NSBezierPath_AMShading.h>
    52 
    53 #import "RBSplitView.h"
    54 
    55 //Heights and Widths
    56 #define MESSAGE_VIEW_MIN_HEIGHT_RATIO		.50						//Mininum height ratio of the message view
    57 #define MESSAGE_VIEW_MIN_WIDTH_RATIO		.50						//Mininum width ratio of the message view
    58 #define ENTRY_TEXTVIEW_MIN_HEIGHT			20						//Mininum height of the text entry view
    59 #define USER_LIST_DEFAULT_WIDTH				120						//Default width of the user list
    60 
    61 //Preferences and files
    62 #define MESSAGE_VIEW_NIB					@"MessageView"			//Filename of the message view nib
    63 #define	USERLIST_THEME						@"UserList Theme"		//File name of the user list theme
    64 #define	USERLIST_LAYOUT						@"UserList Layout"		//File name of the user list layout
    65 #define	KEY_ENTRY_TEXTVIEW_MIN_HEIGHT		@"Minimum Text Height"	//Preference key for text entry height
    66 #define	KEY_ENTRY_USER_LIST_MIN_WIDTH		@"UserList Width"		//Preference key for user list width
    67 #define KEY_USER_LIST_VISIBLE_PREFIX		@"Userlist Visible Chat:" //Preference key prefix for user list visibility
    68 #define KEY_USER_LIST_ON_RIGHT				@"UserList On Right"	// Preference key for user list being on the right
    69 
    70 #define TEXTVIEW_HEIGHT_DEBUG
    71 
    72 @interface AIMessageViewController ()
    73 - (id)initForChat:(AIChat *)inChat;
    74 - (void)chatStatusChanged:(NSNotification *)notification;
    75 - (void)chatParticipatingListObjectsChanged:(NSNotification *)notification;
    76 - (void)_configureMessageDisplay;
    77 - (void)_createAccountSelectionView;
    78 - (void)_destroyAccountSelectionView;
    79 - (void)_configureTextEntryView;
    80 - (void)_updateTextEntryViewHeight;
    81 - (NSInteger)_textEntryViewProperHeightIgnoringUserMininum:(BOOL)ignoreUserMininum;
    82 - (void)_showUserListView;
    83 - (void)_hideUserListView;
    84 - (void)_configureUserList;
    85 - (void)_updateUserListViewWidth;
    86 - (NSInteger)_userListViewProperWidth;
    87 - (void)updateFramesForAccountSelectionView;
    88 - (void)saveUserListMinimumSize;
    89 - (BOOL)userListInitiallyVisible;
    90 - (void)setUserListVisible:(BOOL)inVisible;
    91 - (void)setupShelfView;
    92 - (void)updateUserCount;
    93 
    94 - (NSArray *)contactsMatchingBeginningString:(NSString *)partialWord;
    95 @end
    96 
    97 @implementation AIMessageViewController
    98 
    99 /*!
   100  * @brief Create a new message view controller
   101  */
   102 + (AIMessageViewController *)messageDisplayControllerForChat:(AIChat *)inChat
   103 {
   104     return [[[self alloc] initForChat:inChat] autorelease];
   105 }
   106 
   107 
   108 /*!
   109  * @brief Initialize
   110  */
   111 - (id)initForChat:(AIChat *)inChat
   112 {
   113     if ((self = [super init])) {
   114 		AIListContact	*contact;
   115 		//Init
   116 		chat = [inChat retain];
   117 		contact = chat.listObject;
   118 		view_accountSelection = nil;
   119 		userListController = nil;
   120 		suppressSendLaterPrompt = NO;
   121 		retainingScrollViewUserList = NO;
   122 		
   123 		//Load the view containing our controls
   124 		[NSBundle loadNibNamed:MESSAGE_VIEW_NIB owner:self];
   125 		
   126 		//Register for the various notification we need
   127 		[[NSNotificationCenter defaultCenter] addObserver:self
   128 									   selector:@selector(sendMessage:) 
   129 										   name:Interface_SendEnteredMessage
   130 										 object:chat];
   131 		[[NSNotificationCenter defaultCenter] addObserver:self
   132 									   selector:@selector(didSendMessage:)
   133 										   name:Interface_DidSendEnteredMessage 
   134 										 object:chat];
   135 		[[NSNotificationCenter defaultCenter] addObserver:self
   136 									   selector:@selector(chatStatusChanged:) 
   137 										   name:Chat_StatusChanged
   138 										 object:chat];
   139 		[[NSNotificationCenter defaultCenter] addObserver:self 
   140 									   selector:@selector(chatParticipatingListObjectsChanged:)
   141 										   name:Chat_ParticipatingListObjectsChanged
   142 										 object:chat];
   143 		[[NSNotificationCenter defaultCenter] addObserver:self
   144 									   selector:@selector(redisplaySourceAndDestinationSelector:) 
   145 										   name:Chat_SourceChanged
   146 										 object:chat];
   147 		[[NSNotificationCenter defaultCenter] addObserver:self
   148 									   selector:@selector(redisplaySourceAndDestinationSelector:) 
   149 										   name:Chat_DestinationChanged
   150 										 object:chat];
   151 
   152 		//Observe general preferences for sending keys
   153 		[adium.preferenceController registerPreferenceObserver:self forGroup:PREF_GROUP_GENERAL];
   154 		[adium.preferenceController registerPreferenceObserver:self forGroup:PREF_GROUP_DUAL_WINDOW_INTERFACE];
   155 
   156 		/* Update chat status and participating list objects to configure the user list if necessary
   157 		 * Call chatParticipatingListObjectsChanged first, which will set up the user list. This allows other sizing to match.
   158 		 */
   159 		[self setUserListVisible:(chat.isGroupChat && [self userListInitiallyVisible])];
   160 		
   161 		[self chatParticipatingListObjectsChanged:nil];
   162 		[self chatStatusChanged:nil];
   163 		
   164 		//Configure our views
   165 		[self _configureMessageDisplay];
   166 		[self _configureTextEntryView];
   167 
   168 		//Set our base writing direction
   169 		if (contact) {
   170 			initialBaseWritingDirection = [contact baseWritingDirection];
   171 			[textView_outgoing setBaseWritingDirection:initialBaseWritingDirection];
   172 		}
   173 	}
   174 
   175 	return self;
   176 }
   177 
   178 /*!
   179  * @brief Deallocate
   180  */
   181 - (void)dealloc
   182 {   
   183 	AIListContact	*contact = chat.listObject;
   184 	
   185 	[adium.preferenceController unregisterPreferenceObserver:self];
   186 
   187 	//Store our minimum height for the text entry area, and minimim width for the user list
   188 	[adium.preferenceController setPreference:[NSNumber numberWithInteger:entryMinHeight]
   189 										 forKey:KEY_ENTRY_TEXTVIEW_MIN_HEIGHT
   190 										  group:PREF_GROUP_DUAL_WINDOW_INTERFACE];
   191 
   192 	if (userListController) {
   193 		[self saveUserListMinimumSize];
   194 	}
   195 	
   196 	//Save the base writing direction
   197 	if (contact && initialBaseWritingDirection != [textView_outgoing baseWritingDirection])
   198 		[contact setBaseWritingDirection:[textView_outgoing baseWritingDirection]];
   199 
   200 	[chat release]; chat = nil;
   201 
   202 	//remove observers
   203 	[[NSNotificationCenter defaultCenter] removeObserver:self];
   204 	
   205     //Account selection view
   206 	[self _destroyAccountSelectionView];
   207 	
   208 	[messageDisplayController messageViewIsClosing];
   209     [messageDisplayController release];
   210 	[userListController release];
   211 
   212 	[controllerView_messages release];
   213 	
   214 	//Release the views for which we are responsible (because we loaded them via -[NSBundle loadNibNamed:owner])
   215 	[nibrootView_messageView release];
   216 	[nibrootView_shelfVew release];
   217 	[nibrootView_userList release];
   218 
   219 	//Release the hidden user list view
   220 	if (retainingScrollViewUserList) {
   221 		[scrollView_userList release];
   222 	}
   223 	//release menuItem
   224 	[showHide release];
   225 	
   226 	[undoManager release]; undoManager = nil;
   227 
   228     [super dealloc];
   229 }
   230 
   231 - (void)saveUserListMinimumSize
   232 {
   233 	[adium.preferenceController setPreference:[NSNumber numberWithInteger:userListMinWidth]
   234 										 forKey:KEY_ENTRY_USER_LIST_MIN_WIDTH
   235 										  group:PREF_GROUP_DUAL_WINDOW_INTERFACE];
   236 }
   237 
   238 - (void)updateGradientColors
   239 {
   240 	NSColor *darkerColor = [NSColor colorWithCalibratedWhite:0.90 alpha:1.0];
   241 	NSColor *lighterColor = [NSColor colorWithCalibratedWhite:0.92 alpha:1.0];
   242 	NSColor *leftColor = nil, *rightColor = nil;
   243 
   244 	switch ([messageWindowController tabPosition]) {
   245 		case AdiumTabPositionBottom:
   246 		case AdiumTabPositionTop:
   247 		case AdiumTabPositionLeft:
   248 			leftColor = lighterColor;
   249 			rightColor = darkerColor;
   250 			break;
   251 		case AdiumTabPositionRight:
   252 			leftColor = darkerColor;
   253 			rightColor = lighterColor;
   254 			break;
   255 	}
   256 
   257 	[view_accountSelection setLeftColor:leftColor rightColor:rightColor];
   258 	//XXX
   259 //	[splitView_textEntryHorizontal setLeftColor:leftColor rightColor:rightColor];
   260 }
   261 
   262 /*!
   263  * @brief Invoked before the message view closes
   264  *
   265  * This method is invoked before our message view controller's message view leaves a window.
   266  * We need to clean up our user list to invalidate cursor tracking before the view closes.
   267  */
   268 - (void)messageViewWillLeaveWindowController:(AIMessageWindowController *)inWindowController
   269 {
   270 	if (inWindowController) {
   271 		[userListController contactListWillBeRemovedFromWindow];
   272 	}
   273 	
   274 	[messageWindowController release]; messageWindowController = nil;
   275 }
   276 
   277 - (void)messageViewAddedToWindowController:(AIMessageWindowController *)inWindowController
   278 {
   279 	if (inWindowController) {
   280 		[userListController contactListWasAddedBackToWindow];
   281 	}
   282 	
   283 	if (inWindowController != messageWindowController) {
   284 		[messageWindowController release];
   285 		messageWindowController = [inWindowController retain];
   286 		
   287 		[self updateGradientColors];
   288 	}
   289 }
   290 
   291 /*!
   292  * @brief Retrieve the chat represented by this message view
   293  */
   294 - (AIChat *)chat
   295 {
   296     return chat;
   297 }
   298 
   299 /*!
   300  * @brief Retrieve the source account associated with this chat
   301  */
   302 - (AIAccount *)account
   303 {
   304     return chat.account;
   305 }
   306 
   307 /*!
   308  * @brief Retrieve the destination list object associated with this chat
   309  */
   310 - (AIListContact *)listObject
   311 {
   312     return chat.listObject;
   313 }
   314 
   315 /*!
   316  * @brief Returns the selected list object in our participants list
   317  */
   318 - (AIListObject *)preferredListObject
   319 {
   320 	if (userListView) { //[[shelfView subviews] containsObject:scrollView_userList] && ([userListView selectedRow] != -1)
   321 		return [userListView itemAtRow:[userListView selectedRow]];
   322 	}
   323 	
   324 	return nil;
   325 }
   326 
   327 /*!
   328  * @brief Invoked when the status of our chat changes
   329  *
   330  * The only chat status change we're interested in is one to the disallow account switching flag.  When this flag 
   331  * changes we update the visibility of our account status menus accordingly.
   332  */
   333 - (void)chatStatusChanged:(NSNotification *)notification
   334 {
   335     NSArray	*modifiedKeys = [[notification userInfo] objectForKey:@"Keys"];
   336 	
   337     if (notification == nil || [modifiedKeys containsObject:@"DisallowAccountSwitching"]) {
   338 		[self setAccountSelectionMenuVisibleIfNeeded:YES];
   339     }
   340 }
   341 
   342 
   343 //Message Display ------------------------------------------------------------------------------------------------------
   344 #pragma mark Message Display
   345 /*!
   346  * @brief Configure the message display view
   347  */
   348 - (void)_configureMessageDisplay
   349 {
   350 	//Create the message view
   351 	messageDisplayController = [[adium.interfaceController messageDisplayControllerForChat:chat] retain];
   352 	//Get the messageView from the controller
   353 	controllerView_messages = [[messageDisplayController messageView] retain];
   354 
   355 	/* customView_messages is really just a placeholder.  It's a subview of scrollView_messages, which exists just
   356 	 * to draw a box around itself to give the desired border. NSBox could be used for the same purpose.
   357 	 * We replace customView_messages with the actual message view we want to use, controllerView_messages.
   358 	 *
   359 	 * Note that this does -not- change the documentView of scrollView_messages, which remains NULL.
   360 	 * This is because the controllerView_messages supplies its own scroll view (within the WebView).
   361 	 * We therefore use -[AIMessageWindowOutgoingScrollView setAccessibilityChild:] to manage the accessibility
   362 	 * heirarchy.
   363 	 */
   364 	[controllerView_messages setFrame:[scrollView_messages documentVisibleRect]];
   365 	[scrollView_messages setAccessibilityChild:controllerView_messages];
   366 	[[customView_messages superview] replaceSubview:customView_messages with:controllerView_messages];
   367 
   368 	//This is what draws our transparent background
   369 	//Technically, it could be set in MessageView.nib, too
   370 	[scrollView_messages setBackgroundColor:[NSColor clearColor]];
   371 
   372 	[textView_outgoing setNextResponder:view_contents];
   373 	
   374 	[controllerView_messages setNextResponder:textView_outgoing];
   375 }
   376 
   377 /*!
   378  * @brief The message display controller
   379  */
   380 - (NSObject<AIMessageDisplayController> *)messageDisplayController
   381 {
   382 	return messageDisplayController;
   383 }
   384 
   385 /*!
   386  * @brief Access to our view
   387  */
   388 - (NSView *)view
   389 {
   390     return view_contents;
   391 }
   392 
   393 - (NSScrollView *)messagesScrollView
   394 {
   395 	return scrollView_messages;
   396 }
   397 
   398 /*!
   399  * @brief Support for printing.  Forward the print command to our message display view
   400  */
   401 - (void)adiumPrint:(id)sender
   402 {
   403 	if ([messageDisplayController respondsToSelector:@selector(adiumPrint:)]) {
   404 		[messageDisplayController adiumPrint:sender];
   405 	}
   406 }
   407 
   408 
   409 //Messaging ------------------------------------------------------------------------------------------------------------
   410 #pragma mark Messaging
   411 /*!
   412  * @brief Send the entered message
   413  */
   414 - (IBAction)sendMessage:(id)sender
   415 {
   416 	NSAttributedString	*attributedString = [textView_outgoing textStorage];
   417 	
   418 	//Only send if we have a non-zero-length string
   419     if ([attributedString length] != 0) { 
   420 		AIListObject				*listObject = chat.listObject;
   421 
   422 		//If user typed command /clear, reset the content of the view
   423 		if ([[attributedString string] caseInsensitiveCompare:AILocalizedString(@"/clear", "Command which will clear the message area of a chat. Please include the '/' at the front of your translation.")] == NSOrderedSame) {
   424 			//Reset the content of the view
   425 			[messageDisplayController clearView];
   426 
   427 			//Reset the content of the text field, removing the command as it has been executed
   428 			[self clearTextEntryView];
   429 
   430 			//Commands are not messages, so they don't have to be sent
   431 			return;
   432 		}
   433 		
   434 		if (chat.isGroupChat && !chat.account.online) {
   435 			//Refuse to do anything with a group chat for an offline account.
   436 			NSBeep();
   437 			return;
   438 		}
   439 
   440 		AIChatSendingAbilityType messageSendingAbility = chat.messageSendingAbility;
   441 		if (suppressSendLaterPrompt || (messageSendingAbility == AIChatCanSendMessageNow) ||
   442 			((messageSendingAbility == AIChatCanSendViaServersideOfflineMessage) && chat.account.sendOfflineMessagesWithoutPrompting)) {
   443 			AIContentMessage		*message;
   444 			NSAttributedString		*outgoingAttributedString;
   445 			AIAccount				*account = chat.account;
   446 			//Send the message
   447 			[[NSNotificationCenter defaultCenter] postNotificationName:Interface_WillSendEnteredMessage
   448 													  object:chat
   449 													userInfo:nil];
   450 			
   451 			outgoingAttributedString = [attributedString copy];
   452 			message = [AIContentMessage messageInChat:chat
   453 										   withSource:account
   454 										  destination:chat.listObject
   455 												 date:nil //created for us by AIContentMessage
   456 											  message:outgoingAttributedString
   457 											autoreply:NO];
   458 			[outgoingAttributedString release];
   459 			
   460 			if ([adium.contentController sendContentObject:message]) {
   461 				[[NSNotificationCenter defaultCenter] postNotificationName:Interface_DidSendEnteredMessage 
   462 														  object:chat
   463 														userInfo:nil];
   464 			}
   465 			/* If we sent with AIChatCanSendViaServersideOfflineMessage, we should probably show a status message to
   466 			 * the effect AILocalizedString(@"Your message has been sent. %@ will receive it when online.", nil)
   467 			 */
   468 		} else {
   469 			NSString							*formattedUID = listObject.formattedUID;
   470 			
   471 			NSAlert *alert = [[NSAlert alloc] init];
   472 			NSImage *icon = ([listObject userIcon] ? [listObject userIcon] : [AIServiceIcons serviceIconForObject:listObject
   473 																											 type:AIServiceIconLarge
   474 																										direction:AIIconNormal]);
   475 			icon = [[icon copy] autorelease];
   476 			[icon setScalesWhenResized:NO];
   477 			[alert setIcon:icon];
   478 			[alert setAlertStyle:NSInformationalAlertStyle];
   479 			
   480 			[alert setMessageText:[NSString stringWithFormat:AILocalizedString(@"%@ appears to be offline. How do you want to send this message?", nil),
   481 								   formattedUID]];
   482 
   483 			switch (messageSendingAbility) {
   484 				case AIChatCanSendViaServersideOfflineMessage:
   485 				{
   486 					[alert setInformativeText:[NSString stringWithFormat:
   487 											   AILocalizedString(@"Send Now will deliver your message to the server immediately. %@ will receive the message the next time he or she signs on, even if you are no longer online.\n\nSend When Both Online will send the message the next time both you and %@ are known to be online and you are connected using Adium on this computer.", "Send Later dialogue explanation text for accounts supporting offline messaging support."),
   488 											   formattedUID, formattedUID]];
   489 					[alert addButtonWithTitle:AILocalizedString(@"Send Now", nil)];
   490 					
   491 					[alert addButtonWithTitle:AILocalizedString(@"Send When Both Online", nil)];
   492 					[[[alert buttons] objectAtIndex:1] setKeyEquivalent:@"b"];
   493 					[[[alert buttons] objectAtIndex:1] setKeyEquivalentModifierMask:0];
   494 
   495 					break;
   496 				}
   497 				case AIChatMayNotBeAbleToSendMessage:
   498 				{
   499 					[alert setInformativeText:[NSString stringWithFormat:
   500 											   AILocalizedString(@"Send Later will send the message the next time both you and %@ are online. Send Now may work if %@ is invisible or is not on your contact list and so only appears to be offline.", "Send Later dialogue explanation text"),
   501 											   formattedUID, formattedUID, formattedUID]];
   502 					[alert addButtonWithTitle:AILocalizedString(@"Send Now", nil)];
   503 					
   504 					[alert addButtonWithTitle:AILocalizedString(@"Send Later", nil)];
   505 					[[[alert buttons] objectAtIndex:1] setKeyEquivalent:@"l"];
   506 					[[[alert buttons] objectAtIndex:1] setKeyEquivalentModifierMask:0];
   507 					
   508 					break;
   509 				}
   510 				case AIChatCanNotSendMessage:
   511 				{
   512 					[alert setInformativeText:[NSString stringWithFormat:
   513 											   AILocalizedString(@"Send Later will send the message the next time both you and %@ are online.", "Send Later dialogue explanation text"),
   514 											   formattedUID, formattedUID, formattedUID]];					
   515 					[alert addButtonWithTitle:AILocalizedString(@"Send Later", nil)];
   516 					[[[alert buttons] objectAtIndex:0] setKeyEquivalent:@"l"];
   517 					[[[alert buttons] objectAtIndex:0] setKeyEquivalentModifierMask:0];
   518 					
   519 					break;
   520 				}
   521 				case AIChatCanSendMessageNow:
   522 				{
   523 					//We will never get here.
   524 					break;
   525 				}
   526 			}
   527 
   528 			[alert addButtonWithTitle:AILocalizedString(@"Don't Send", nil)];
   529 
   530 			NSButton *dontSendButton = ((messageSendingAbility == AIChatCanNotSendMessage) ?
   531 										[[alert buttons] objectAtIndex:1] :
   532 										[[alert buttons] objectAtIndex:2]);
   533 			[dontSendButton setKeyEquivalent:@"\E"];
   534 			[dontSendButton setKeyEquivalentModifierMask:0];
   535 			
   536 			[alert beginSheetModalForWindow:[view_contents window]
   537 							  modalDelegate:[self retain] /* Will release after the sheet ends */
   538 							 didEndSelector:@selector(alertDidEnd:returnCode:contextInfo:)
   539                                 contextInfo:[[NSNumber numberWithInteger:messageSendingAbility] retain] /* Will release after the sheet ends */];
   540 			[alert release];
   541 		}
   542     }
   543 }
   544 
   545 /*!
   546  * @brief Send Later button was pressed
   547  */ 
   548 - (void)alertDidEnd:(NSAlert *)alert returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo
   549 {
   550 	AIChatSendingAbilityType messageSendingAbility = [(NSNumber *)contextInfo integerValue];
   551 
   552 	switch (returnCode) {
   553 		case NSAlertFirstButtonReturn:
   554 			/* The AIChatCanNotSendMessage dalogue has Send Later as the first choice;
   555 			 * all others have Send Now as the first choice.
   556 			 */
   557 			if (messageSendingAbility == AIChatCanNotSendMessage) {
   558 				 /* Send Later */
   559 				[self sendMessageLater:nil];
   560 
   561 			} else {
   562 				 /* Send Now */
   563 				suppressSendLaterPrompt = YES;
   564 				[self sendMessage:nil];
   565 			}
   566 			break;
   567 			
   568 		case NSAlertSecondButtonReturn:
   569 			/* The AIChatCanNotSendMessage dalogue has Cancel as the second choice;
   570 			 * all others have Send Later as the first choice.
   571 			 */
   572 			if (messageSendingAbility != AIChatCanNotSendMessage) {
   573 				/* Send Later */
   574 				[self sendMessageLater:nil];
   575 			}			
   576 			break;
   577 
   578 		case NSAlertThirdButtonReturn: /* Don't Send */
   579 			break;		
   580 	}
   581 	
   582 	//Retained when the alert was created to guard against a crash if the chat tab being closed while we are open
   583 	[self release];
   584 	[(NSNumber *)contextInfo release];
   585 }
   586 
   587 /*!
   588  * @brief Invoked after our entered message sends
   589  *
   590  * This method hides the account selection view and clears the entered message after our message sends
   591  */
   592 - (IBAction)didSendMessage:(id)sender
   593 {
   594     [self setAccountSelectionMenuVisibleIfNeeded:NO];
   595     [self clearTextEntryView];
   596 }
   597 
   598 /*!
   599  * @brief Offline messaging
   600  */
   601 - (IBAction)sendMessageLater:(id)sender
   602 {
   603 	//If the chat can _now_ send a message, send it immediately instead of waiting for "later".
   604 	if ([chat messageSendingAbility] == AIChatCanSendMessageNow) {
   605 		[self sendMessage:sender];
   606 		return;
   607 	}
   608 
   609 	//Put the alert on the metaContact containing this listContact if applicable
   610 	AIMetaContact *listContact = chat.listObject.metaContact;
   611 
   612 	if (listContact) {
   613 		NSMutableDictionary *detailsDict, *alertDict;
   614 		
   615 		detailsDict = [NSMutableDictionary dictionary];
   616 		[detailsDict setObject:chat.account.internalObjectID forKey:@"Account ID"];
   617 		[detailsDict setObject:[NSNumber numberWithBool:YES] forKey:@"Allow Other"];
   618 		[detailsDict setObject:listContact.internalObjectID forKey:@"Destination ID"];
   619 
   620 		alertDict = [NSMutableDictionary dictionary];
   621 		[alertDict setObject:detailsDict forKey:@"ActionDetails"];
   622 		[alertDict setObject:CONTACT_SEEN_ONLINE_YES forKey:@"EventID"];
   623 		[alertDict setObject:@"SendMessage" forKey:@"ActionID"];
   624 		[alertDict setObject:[NSNumber numberWithBool:YES] forKey:@"OneTime"]; 
   625 		
   626 		[alertDict setObject:listContact forKey:@"TEMP-ListContact"];
   627 		
   628 		[adium.contentController filterAttributedString:[[[textView_outgoing textStorage] copy] autorelease]
   629 										  usingFilterType:AIFilterContent
   630 												direction:AIFilterOutgoing
   631 											filterContext:listContact
   632 										  notifyingTarget:self
   633 												 selector:@selector(gotFilteredMessageToSendLater:receivingContext:)
   634 												  context:alertDict];
   635 
   636 		[self didSendMessage:nil];
   637 	}
   638 }
   639 
   640 /*!
   641  * @brief Offline messaging
   642  */
   643 //XXX - Offline messaging code SHOULD NOT BE IN HERE! -ai
   644 - (void)gotFilteredMessageToSendLater:(NSAttributedString *)filteredMessage receivingContext:(NSMutableDictionary *)alertDict
   645 {
   646 	NSMutableDictionary	*detailsDict;
   647 	AIListContact		*listContact;
   648 	
   649 	detailsDict = [alertDict objectForKey:@"ActionDetails"];
   650 	[detailsDict setObject:[filteredMessage dataRepresentation] forKey:@"Message"];
   651 
   652 	listContact = [[alertDict objectForKey:@"TEMP-ListContact"] retain];
   653 	[alertDict removeObjectForKey:@"TEMP-ListContact"];
   654 	
   655 	[adium.contactAlertsController addAlert:alertDict 
   656 								 toListObject:listContact
   657 							 setAsNewDefaults:NO];
   658 	[listContact release];
   659 }
   660 
   661 //Account Selection ----------------------------------------------------------------------------------------------------
   662 #pragma mark Account Selection
   663 /*!
   664  * @brief
   665  */
   666 - (void)accountSelectionViewFrameDidChange:(NSNotification *)notification
   667 {
   668 	[self updateFramesForAccountSelectionView];
   669 }
   670 
   671 /*!
   672  * @brief Redisplay the source/destination account selector
   673  */
   674 - (void)redisplaySourceAndDestinationSelector:(NSNotification *)notification
   675 {
   676 	// Update the textView's chat source, in case any attributes it monitors changed.
   677 	[textView_outgoing setChat:chat];
   678 	[self setAccountSelectionMenuVisibleIfNeeded:YES];
   679 }
   680 
   681 /*!
   682  * @brief Toggle visibility of the account selection menus
   683  *
   684  * Invoking this method with NO will hide the account selection menus.  Invoking it with YES will show the account
   685  * selection menus if they are needed.
   686  */
   687 - (void)setAccountSelectionMenuVisibleIfNeeded:(BOOL)makeVisible
   688 {
   689 	//Hide or show the account selection view as requested
   690 	if (makeVisible) {
   691 		[self _createAccountSelectionView];
   692 	} else {
   693 		[self _destroyAccountSelectionView];
   694 	}
   695 }
   696 
   697 /*!
   698  * @brief Show the account selection view
   699  */
   700 - (void)_createAccountSelectionView
   701 {
   702 	if (!view_accountSelection) {
   703 		NSRect	contentFrame = [splitView_textEntryHorizontal frame];
   704 
   705 		//Create the account selection view and insert it into our window
   706 		view_accountSelection = [[AIAccountSelectionView alloc] initWithFrame:contentFrame];
   707 
   708 		[view_accountSelection setAutoresizingMask:(NSViewWidthSizable | NSViewMinYMargin)];
   709 		
   710 		[self updateGradientColors];
   711 		
   712 		//Insert the account selection view at the top of our view
   713 		[[shelfView contentView] addSubview:view_accountSelection];
   714 		[view_accountSelection setChat:chat];
   715 
   716 		[[NSNotificationCenter defaultCenter] addObserver:self
   717 												 selector:@selector(accountSelectionViewFrameDidChange:)
   718 													 name:AIViewFrameDidChangeNotification
   719 												   object:view_accountSelection];
   720 		
   721 		[self updateFramesForAccountSelectionView];
   722 			
   723 		//Redisplay everything
   724 		[[shelfView contentView] setNeedsDisplay:YES];
   725 	} else {
   726 		[view_accountSelection setChat:chat];
   727 	}
   728 }
   729 
   730 /*!
   731  * @brief Hide the account selection view
   732  */
   733 - (void)_destroyAccountSelectionView
   734 {
   735 	if (view_accountSelection) {
   736 		//Remove the observer
   737 		[[NSNotificationCenter defaultCenter] removeObserver:self
   738 														name:AIViewFrameDidChangeNotification
   739 													  object:view_accountSelection];
   740 
   741 		//Remove the account selection view from our window, clean it up
   742 		[view_accountSelection removeFromSuperview];
   743 		[view_accountSelection release]; view_accountSelection = nil;
   744 
   745 		//Redisplay everything
   746 		[self updateFramesForAccountSelectionView];
   747 	}
   748 }
   749 
   750 /*!
   751  * @brief Position the account selection view, if it is present, and the messages/text entry splitview appropriately
   752  */
   753 - (void)updateFramesForAccountSelectionView
   754 {
   755 	NSInteger 	accountSelectionHeight = (view_accountSelection ? [view_accountSelection frame].size.height : 0);
   756 
   757 	if (view_accountSelection) {
   758 		[view_accountSelection setFrameOrigin:NSMakePoint(NSMinX([view_accountSelection frame]), NSHeight([[view_accountSelection superview] frame]) - accountSelectionHeight)];
   759 		[view_accountSelection setNeedsDisplay:YES];
   760 	}
   761 
   762 	NSRect splitView_textEntryHorizontalFrame = [splitView_textEntryHorizontal frame];
   763 	splitView_textEntryHorizontalFrame.size.height = NSHeight([[splitView_textEntryHorizontal superview] frame]) - accountSelectionHeight - NSMinY(splitView_textEntryHorizontalFrame);
   764 	[splitView_textEntryHorizontal setFrame:splitView_textEntryHorizontalFrame];
   765 
   766 	[splitView_textEntryHorizontal setNeedsDisplay:YES];
   767 }	
   768 
   769 
   770 //Text Entry -----------------------------------------------------------------------------------------------------------
   771 #pragma mark Text Entry
   772 /*!
   773  * @brief Preferences changed, update sending keys
   774  */
   775 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key object:(AIListObject *)object
   776 					preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
   777 {
   778 	if ([group isEqualToString:PREF_GROUP_GENERAL]) {
   779 		[textView_outgoing setSendOnReturn:[[prefDict objectForKey:SEND_ON_RETURN] boolValue]];
   780 		[textView_outgoing setSendOnEnter:[[prefDict objectForKey:SEND_ON_ENTER] boolValue]];
   781 	} else if ([group isEqualToString:PREF_GROUP_DUAL_WINDOW_INTERFACE]) {
   782 		
   783 		if (firstTime || [key isEqualToString:KEY_ENTRY_USER_LIST_MIN_WIDTH]) {
   784 			NSInteger oldWidth = userListMinWidth;
   785 			
   786 			userListMinWidth = [[prefDict objectForKey:KEY_ENTRY_USER_LIST_MIN_WIDTH] integerValue];
   787 			
   788 			if (oldWidth != userListMinWidth) {
   789 				[shelfView setShelfWidth:userListMinWidth];
   790 			}
   791 		}
   792 		
   793 		if (firstTime || [key isEqualToString:KEY_USER_LIST_ON_RIGHT]) {
   794 			userListOnRight = [[prefDict objectForKey:KEY_USER_LIST_ON_RIGHT] boolValue];
   795 
   796 			[shelfView setShelfOnRight:userListOnRight];
   797 		}
   798 	}
   799 }
   800 
   801 /*!
   802  * @brief Configure the text entry view
   803  */
   804 - (void)_configureTextEntryView
   805 {	
   806 	//Configure the text entry view
   807     [textView_outgoing setTarget:self action:@selector(sendMessage:)];
   808 
   809 	//This is necessary for tab completion.
   810 	[textView_outgoing setDelegate:self];
   811     
   812 	[textView_outgoing setTextContainerInset:NSMakeSize(0,2)];
   813     if ([textView_outgoing respondsToSelector:@selector(setUsesFindPanel:)]) {
   814 		[textView_outgoing setUsesFindPanel:YES];
   815     }
   816 	[textView_outgoing setClearOnEscape:YES];
   817 	[textView_outgoing setTypingAttributes:[adium.contentController defaultFormattingAttributes]];
   818 	
   819 	//User's choice of mininum height for their text entry view
   820 	entryMinHeight = [[adium.preferenceController preferenceForKey:KEY_ENTRY_TEXTVIEW_MIN_HEIGHT
   821 															   group:PREF_GROUP_DUAL_WINDOW_INTERFACE] integerValue];
   822 	if (entryMinHeight <= 0) entryMinHeight = [self _textEntryViewProperHeightIgnoringUserMininum:YES];
   823 	
   824 	//Associate the view with our message view so it knows which view to scroll in response to page up/down
   825 	//and other special key-presses.
   826 	[textView_outgoing setAssociatedView:[messageDisplayController messageScrollView]];
   827 	
   828 	//Associate the text entry view with our chat and inform Adium that it exists.
   829 	//This is necessary for text entry filters to work correctly.
   830 	[textView_outgoing setChat:chat];
   831 	
   832     //Observe text entry view size changes so we can dynamically resize as the user enters text
   833     [[NSNotificationCenter defaultCenter] addObserver:self
   834 											 selector:@selector(outgoingTextViewDesiredSizeDidChange:)
   835 												 name:AIViewDesiredSizeDidChangeNotification 
   836 											   object:textView_outgoing];
   837 
   838 	[self _updateTextEntryViewHeight];
   839 }
   840 
   841 /*!
   842  * @brief Sets our text entry view as the first responder
   843  */
   844 - (void)makeTextEntryViewFirstResponder
   845 {
   846     [[textView_outgoing window] makeFirstResponder:textView_outgoing];
   847 }
   848 
   849 - (void)didSelect
   850 {
   851 	[self makeTextEntryViewFirstResponder];
   852 	
   853 	/* When we're selected, it's as if the user list controller is back in the window */
   854 	[userListController contactListWasAddedBackToWindow];
   855 }
   856 
   857 - (void)willDeselect
   858 {
   859 	/* When we're deselected (backgrounded), the user list controller is effectively out of the window */
   860 	[userListController contactListWillBeRemovedFromWindow];
   861 	// Mark the current location in the message display for this change, if it's not an inactive-switch.
   862 	if (messageWindowController.window.isKeyWindow) {
   863 		[messageDisplayController markForFocusChange];
   864 	}
   865 }
   866 
   867 /*!
   868  * @brief Returns the Text Entry View
   869  *
   870  * Make sure you need to use this. If you just need to enter text, see -addToTextEntryView:
   871  */
   872 - (AIMessageEntryTextView *)textEntryView
   873 {
   874 	return textView_outgoing;
   875 }
   876 
   877 /*!
   878  * @brief Clear the message entry text view
   879  */
   880 - (void)clearTextEntryView
   881 {
   882 	NSWritingDirection	writingDirection;
   883 
   884 	writingDirection = [textView_outgoing baseWritingDirection];
   885 	
   886 	[textView_outgoing setString:@""];
   887 	[textView_outgoing setTypingAttributes:[adium.contentController defaultFormattingAttributes]];
   888 	
   889 	[textView_outgoing setBaseWritingDirection:writingDirection];	//Preserve the writing diraction
   890 
   891     [[NSNotificationCenter defaultCenter] postNotificationName:NSTextDidChangeNotification
   892 														object:textView_outgoing];
   893 }
   894 
   895 /*!
   896  * @brief Add text to the message entry text view 
   897  *
   898  * Adds the passed string to the entry text view at the insertion point.  If there is selected text in the view, it
   899  * will be replaced.
   900  */
   901 - (void)addToTextEntryView:(NSAttributedString *)inString
   902 {
   903     [textView_outgoing insertText:inString];
   904     [[NSNotificationCenter defaultCenter] postNotificationName:NSTextDidChangeNotification object:textView_outgoing];
   905 }
   906 
   907 /*!
   908  * @brief Add data to the message entry text view 
   909  *
   910  * Adds the passed pasteboard data to the entry text view at the insertion point.  If there is selected text in the
   911  * view, it will be replaced.
   912  */
   913 - (void)addDraggedDataToTextEntryView:(id <NSDraggingInfo>)draggingInfo
   914 {
   915     [textView_outgoing performDragOperation:draggingInfo];
   916     [[NSNotificationCenter defaultCenter] postNotificationName:NSTextDidChangeNotification object:textView_outgoing];
   917 }
   918 
   919 /*!
   920  * @brief Update the text entry view's height when its desired size changes
   921  */
   922 - (void)outgoingTextViewDesiredSizeDidChange:(NSNotification *)notification
   923 {
   924 	[self _updateTextEntryViewHeight];
   925 }
   926 
   927 - (void)tabViewDidChangeVisibility
   928 {
   929 	[self _updateTextEntryViewHeight];
   930 }
   931 
   932 /* 
   933  * @brief Update the height of our text entry view
   934  *
   935  * This method sets the height of the text entry view to the most ideal value, and adjusts the other views in our
   936  * window to fill the remaining space.
   937  */
   938 - (void)_updateTextEntryViewHeight
   939 {
   940 	NSInteger		height = [self _textEntryViewProperHeightIgnoringUserMininum:NO];
   941 	//Display the vertical scroller if our view is not tall enough to display all the entered text
   942 	[scrollView_outgoing setHasVerticalScroller:(height < [textView_outgoing desiredSize].height)];
   943 	
   944 	//First, set the text entry subview to the exact height we want
   945 	[[splitView_textEntryHorizontal subviewAtPosition:1] setMinDimension:height andMaxDimension:height];
   946 	[splitView_textEntryHorizontal adjustSubviews];
   947 	
   948 	//Now, allow it to be resized again between the text view's minimum size and the max size which is based on the splitview's height
   949 	[[splitView_textEntryHorizontal subviewAtPosition:1] setMinDimension:[self _textEntryViewProperHeightIgnoringUserMininum:YES] andMaxDimension:([splitView_textEntryHorizontal frame].size.height * MESSAGE_VIEW_MIN_HEIGHT_RATIO)];
   950 }
   951 
   952 /*!
   953  * @brief Returns the height our text entry view should be
   954  *
   955  * This method takes into account user preference, the amount of entered text, and the current window size to return
   956  * a height which is most ideal for the text entry view.
   957  *
   958  * @param ignoreUserMininum If YES, the user's preference for mininum height will be ignored
   959  */
   960 - (NSInteger)_textEntryViewProperHeightIgnoringUserMininum:(BOOL)ignoreUserMininum
   961 {
   962 	NSInteger dividerThickness = [splitView_textEntryHorizontal dividerThickness];
   963 	NSInteger allowedHeight = ([splitView_textEntryHorizontal frame].size.height / 2.0) - dividerThickness;
   964 	NSInteger	height;
   965 	
   966 	//Our primary goal is to display all the entered text
   967 	height = [textView_outgoing desiredSize].height;
   968 
   969 	//But we must never fall below the user's prefered mininum or above the allowed height
   970 	if (!ignoreUserMininum && height < entryMinHeight) {
   971 		height = entryMinHeight;
   972 	}
   973 	if (height > allowedHeight) height = allowedHeight;
   974 	
   975 	return height;
   976 }
   977 
   978 #pragma mark Autocompletion
   979 - (BOOL)canTabCompleteForPartialWord:(NSString *)partialWord
   980 {
   981 	return ([self contactsMatchingBeginningString:partialWord].count > 0 ||
   982 			[self.chat.displayName rangeOfString:partialWord options:(NSDiacriticInsensitiveSearch | NSCaseInsensitiveSearch | NSAnchoredSearch)].location != NSNotFound);
   983 }
   984 
   985 /*!
   986  * @brief Should the tab key cause an autocompletion if possible?
   987  *
   988  * We only tab to autocomplete for a group chat
   989  */
   990 - (BOOL)textViewShouldTabComplete:(NSTextView *)inTextView
   991 {
   992 	if (self.chat.isGroupChat) {
   993 		NSRange completionRange = inTextView.rangeForUserCompletion;
   994 		NSString *partialWord = [inTextView.textStorage.string substringWithRange:completionRange];
   995 		return [self canTabCompleteForPartialWord:partialWord]; 
   996 	}
   997 	
   998 	return NO;
   999 }
  1000 
  1001 - (NSRange)textView:(NSTextView *)inTextView rangeForCompletion:(NSRange)charRange
  1002 {
  1003 	if (self.chat.isGroupChat && charRange.location > 0) {
  1004 		NSString *partialWord = nil;
  1005 		NSString *allText = [inTextView.textStorage.string substringWithRange:NSMakeRange(0, NSMaxRange(charRange))];
  1006 		NSRange whitespacePosition = [allText rangeOfCharacterFromSet:[NSCharacterSet whitespaceCharacterSet] options:NSBackwardsSearch];
  1007 		
  1008 		if (whitespacePosition.location == NSNotFound) {
  1009 			// We went back to the beginning of the string and still didn't find a whitespace; use the whole thing.
  1010 			partialWord = allText;
  1011 			whitespacePosition = NSMakeRange(0, 0);
  1012 		} else {
  1013 			// We found a whitespace, use from it until our current position.
  1014 			partialWord = [allText substringWithRange:NSMakeRange(NSMaxRange(whitespacePosition), allText.length - NSMaxRange(whitespacePosition))];
  1015 		}
  1016 		
  1017 		// If this matches any contacts or the room name, use this new range for autocompletion.
  1018 		if ([self canTabCompleteForPartialWord:partialWord]) {
  1019 			charRange = NSMakeRange(NSMaxRange(whitespacePosition), allText.length - NSMaxRange(whitespacePosition));
  1020 		}
  1021 	}
  1022 	
  1023 	return charRange;
  1024 }
  1025 
  1026 - (NSArray *)contactsMatchingBeginningString:(NSString *)partialWord
  1027 {
  1028 	NSMutableArray *contacts = [NSMutableArray array];
  1029 	
  1030 	for (AIListContact *listContact in self.chat) {
  1031 		if ([listContact.UID rangeOfString:partialWord
  1032 								   options:(NSDiacriticInsensitiveSearch | NSCaseInsensitiveSearch | NSAnchoredSearch)].location != NSNotFound ||
  1033 			[listContact.formattedUID rangeOfString:partialWord
  1034 											options:(NSDiacriticInsensitiveSearch | NSCaseInsensitiveSearch | NSAnchoredSearch)].location != NSNotFound ||
  1035 			[listContact.displayName rangeOfString:partialWord
  1036 										   options:(NSDiacriticInsensitiveSearch | NSCaseInsensitiveSearch | NSAnchoredSearch)].location != NSNotFound) {
  1037 				[contacts addObject:listContact];
  1038 		}
  1039 	}
  1040 	
  1041 	return contacts;
  1042 }
  1043 
  1044 - (NSArray *)textView:(NSTextView *)textView completions:(NSArray *)words forPartialWordRange:(NSRange)charRange indexOfSelectedItem:(NSInteger *)index
  1045 {
  1046 	NSMutableArray	*completions = nil;
  1047 	
  1048 	if (self.chat.isGroupChat) {
  1049 		NSString *suffix = nil;
  1050 		NSString *partialWord = [textView.textStorage.string substringWithRange:charRange];
  1051 		BOOL autoCompleteUID = [self.chat.account chatShouldAutocompleteUID:self.chat];
  1052 		
  1053 		//At the start of a line, append ": "
  1054 		if (charRange.location == 0) {
  1055 			suffix = @": ";
  1056 		}
  1057 		
  1058 		completions = [NSMutableArray array];
  1059 		
  1060 		for (AIListContact *listContact in [self contactsMatchingBeginningString:partialWord]) {
  1061 			NSString *displayName = [self.chat aliasForContact:listContact];
  1062 			
  1063 			if (!displayName)
  1064 				displayName = autoCompleteUID ? listContact.formattedUID : listContact.displayName;
  1065 			
  1066 			[completions addObject:(suffix ? [displayName stringByAppendingString:suffix] : displayName)];
  1067 		}
  1068 		
  1069 		if ([self.chat.displayName rangeOfString:partialWord options:(NSDiacriticInsensitiveSearch | NSCaseInsensitiveSearch | NSAnchoredSearch)].location != NSNotFound) {
  1070 			[completions addObject:self.chat.displayName];
  1071 		}
  1072 
  1073 		if ([completions count]) {			
  1074 			*index = 0;
  1075 		}
  1076 	}
  1077 
  1078 	return [completions count] ? completions : words;
  1079 }
  1080 
  1081 //User List ------------------------------------------------------------------------------------------------------------
  1082 #pragma mark User List
  1083 /*!
  1084  * @brief Selected list objects
  1085  *
  1086  * An array of the list objects selected in the user list.
  1087  */
  1088 - (NSArray *)selectedListObjects
  1089 {
  1090 	return [userListView arrayOfListObjects];
  1091 }
  1092 
  1093 /*!
  1094  * @brief Is the user list initially visible?
  1095  */
  1096 - (BOOL)userListInitiallyVisible
  1097 {
  1098 	NSNumber *visibility = [adium.preferenceController preferenceForKey:[KEY_USER_LIST_VISIBLE_PREFIX stringByAppendingFormat:@"%@.%@",
  1099 																		 chat.account.internalObjectID,
  1100 																		 chat.name]
  1101 																  group:PREF_GROUP_DUAL_WINDOW_INTERFACE];
  1102 	
  1103 	return visibility ? [visibility boolValue] : YES;
  1104 }
  1105 
  1106 /*!
  1107  * @brief Set visibility of the user list
  1108  */
  1109 - (void)setUserListVisible:(BOOL)inVisible
  1110 {
  1111 	if (inVisible) {
  1112 		[self _showUserListView];
  1113 	} else {
  1114 		[self _hideUserListView];
  1115 	}
  1116 	
  1117 	[adium.preferenceController setPreference:[NSNumber numberWithBool:inVisible]
  1118 									   forKey:[KEY_USER_LIST_VISIBLE_PREFIX stringByAppendingFormat:@"%@.%@",
  1119 											   chat.account.internalObjectID,
  1120 											   chat.name]
  1121 										group:PREF_GROUP_DUAL_WINDOW_INTERFACE];
  1122 }
  1123 
  1124 /*!
  1125  * @brief Returns YES if the user list is currently visible
  1126  */
  1127 - (BOOL)userListVisible
  1128 {
  1129 	return [shelfView isShelfVisible];
  1130 }
  1131 
  1132 /* @name	toggleUserlist
  1133  * @brief	toggles the state of the userlist shelf
  1134  */
  1135 - (void)toggleUserList
  1136 {
  1137 	if (chat.isGroupChat)
  1138 		[self setUserListVisible:![self userListVisible]];
  1139 }
  1140 
  1141 - (void)toggleUserListSide
  1142 {
  1143 	if(chat.isGroupChat) {
  1144 		userListOnRight = !userListOnRight;
  1145 		
  1146 		// We'll update the actual side when this preference change is told to us.
  1147 		[adium.preferenceController setPreference:[NSNumber numberWithInteger:userListOnRight]
  1148 										   forKey:KEY_USER_LIST_ON_RIGHT
  1149 											group:PREF_GROUP_DUAL_WINDOW_INTERFACE];
  1150 	}
  1151 }
  1152 
  1153 /*!
  1154  * @brief Show the user list
  1155  */
  1156 - (void)_showUserListView
  1157 {	
  1158 	[self setupShelfView];
  1159 	
  1160 	[shelfView setDrawShelfLine:NO];
  1161 
  1162 	//Configure the user list
  1163 	[self _configureUserList];
  1164 	[self updateUserCount];
  1165 
  1166 	//Add the user list back to our window if it's missing
  1167 	if (![self userListVisible]) {
  1168 		[self _updateUserListViewWidth];
  1169 		
  1170 		if (retainingScrollViewUserList) {
  1171 			[scrollView_userList release];
  1172 			retainingScrollViewUserList = NO;
  1173 		}
  1174 	}
  1175 }
  1176 
  1177 /*!
  1178  * @brief Hide the user list.
  1179  *
  1180  * We gain responsibility for releasing scrollView_userList after we hide it
  1181  */
  1182 - (void)_hideUserListView
  1183 {
  1184 	if ([self userListVisible]) {
  1185 		[scrollView_userList retain];
  1186 		[scrollView_userList removeFromSuperview];
  1187 		retainingScrollViewUserList = YES;
  1188 		
  1189 		[userListController release];
  1190 		userListController = nil;
  1191 	
  1192 		//need to collapse the splitview
  1193 		[shelfView setShelfIsVisible:NO];
  1194 	}
  1195 }
  1196 
  1197 /*!
  1198  * @brief Configure the user list
  1199  *
  1200  * Configures the user list view and prepares it for display.  If the user list is not being shown, this configuration
  1201  * should be avoided for performance.
  1202  */
  1203 - (void)_configureUserList
  1204 {
  1205 	if (!userListController) {
  1206 		NSDictionary	*themeDict = [NSDictionary dictionaryNamed:USERLIST_THEME forClass:[self class]];
  1207 		NSDictionary	*layoutDict = [NSDictionary dictionaryNamed:USERLIST_LAYOUT forClass:[self class]];
  1208 		
  1209 		//Create and configure a controller to manage the user list
  1210 		userListController = [[ESChatUserListController alloc] initWithContactListView:userListView
  1211 																		  inScrollView:scrollView_userList 
  1212 																			  delegate:self];
  1213 		[userListController setContactListRoot:chat];
  1214 		[userListController updateLayoutFromPrefDict:layoutDict andThemeFromPrefDict:themeDict];
  1215 		[userListController setHideRoot:YES];
  1216 	}
  1217 }
  1218 
  1219 /*!
  1220  * @brief Update the user list in response to changes
  1221  *
  1222  * This method is invoked when the chat's participating contacts change.  In resopnse, it sets correct visibility of
  1223  * the user list, and updates the displayed users.
  1224  */
  1225 - (void)chatParticipatingListObjectsChanged:(NSNotification *)notification
  1226 {
  1227     //Update the user list
  1228 	AILogWithSignature(@"%i, so %@ %@",[self userListVisible], ([self userListVisible] ? @"reloading" : @"not reloading"),
  1229 					   userListController);
  1230 	
  1231 	[chat resortParticipants];
  1232 	
  1233     if ([self userListVisible]) {
  1234         [userListController reloadData];
  1235 		
  1236 		[self updateUserCount];
  1237     }
  1238 }
  1239 
  1240 - (void)updateUserCount
  1241 {
  1242 	NSString *userCount = nil;
  1243 	
  1244 	if (self.chat.containedObjects.count == 1) {
  1245 		userCount = AILocalizedString(@"1 user", nil);
  1246 	} else {
  1247 		userCount = AILocalizedString(@"%u users", nil);
  1248 	}
  1249 	
  1250 	[shelfView setResizeThumbStringValue:[NSString stringWithFormat:userCount, self.chat.containedObjects.count]];
  1251 }
  1252 
  1253 /*!
  1254  * @brief The selection in the user list changed
  1255  *
  1256  * When the user list selection changes, we update the chat's "preferred list object", which is used
  1257  * elsewhere to identify the currently 'selected' contact for Get Info, Messaging, etc.
  1258  */
  1259 - (void)outlineViewSelectionDidChange:(NSNotification *)notification
  1260 {
  1261 	if ([notification object] == userListView) {
  1262 		[chat setPreferredListObject:(AIListContact *)[userListView listObject]];
  1263 	}
  1264 }
  1265 
  1266 /*!
  1267  * @brief Perform default action on the selected user list object
  1268  *
  1269  * Here we could open a private message or display info for the user, however we perform no action
  1270  * at the moment.
  1271  */
  1272 - (void)performDefaultActionOnSelectedObject:(AIListObject *)listObject sender:(NSOutlineView *)sender
  1273 {
  1274 	if ([listObject isKindOfClass:[AIListContact class]]) {
  1275 		[adium.interfaceController setActiveChat:[adium.chatController openChatWithContact:(AIListContact *)listObject
  1276 												  onPreferredAccount:YES]];
  1277 	}
  1278 }
  1279 
  1280 /* 
  1281  * @brief Update the width of our user list view
  1282  *
  1283  * This method sets the width of the user list view to the most ideal value, and adjusts the other views in our
  1284  * window to fill the remaining space.
  1285  */
  1286 - (void)_updateUserListViewWidth
  1287 {
  1288 	NSInteger		width = [self _userListViewProperWidth];
  1289 	NSInteger		widthWithDivider = 1 + width;	//resize bar effective width  
  1290 	NSRect	tempFrame;
  1291 
  1292 	//Size the user list view to the desired width
  1293 	tempFrame = [scrollView_userList frame];
  1294 	[scrollView_userList setFrame:NSMakeRect([shelfView frame].size.width - width,
  1295 											 tempFrame.origin.y,
  1296 											 width,
  1297 											 tempFrame.size.height)];
  1298 	
  1299 	//Size the message view to fill the remaining space
  1300 	tempFrame = [scrollView_messages frame];
  1301 	[scrollView_messages setFrame:NSMakeRect(tempFrame.origin.x,
  1302 											 tempFrame.origin.y,
  1303 											 [shelfView frame].size.width - widthWithDivider,
  1304 											 tempFrame.size.height)];
  1305 
  1306 	//Redisplay both views and the divider
  1307 	[shelfView setNeedsDisplay:YES];
  1308 }
  1309 
  1310 /*!
  1311  * @brief Returns the width our user list view should be
  1312  *
  1313  * This method takes into account user preference and the current window size to return a width which is most
  1314  * ideal for the user list view.
  1315  */
  1316 - (NSInteger)_userListViewProperWidth
  1317 {
  1318 	NSInteger dividerThickness = 1;
  1319 	NSInteger allowedWidth = ([shelfView frame].size.width / 2.0) - dividerThickness;
  1320 	NSInteger width = userListMinWidth;
  1321 	
  1322 	//We must never fall below the user's prefered mininum or above the allowed width
  1323 	if (width > allowedWidth) width = allowedWidth;
  1324 
  1325 	return width;
  1326 }
  1327 
  1328 -(CGFloat)shelfSplitView:(KNShelfSplitView *)shelfSplitView validateWidth:(CGFloat)proposedWidth
  1329 {
  1330 	if (userListMinWidth != proposedWidth) {
  1331 		[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(saveUserListMinimumSize) object:nil];
  1332 		[self performSelector:@selector(saveUserListMinimumSize) withObject:nil afterDelay:0.5];
  1333 	}
  1334 	
  1335 	userListMinWidth = proposedWidth;
  1336 	
  1337 	return userListMinWidth;
  1338 }
  1339 
  1340 //Split Views --------------------------------------------------------------------------------------------------
  1341 #pragma mark Split Views
  1342 
  1343 // This method will be called after a RBSplitView is resized with setFrameSize: but before
  1344 // adjustSubviews is called on it.
  1345 - (void)splitView:(RBSplitView*)sender wasResizedFrom:(CGFloat)oldDimension to:(CGFloat)newDimension
  1346 {
  1347 	[[sender subviewAtPosition:0] setDimension:[[sender subviewAtPosition:0] dimension] + (newDimension - oldDimension)];
  1348 }
  1349 
  1350 // This method will be called whenever a subview's frame is changed, usually from inside adjustSubviews' final loop.
  1351 // You'd normally use this to move some auxiliary view to keep it aligned with the subview.
  1352 - (void)splitView:(RBSplitView*)sender changedFrameOfSubview:(RBSplitSubview*)subview from:(NSRect)fromRect to:(NSRect)toRect
  1353 {
  1354 	if ([sender subviewAtPosition:1] == subview) {
  1355 		if ([sender isDragging])
  1356 			entryMinHeight = NSHeight(toRect);
  1357 	}
  1358 }
  1359 
  1360 - (void)splitViewDidHaveResizeDoubleClick:(KNShelfSplitView *)sender
  1361 {
  1362 	[self toggleUserList];
  1363 }
  1364 
  1365 #pragma mark Shelfview
  1366 /* @name	setupShelfView
  1367  * @brief	sets up shelfsplitview containing userlist & contentviews
  1368  */
  1369  -(void)setupShelfView
  1370 {
  1371 	[shelfView setShelfWidth:userListMinWidth];
  1372 	
  1373 	AILogWithSignature(@"ShelfView %@ (content view is %@) --> superview %@, in window %@; frame %@; content view %@ shelf view %@ in window %@",
  1374 					   shelfView, [shelfView contentView], [shelfView superview], [shelfView window], NSStringFromRect([[shelfView superview] frame]),
  1375 					   splitView_textEntryHorizontal,
  1376 					   scrollView_userList, [scrollView_userList window]);
  1377 	[shelfView setContextButtonImage:[NSImage imageNamed:@"sidebarActionWidget"]];
  1378 	
  1379 	[shelfView setShelfIsVisible:YES];
  1380 }
  1381 
  1382 -(NSMenu *)contextMenuForShelfSplitView:(KNShelfSplitView *)shelfSplitView
  1383 {
  1384 	return chat.actionMenu;
  1385 }
  1386 
  1387 #pragma mark Undo
  1388 - (NSUndoManager *)undoManagerForTextView:(NSTextView *)aTextView
  1389 {
  1390 	if (!undoManager)
  1391 		undoManager = [[NSUndoManager alloc] init];
  1392 
  1393 	return undoManager;
  1394 }
  1395 
  1396 
  1397 @end