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.
2 * Adium is the legal property of its developers, whose names are listed in the copyright file included
3 * with this source distribution.
5 * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
6 * General Public License as published by the Free Software Foundation; either version 2 of the License,
7 * or (at your option) any later version.
9 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
10 * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
11 * Public License for more details.
13 * You should have received a copy of the GNU General Public License along with this program; if not,
14 * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
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"
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>
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>
51 #import <PSMTabBarControl/NSBezierPath_AMShading.h>
53 #import "RBSplitView.h"
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
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
70 #define TEXTVIEW_HEIGHT_DEBUG
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;
94 - (NSArray *)contactsMatchingBeginningString:(NSString *)partialWord;
97 @implementation AIMessageViewController
100 * @brief Create a new message view controller
102 + (AIMessageViewController *)messageDisplayControllerForChat:(AIChat *)inChat
104 return [[[self alloc] initForChat:inChat] autorelease];
111 - (id)initForChat:(AIChat *)inChat
113 if ((self = [super init])) {
114 AIListContact *contact;
116 chat = [inChat retain];
117 contact = chat.listObject;
118 view_accountSelection = nil;
119 userListController = nil;
120 suppressSendLaterPrompt = NO;
121 retainingScrollViewUserList = NO;
123 //Load the view containing our controls
124 [NSBundle loadNibNamed:MESSAGE_VIEW_NIB owner:self];
126 //Register for the various notification we need
127 [[NSNotificationCenter defaultCenter] addObserver:self
128 selector:@selector(sendMessage:)
129 name:Interface_SendEnteredMessage
131 [[NSNotificationCenter defaultCenter] addObserver:self
132 selector:@selector(didSendMessage:)
133 name:Interface_DidSendEnteredMessage
135 [[NSNotificationCenter defaultCenter] addObserver:self
136 selector:@selector(chatStatusChanged:)
137 name:Chat_StatusChanged
139 [[NSNotificationCenter defaultCenter] addObserver:self
140 selector:@selector(chatParticipatingListObjectsChanged:)
141 name:Chat_ParticipatingListObjectsChanged
143 [[NSNotificationCenter defaultCenter] addObserver:self
144 selector:@selector(redisplaySourceAndDestinationSelector:)
145 name:Chat_SourceChanged
147 [[NSNotificationCenter defaultCenter] addObserver:self
148 selector:@selector(redisplaySourceAndDestinationSelector:)
149 name:Chat_DestinationChanged
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];
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.
159 [self setUserListVisible:(chat.isGroupChat && [self userListInitiallyVisible])];
161 [self chatParticipatingListObjectsChanged:nil];
162 [self chatStatusChanged:nil];
164 //Configure our views
165 [self _configureMessageDisplay];
166 [self _configureTextEntryView];
168 //Set our base writing direction
170 initialBaseWritingDirection = [contact baseWritingDirection];
171 [textView_outgoing setBaseWritingDirection:initialBaseWritingDirection];
183 AIListContact *contact = chat.listObject;
185 [adium.preferenceController unregisterPreferenceObserver:self];
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];
192 if (userListController) {
193 [self saveUserListMinimumSize];
196 //Save the base writing direction
197 if (contact && initialBaseWritingDirection != [textView_outgoing baseWritingDirection])
198 [contact setBaseWritingDirection:[textView_outgoing baseWritingDirection]];
200 [chat release]; chat = nil;
203 [[NSNotificationCenter defaultCenter] removeObserver:self];
205 //Account selection view
206 [self _destroyAccountSelectionView];
208 [messageDisplayController messageViewIsClosing];
209 [messageDisplayController release];
210 [userListController release];
212 [controllerView_messages release];
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];
219 //Release the hidden user list view
220 if (retainingScrollViewUserList) {
221 [scrollView_userList release];
226 [undoManager release]; undoManager = nil;
231 - (void)saveUserListMinimumSize
233 [adium.preferenceController setPreference:[NSNumber numberWithInteger:userListMinWidth]
234 forKey:KEY_ENTRY_USER_LIST_MIN_WIDTH
235 group:PREF_GROUP_DUAL_WINDOW_INTERFACE];
238 - (void)updateGradientColors
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;
244 switch ([messageWindowController tabPosition]) {
245 case AdiumTabPositionBottom:
246 case AdiumTabPositionTop:
247 case AdiumTabPositionLeft:
248 leftColor = lighterColor;
249 rightColor = darkerColor;
251 case AdiumTabPositionRight:
252 leftColor = darkerColor;
253 rightColor = lighterColor;
257 [view_accountSelection setLeftColor:leftColor rightColor:rightColor];
259 // [splitView_textEntryHorizontal setLeftColor:leftColor rightColor:rightColor];
263 * @brief Invoked before the message view closes
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.
268 - (void)messageViewWillLeaveWindowController:(AIMessageWindowController *)inWindowController
270 if (inWindowController) {
271 [userListController contactListWillBeRemovedFromWindow];
274 [messageWindowController release]; messageWindowController = nil;
277 - (void)messageViewAddedToWindowController:(AIMessageWindowController *)inWindowController
279 if (inWindowController) {
280 [userListController contactListWasAddedBackToWindow];
283 if (inWindowController != messageWindowController) {
284 [messageWindowController release];
285 messageWindowController = [inWindowController retain];
287 [self updateGradientColors];
292 * @brief Retrieve the chat represented by this message view
300 * @brief Retrieve the source account associated with this chat
302 - (AIAccount *)account
308 * @brief Retrieve the destination list object associated with this chat
310 - (AIListContact *)listObject
312 return chat.listObject;
316 * @brief Returns the selected list object in our participants list
318 - (AIListObject *)preferredListObject
320 if (userListView) { //[[shelfView subviews] containsObject:scrollView_userList] && ([userListView selectedRow] != -1)
321 return [userListView itemAtRow:[userListView selectedRow]];
328 * @brief Invoked when the status of our chat changes
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.
333 - (void)chatStatusChanged:(NSNotification *)notification
335 NSArray *modifiedKeys = [[notification userInfo] objectForKey:@"Keys"];
337 if (notification == nil || [modifiedKeys containsObject:@"DisallowAccountSwitching"]) {
338 [self setAccountSelectionMenuVisibleIfNeeded:YES];
343 //Message Display ------------------------------------------------------------------------------------------------------
344 #pragma mark Message Display
346 * @brief Configure the message display view
348 - (void)_configureMessageDisplay
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];
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.
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
364 [controllerView_messages setFrame:[scrollView_messages documentVisibleRect]];
365 [scrollView_messages setAccessibilityChild:controllerView_messages];
366 [[customView_messages superview] replaceSubview:customView_messages with:controllerView_messages];
368 //This is what draws our transparent background
369 //Technically, it could be set in MessageView.nib, too
370 [scrollView_messages setBackgroundColor:[NSColor clearColor]];
372 [textView_outgoing setNextResponder:view_contents];
374 [controllerView_messages setNextResponder:textView_outgoing];
378 * @brief The message display controller
380 - (NSObject<AIMessageDisplayController> *)messageDisplayController
382 return messageDisplayController;
386 * @brief Access to our view
390 return view_contents;
393 - (NSScrollView *)messagesScrollView
395 return scrollView_messages;
399 * @brief Support for printing. Forward the print command to our message display view
401 - (void)adiumPrint:(id)sender
403 if ([messageDisplayController respondsToSelector:@selector(adiumPrint:)]) {
404 [messageDisplayController adiumPrint:sender];
409 //Messaging ------------------------------------------------------------------------------------------------------------
410 #pragma mark Messaging
412 * @brief Send the entered message
414 - (IBAction)sendMessage:(id)sender
416 NSAttributedString *attributedString = [textView_outgoing textStorage];
418 //Only send if we have a non-zero-length string
419 if ([attributedString length] != 0) {
420 AIListObject *listObject = chat.listObject;
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];
427 //Reset the content of the text field, removing the command as it has been executed
428 [self clearTextEntryView];
430 //Commands are not messages, so they don't have to be sent
434 if (chat.isGroupChat && !chat.account.online) {
435 //Refuse to do anything with a group chat for an offline account.
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;
447 [[NSNotificationCenter defaultCenter] postNotificationName:Interface_WillSendEnteredMessage
451 outgoingAttributedString = [attributedString copy];
452 message = [AIContentMessage messageInChat:chat
454 destination:chat.listObject
455 date:nil //created for us by AIContentMessage
456 message:outgoingAttributedString
458 [outgoingAttributedString release];
460 if ([adium.contentController sendContentObject:message]) {
461 [[NSNotificationCenter defaultCenter] postNotificationName:Interface_DidSendEnteredMessage
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)
469 NSString *formattedUID = listObject.formattedUID;
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];
480 [alert setMessageText:[NSString stringWithFormat:AILocalizedString(@"%@ appears to be offline. How do you want to send this message?", nil),
483 switch (messageSendingAbility) {
484 case AIChatCanSendViaServersideOfflineMessage:
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)];
491 [alert addButtonWithTitle:AILocalizedString(@"Send When Both Online", nil)];
492 [[[alert buttons] objectAtIndex:1] setKeyEquivalent:@"b"];
493 [[[alert buttons] objectAtIndex:1] setKeyEquivalentModifierMask:0];
497 case AIChatMayNotBeAbleToSendMessage:
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)];
504 [alert addButtonWithTitle:AILocalizedString(@"Send Later", nil)];
505 [[[alert buttons] objectAtIndex:1] setKeyEquivalent:@"l"];
506 [[[alert buttons] objectAtIndex:1] setKeyEquivalentModifierMask:0];
510 case AIChatCanNotSendMessage:
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];
521 case AIChatCanSendMessageNow:
523 //We will never get here.
528 [alert addButtonWithTitle:AILocalizedString(@"Don't Send", nil)];
530 NSButton *dontSendButton = ((messageSendingAbility == AIChatCanNotSendMessage) ?
531 [[alert buttons] objectAtIndex:1] :
532 [[alert buttons] objectAtIndex:2]);
533 [dontSendButton setKeyEquivalent:@"\E"];
534 [dontSendButton setKeyEquivalentModifierMask:0];
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 */];
546 * @brief Send Later button was pressed
548 - (void)alertDidEnd:(NSAlert *)alert returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo
550 AIChatSendingAbilityType messageSendingAbility = [(NSNumber *)contextInfo integerValue];
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.
557 if (messageSendingAbility == AIChatCanNotSendMessage) {
559 [self sendMessageLater:nil];
563 suppressSendLaterPrompt = YES;
564 [self sendMessage:nil];
568 case NSAlertSecondButtonReturn:
569 /* The AIChatCanNotSendMessage dalogue has Cancel as the second choice;
570 * all others have Send Later as the first choice.
572 if (messageSendingAbility != AIChatCanNotSendMessage) {
574 [self sendMessageLater:nil];
578 case NSAlertThirdButtonReturn: /* Don't Send */
582 //Retained when the alert was created to guard against a crash if the chat tab being closed while we are open
584 [(NSNumber *)contextInfo release];
588 * @brief Invoked after our entered message sends
590 * This method hides the account selection view and clears the entered message after our message sends
592 - (IBAction)didSendMessage:(id)sender
594 [self setAccountSelectionMenuVisibleIfNeeded:NO];
595 [self clearTextEntryView];
599 * @brief Offline messaging
601 - (IBAction)sendMessageLater:(id)sender
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];
609 //Put the alert on the metaContact containing this listContact if applicable
610 AIMetaContact *listContact = chat.listObject.metaContact;
613 NSMutableDictionary *detailsDict, *alertDict;
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"];
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"];
626 [alertDict setObject:listContact forKey:@"TEMP-ListContact"];
628 [adium.contentController filterAttributedString:[[[textView_outgoing textStorage] copy] autorelease]
629 usingFilterType:AIFilterContent
630 direction:AIFilterOutgoing
631 filterContext:listContact
633 selector:@selector(gotFilteredMessageToSendLater:receivingContext:)
636 [self didSendMessage:nil];
641 * @brief Offline messaging
643 //XXX - Offline messaging code SHOULD NOT BE IN HERE! -ai
644 - (void)gotFilteredMessageToSendLater:(NSAttributedString *)filteredMessage receivingContext:(NSMutableDictionary *)alertDict
646 NSMutableDictionary *detailsDict;
647 AIListContact *listContact;
649 detailsDict = [alertDict objectForKey:@"ActionDetails"];
650 [detailsDict setObject:[filteredMessage dataRepresentation] forKey:@"Message"];
652 listContact = [[alertDict objectForKey:@"TEMP-ListContact"] retain];
653 [alertDict removeObjectForKey:@"TEMP-ListContact"];
655 [adium.contactAlertsController addAlert:alertDict
656 toListObject:listContact
657 setAsNewDefaults:NO];
658 [listContact release];
661 //Account Selection ----------------------------------------------------------------------------------------------------
662 #pragma mark Account Selection
666 - (void)accountSelectionViewFrameDidChange:(NSNotification *)notification
668 [self updateFramesForAccountSelectionView];
672 * @brief Redisplay the source/destination account selector
674 - (void)redisplaySourceAndDestinationSelector:(NSNotification *)notification
676 // Update the textView's chat source, in case any attributes it monitors changed.
677 [textView_outgoing setChat:chat];
678 [self setAccountSelectionMenuVisibleIfNeeded:YES];
682 * @brief Toggle visibility of the account selection menus
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.
687 - (void)setAccountSelectionMenuVisibleIfNeeded:(BOOL)makeVisible
689 //Hide or show the account selection view as requested
691 [self _createAccountSelectionView];
693 [self _destroyAccountSelectionView];
698 * @brief Show the account selection view
700 - (void)_createAccountSelectionView
702 if (!view_accountSelection) {
703 NSRect contentFrame = [splitView_textEntryHorizontal frame];
705 //Create the account selection view and insert it into our window
706 view_accountSelection = [[AIAccountSelectionView alloc] initWithFrame:contentFrame];
708 [view_accountSelection setAutoresizingMask:(NSViewWidthSizable | NSViewMinYMargin)];
710 [self updateGradientColors];
712 //Insert the account selection view at the top of our view
713 [[shelfView contentView] addSubview:view_accountSelection];
714 [view_accountSelection setChat:chat];
716 [[NSNotificationCenter defaultCenter] addObserver:self
717 selector:@selector(accountSelectionViewFrameDidChange:)
718 name:AIViewFrameDidChangeNotification
719 object:view_accountSelection];
721 [self updateFramesForAccountSelectionView];
723 //Redisplay everything
724 [[shelfView contentView] setNeedsDisplay:YES];
726 [view_accountSelection setChat:chat];
731 * @brief Hide the account selection view
733 - (void)_destroyAccountSelectionView
735 if (view_accountSelection) {
736 //Remove the observer
737 [[NSNotificationCenter defaultCenter] removeObserver:self
738 name:AIViewFrameDidChangeNotification
739 object:view_accountSelection];
741 //Remove the account selection view from our window, clean it up
742 [view_accountSelection removeFromSuperview];
743 [view_accountSelection release]; view_accountSelection = nil;
745 //Redisplay everything
746 [self updateFramesForAccountSelectionView];
751 * @brief Position the account selection view, if it is present, and the messages/text entry splitview appropriately
753 - (void)updateFramesForAccountSelectionView
755 NSInteger accountSelectionHeight = (view_accountSelection ? [view_accountSelection frame].size.height : 0);
757 if (view_accountSelection) {
758 [view_accountSelection setFrameOrigin:NSMakePoint(NSMinX([view_accountSelection frame]), NSHeight([[view_accountSelection superview] frame]) - accountSelectionHeight)];
759 [view_accountSelection setNeedsDisplay:YES];
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];
766 [splitView_textEntryHorizontal setNeedsDisplay:YES];
770 //Text Entry -----------------------------------------------------------------------------------------------------------
771 #pragma mark Text Entry
773 * @brief Preferences changed, update sending keys
775 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key object:(AIListObject *)object
776 preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
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]) {
783 if (firstTime || [key isEqualToString:KEY_ENTRY_USER_LIST_MIN_WIDTH]) {
784 NSInteger oldWidth = userListMinWidth;
786 userListMinWidth = [[prefDict objectForKey:KEY_ENTRY_USER_LIST_MIN_WIDTH] integerValue];
788 if (oldWidth != userListMinWidth) {
789 [shelfView setShelfWidth:userListMinWidth];
793 if (firstTime || [key isEqualToString:KEY_USER_LIST_ON_RIGHT]) {
794 userListOnRight = [[prefDict objectForKey:KEY_USER_LIST_ON_RIGHT] boolValue];
796 [shelfView setShelfOnRight:userListOnRight];
802 * @brief Configure the text entry view
804 - (void)_configureTextEntryView
806 //Configure the text entry view
807 [textView_outgoing setTarget:self action:@selector(sendMessage:)];
809 //This is necessary for tab completion.
810 [textView_outgoing setDelegate:self];
812 [textView_outgoing setTextContainerInset:NSMakeSize(0,2)];
813 if ([textView_outgoing respondsToSelector:@selector(setUsesFindPanel:)]) {
814 [textView_outgoing setUsesFindPanel:YES];
816 [textView_outgoing setClearOnEscape:YES];
817 [textView_outgoing setTypingAttributes:[adium.contentController defaultFormattingAttributes]];
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];
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]];
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];
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];
838 [self _updateTextEntryViewHeight];
842 * @brief Sets our text entry view as the first responder
844 - (void)makeTextEntryViewFirstResponder
846 [[textView_outgoing window] makeFirstResponder:textView_outgoing];
851 [self makeTextEntryViewFirstResponder];
853 /* When we're selected, it's as if the user list controller is back in the window */
854 [userListController contactListWasAddedBackToWindow];
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];
868 * @brief Returns the Text Entry View
870 * Make sure you need to use this. If you just need to enter text, see -addToTextEntryView:
872 - (AIMessageEntryTextView *)textEntryView
874 return textView_outgoing;
878 * @brief Clear the message entry text view
880 - (void)clearTextEntryView
882 NSWritingDirection writingDirection;
884 writingDirection = [textView_outgoing baseWritingDirection];
886 [textView_outgoing setString:@""];
887 [textView_outgoing setTypingAttributes:[adium.contentController defaultFormattingAttributes]];
889 [textView_outgoing setBaseWritingDirection:writingDirection]; //Preserve the writing diraction
891 [[NSNotificationCenter defaultCenter] postNotificationName:NSTextDidChangeNotification
892 object:textView_outgoing];
896 * @brief Add text to the message entry text view
898 * Adds the passed string to the entry text view at the insertion point. If there is selected text in the view, it
901 - (void)addToTextEntryView:(NSAttributedString *)inString
903 [textView_outgoing insertText:inString];
904 [[NSNotificationCenter defaultCenter] postNotificationName:NSTextDidChangeNotification object:textView_outgoing];
908 * @brief Add data to the message entry text view
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.
913 - (void)addDraggedDataToTextEntryView:(id <NSDraggingInfo>)draggingInfo
915 [textView_outgoing performDragOperation:draggingInfo];
916 [[NSNotificationCenter defaultCenter] postNotificationName:NSTextDidChangeNotification object:textView_outgoing];
920 * @brief Update the text entry view's height when its desired size changes
922 - (void)outgoingTextViewDesiredSizeDidChange:(NSNotification *)notification
924 [self _updateTextEntryViewHeight];
927 - (void)tabViewDidChangeVisibility
929 [self _updateTextEntryViewHeight];
933 * @brief Update the height of our text entry view
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.
938 - (void)_updateTextEntryViewHeight
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)];
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];
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)];
953 * @brief Returns the height our text entry view should be
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.
958 * @param ignoreUserMininum If YES, the user's preference for mininum height will be ignored
960 - (NSInteger)_textEntryViewProperHeightIgnoringUserMininum:(BOOL)ignoreUserMininum
962 NSInteger dividerThickness = [splitView_textEntryHorizontal dividerThickness];
963 NSInteger allowedHeight = ([splitView_textEntryHorizontal frame].size.height / 2.0) - dividerThickness;
966 //Our primary goal is to display all the entered text
967 height = [textView_outgoing desiredSize].height;
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;
973 if (height > allowedHeight) height = allowedHeight;
978 #pragma mark Autocompletion
979 - (BOOL)canTabCompleteForPartialWord:(NSString *)partialWord
981 return ([self contactsMatchingBeginningString:partialWord].count > 0 ||
982 [self.chat.displayName rangeOfString:partialWord options:(NSDiacriticInsensitiveSearch | NSCaseInsensitiveSearch | NSAnchoredSearch)].location != NSNotFound);
986 * @brief Should the tab key cause an autocompletion if possible?
988 * We only tab to autocomplete for a group chat
990 - (BOOL)textViewShouldTabComplete:(NSTextView *)inTextView
992 if (self.chat.isGroupChat) {
993 NSRange completionRange = inTextView.rangeForUserCompletion;
994 NSString *partialWord = [inTextView.textStorage.string substringWithRange:completionRange];
995 return [self canTabCompleteForPartialWord:partialWord];
1001 - (NSRange)textView:(NSTextView *)inTextView rangeForCompletion:(NSRange)charRange
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];
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);
1013 // We found a whitespace, use from it until our current position.
1014 partialWord = [allText substringWithRange:NSMakeRange(NSMaxRange(whitespacePosition), allText.length - NSMaxRange(whitespacePosition))];
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));
1026 - (NSArray *)contactsMatchingBeginningString:(NSString *)partialWord
1028 NSMutableArray *contacts = [NSMutableArray array];
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];
1044 - (NSArray *)textView:(NSTextView *)textView completions:(NSArray *)words forPartialWordRange:(NSRange)charRange indexOfSelectedItem:(NSInteger *)index
1046 NSMutableArray *completions = nil;
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];
1053 //At the start of a line, append ": "
1054 if (charRange.location == 0) {
1058 completions = [NSMutableArray array];
1060 for (AIListContact *listContact in [self contactsMatchingBeginningString:partialWord]) {
1061 NSString *displayName = [self.chat aliasForContact:listContact];
1064 displayName = autoCompleteUID ? listContact.formattedUID : listContact.displayName;
1066 [completions addObject:(suffix ? [displayName stringByAppendingString:suffix] : displayName)];
1069 if ([self.chat.displayName rangeOfString:partialWord options:(NSDiacriticInsensitiveSearch | NSCaseInsensitiveSearch | NSAnchoredSearch)].location != NSNotFound) {
1070 [completions addObject:self.chat.displayName];
1073 if ([completions count]) {
1078 return [completions count] ? completions : words;
1081 //User List ------------------------------------------------------------------------------------------------------------
1082 #pragma mark User List
1084 * @brief Selected list objects
1086 * An array of the list objects selected in the user list.
1088 - (NSArray *)selectedListObjects
1090 return [userListView arrayOfListObjects];
1094 * @brief Is the user list initially visible?
1096 - (BOOL)userListInitiallyVisible
1098 NSNumber *visibility = [adium.preferenceController preferenceForKey:[KEY_USER_LIST_VISIBLE_PREFIX stringByAppendingFormat:@"%@.%@",
1099 chat.account.internalObjectID,
1101 group:PREF_GROUP_DUAL_WINDOW_INTERFACE];
1103 return visibility ? [visibility boolValue] : YES;
1107 * @brief Set visibility of the user list
1109 - (void)setUserListVisible:(BOOL)inVisible
1112 [self _showUserListView];
1114 [self _hideUserListView];
1117 [adium.preferenceController setPreference:[NSNumber numberWithBool:inVisible]
1118 forKey:[KEY_USER_LIST_VISIBLE_PREFIX stringByAppendingFormat:@"%@.%@",
1119 chat.account.internalObjectID,
1121 group:PREF_GROUP_DUAL_WINDOW_INTERFACE];
1125 * @brief Returns YES if the user list is currently visible
1127 - (BOOL)userListVisible
1129 return [shelfView isShelfVisible];
1132 /* @name toggleUserlist
1133 * @brief toggles the state of the userlist shelf
1135 - (void)toggleUserList
1137 if (chat.isGroupChat)
1138 [self setUserListVisible:![self userListVisible]];
1141 - (void)toggleUserListSide
1143 if(chat.isGroupChat) {
1144 userListOnRight = !userListOnRight;
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];
1154 * @brief Show the user list
1156 - (void)_showUserListView
1158 [self setupShelfView];
1160 [shelfView setDrawShelfLine:NO];
1162 //Configure the user list
1163 [self _configureUserList];
1164 [self updateUserCount];
1166 //Add the user list back to our window if it's missing
1167 if (![self userListVisible]) {
1168 [self _updateUserListViewWidth];
1170 if (retainingScrollViewUserList) {
1171 [scrollView_userList release];
1172 retainingScrollViewUserList = NO;
1178 * @brief Hide the user list.
1180 * We gain responsibility for releasing scrollView_userList after we hide it
1182 - (void)_hideUserListView
1184 if ([self userListVisible]) {
1185 [scrollView_userList retain];
1186 [scrollView_userList removeFromSuperview];
1187 retainingScrollViewUserList = YES;
1189 [userListController release];
1190 userListController = nil;
1192 //need to collapse the splitview
1193 [shelfView setShelfIsVisible:NO];
1198 * @brief Configure the user list
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.
1203 - (void)_configureUserList
1205 if (!userListController) {
1206 NSDictionary *themeDict = [NSDictionary dictionaryNamed:USERLIST_THEME forClass:[self class]];
1207 NSDictionary *layoutDict = [NSDictionary dictionaryNamed:USERLIST_LAYOUT forClass:[self class]];
1209 //Create and configure a controller to manage the user list
1210 userListController = [[ESChatUserListController alloc] initWithContactListView:userListView
1211 inScrollView:scrollView_userList
1213 [userListController setContactListRoot:chat];
1214 [userListController updateLayoutFromPrefDict:layoutDict andThemeFromPrefDict:themeDict];
1215 [userListController setHideRoot:YES];
1220 * @brief Update the user list in response to changes
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.
1225 - (void)chatParticipatingListObjectsChanged:(NSNotification *)notification
1227 //Update the user list
1228 AILogWithSignature(@"%i, so %@ %@",[self userListVisible], ([self userListVisible] ? @"reloading" : @"not reloading"),
1229 userListController);
1231 [chat resortParticipants];
1233 if ([self userListVisible]) {
1234 [userListController reloadData];
1236 [self updateUserCount];
1240 - (void)updateUserCount
1242 NSString *userCount = nil;
1244 if (self.chat.containedObjects.count == 1) {
1245 userCount = AILocalizedString(@"1 user", nil);
1247 userCount = AILocalizedString(@"%u users", nil);
1250 [shelfView setResizeThumbStringValue:[NSString stringWithFormat:userCount, self.chat.containedObjects.count]];
1254 * @brief The selection in the user list changed
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.
1259 - (void)outlineViewSelectionDidChange:(NSNotification *)notification
1261 if ([notification object] == userListView) {
1262 [chat setPreferredListObject:(AIListContact *)[userListView listObject]];
1267 * @brief Perform default action on the selected user list object
1269 * Here we could open a private message or display info for the user, however we perform no action
1272 - (void)performDefaultActionOnSelectedObject:(AIListObject *)listObject sender:(NSOutlineView *)sender
1274 if ([listObject isKindOfClass:[AIListContact class]]) {
1275 [adium.interfaceController setActiveChat:[adium.chatController openChatWithContact:(AIListContact *)listObject
1276 onPreferredAccount:YES]];
1281 * @brief Update the width of our user list view
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.
1286 - (void)_updateUserListViewWidth
1288 NSInteger width = [self _userListViewProperWidth];
1289 NSInteger widthWithDivider = 1 + width; //resize bar effective width
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,
1297 tempFrame.size.height)];
1299 //Size the message view to fill the remaining space
1300 tempFrame = [scrollView_messages frame];
1301 [scrollView_messages setFrame:NSMakeRect(tempFrame.origin.x,
1303 [shelfView frame].size.width - widthWithDivider,
1304 tempFrame.size.height)];
1306 //Redisplay both views and the divider
1307 [shelfView setNeedsDisplay:YES];
1311 * @brief Returns the width our user list view should be
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.
1316 - (NSInteger)_userListViewProperWidth
1318 NSInteger dividerThickness = 1;
1319 NSInteger allowedWidth = ([shelfView frame].size.width / 2.0) - dividerThickness;
1320 NSInteger width = userListMinWidth;
1322 //We must never fall below the user's prefered mininum or above the allowed width
1323 if (width > allowedWidth) width = allowedWidth;
1328 -(CGFloat)shelfSplitView:(KNShelfSplitView *)shelfSplitView validateWidth:(CGFloat)proposedWidth
1330 if (userListMinWidth != proposedWidth) {
1331 [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(saveUserListMinimumSize) object:nil];
1332 [self performSelector:@selector(saveUserListMinimumSize) withObject:nil afterDelay:0.5];
1335 userListMinWidth = proposedWidth;
1337 return userListMinWidth;
1340 //Split Views --------------------------------------------------------------------------------------------------
1341 #pragma mark Split Views
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
1347 [[sender subviewAtPosition:0] setDimension:[[sender subviewAtPosition:0] dimension] + (newDimension - oldDimension)];
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
1354 if ([sender subviewAtPosition:1] == subview) {
1355 if ([sender isDragging])
1356 entryMinHeight = NSHeight(toRect);
1360 - (void)splitViewDidHaveResizeDoubleClick:(KNShelfSplitView *)sender
1362 [self toggleUserList];
1365 #pragma mark Shelfview
1366 /* @name setupShelfView
1367 * @brief sets up shelfsplitview containing userlist & contentviews
1369 -(void)setupShelfView
1371 [shelfView setShelfWidth:userListMinWidth];
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"]];
1379 [shelfView setShelfIsVisible:YES];
1382 -(NSMenu *)contextMenuForShelfSplitView:(KNShelfSplitView *)shelfSplitView
1384 return chat.actionMenu;
1388 - (NSUndoManager *)undoManagerForTextView:(NSTextView *)aTextView
1391 undoManager = [[NSUndoManager alloc] init];