* Adium is the legal property of its developers, whose names are listed in the copyright file included * with this source distribution. * This program is free software; you can redistribute it and/or modify it under the terms of the GNU * General Public License as published by the Free Software Foundation; either version 2 of the License, * or (at your option) any later version. * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General * Public License for more details. * You should have received a copy of the GNU General Public License along with this program; if not, * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. #import "AIMessageViewController.h" #import "AIAccountSelectionView.h" #import "AIMessageWindowController.h" #import "ESGeneralPreferencesPlugin.h" #import "AIDualWindowInterfacePlugin.h" #import "AIMessageWindowOutgoingScrollView.h" #import "AIGradientView.h" #import <Adium/AIChatControllerProtocol.h> #import <Adium/AIContactAlertsControllerProtocol.h> #import <Adium/AIContentControllerProtocol.h> #import <Adium/AIContentMessage.h> #import <Adium/AIMetaContact.h> #import <Adium/AIListOutlineView.h> #import <Adium/AIServiceIcons.h> #import <AIUtilities/AIAttributedStringAdditions.h> #import <AIUtilities/AIDictionaryAdditions.h> #import <AIUtilities/AIOSCompatibility.h> #define MESSAGE_VIEW_MIN_HEIGHT_RATIO 0.5f // Mininum height ratio of the message view #define MESSAGE_VIEW_MIN_WIDTH_RATIO 0.5f // Mininum width ratio of the message view #define ENTRY_TEXTVIEW_MIN_HEIGHT 20 // Mininum height of the text entry view #define USER_LIST_DEFAULT_WIDTH 120 // Default width of the user list #define MESSAGE_VIEW_NIB @"MessageView" // Filename of the message view nib #define USERLIST_THEME @"UserList Theme" // File name of the user list theme #define USERLIST_LAYOUT @"UserList Layout" // File name of the user list layout #define KEY_ENTRY_TEXTVIEW_MIN_HEIGHT @"Minimum Text Height" // Preference key for text entry height #define KEY_ENTRY_USER_LIST_MIN_WIDTH @"UserList Minimum Width" // Preference key for user list width #define KEY_USER_LIST_VISIBLE_PREFIX @"Userlist Visible Chat:" // Preference key prefix for user list visibility #define KEY_USER_LIST_ON_RIGHT @"UserList On Right" // Preference key for user list being on the right @interface AIMessageViewController () - ( id ) initForChat : ( AIChat * ) inChat ; - ( void ) chatStatusChanged: ( NSNotification * ) notification ; - ( void ) chatParticipatingListObjectsChanged: ( NSNotification * ) notification ; - ( void ) _configureMessageDisplay ; - ( void ) _createAccountSelectionView ; - ( void ) _destroyAccountSelectionView ; - ( void ) _configureTextEntryView ; - ( void ) _updateTextEntryViewHeight ; - ( CGFloat ) _textEntryViewProperHeightIgnoringUserMininum: ( BOOL ) ignoreUserMinimum ; - ( void ) _showUserListView ; - ( void ) _hideUserListView ; - ( void ) _configureUserList ; - ( CGFloat ) _userListViewDividerPositionIgnoringUserMinimum: ( BOOL ) ignoreUserMinimum ; - ( void ) updateFramesForAccountSelectionView ; - ( void ) saveUserListMinimumSize ; - ( BOOL ) userListInitiallyVisible ; - ( void ) setUserListVisible: ( BOOL ) inVisible ; - ( NSArray * ) contactsMatchingBeginningString: ( NSString * ) partialWord ; - ( void ) alertDidEnd: ( NSAlert * ) alert returnCode: ( NSInteger ) returnCode contextInfo: ( void * ) contextInfo ; - ( void ) gotFilteredMessageToSendLater: ( NSAttributedString * ) filteredMessage receivingContext: ( NSMutableDictionary * ) alertDict ; - ( void ) outgoingTextViewDesiredSizeDidChange: ( NSNotification * ) notification ; @implementation AIMessageViewController * @brief Create a new message view controller + ( AIMessageViewController * ) messageDisplayControllerForChat: ( AIChat * ) inChat return [[[ self alloc ] initForChat : inChat ] autorelease ]; - ( id ) initForChat: ( AIChat * ) inChat if (( self = [ super init ])) { contact = chat . listObject ; accountSelectionVisible = NO ; userListController = nil ; suppressSendLaterPrompt = NO ; //Load the view containing our controls [ NSBundle loadNibNamed : MESSAGE_VIEW_NIB owner : self ]; //Register for the various notification we need [[ NSNotificationCenter defaultCenter ] addObserver : self selector : @selector ( sendMessage : ) name : Interface_SendEnteredMessage [[ NSNotificationCenter defaultCenter ] addObserver : self selector : @selector ( didSendMessage : ) name : Interface_DidSendEnteredMessage [[ NSNotificationCenter defaultCenter ] addObserver : self selector : @selector ( chatStatusChanged : ) [[ NSNotificationCenter defaultCenter ] addObserver : self selector : @selector ( chatParticipatingListObjectsChanged : ) name : Chat_ParticipatingListObjectsChanged [[ NSNotificationCenter defaultCenter ] addObserver : self selector : @selector ( redisplaySourceAndDestinationSelector : ) [[ NSNotificationCenter defaultCenter ] addObserver : self selector : @selector ( redisplaySourceAndDestinationSelector : ) name : Chat_DestinationChanged //Observe general preferences for sending keys [ adium . preferenceController registerPreferenceObserver : self forGroup : PREF_GROUP_GENERAL ]; [ adium . preferenceController registerPreferenceObserver : self forGroup : PREF_GROUP_DUAL_WINDOW_INTERFACE ]; /* Update chat status and participating list objects to configure the user list if necessary * Call chatParticipatingListObjectsChanged first, which will set up the user list. This allows other sizing to match. [ self setUserListVisible : ( chat . isGroupChat && [ self userListInitiallyVisible ])]; [ self chatParticipatingListObjectsChanged : nil ]; [ self chatStatusChanged : nil ]; [ self _configureMessageDisplay ]; [ self _configureTextEntryView ]; [ self _configureUserList ]; [ actionBarView setBackgroundColor : [ NSColor colorWithCalibratedWhite : 0.98f alpha : 1.0f ]]; [ actionBarView setMiddleColor : [ NSColor colorWithCalibratedWhite : 0.91f alpha : 1.0f ]]; //Set our base writing direction initialBaseWritingDirection = [ contact baseWritingDirection ]; [ textView_outgoing setBaseWritingDirection : initialBaseWritingDirection ]; AIListContact * contact = chat . listObject ; [ adium . preferenceController unregisterPreferenceObserver : self ]; //Store our minimum height for the text entry area, and minimim width for the user list [ adium . preferenceController setPreference : [ NSNumber numberWithDouble : entryMinHeight ] forKey : KEY_ENTRY_TEXTVIEW_MIN_HEIGHT group : PREF_GROUP_DUAL_WINDOW_INTERFACE ]; if ( userListController ) { [ self saveUserListMinimumSize ]; //Save the base writing direction if ( contact && initialBaseWritingDirection != [ textView_outgoing baseWritingDirection ]) [ contact setBaseWritingDirection : [ textView_outgoing baseWritingDirection ]]; [ chat release ]; chat = nil ; [[ NSNotificationCenter defaultCenter ] removeObserver : self ]; [ self _destroyAccountSelectionView ]; [ messageDisplayController messageViewIsClosing ]; [ messageDisplayController release ]; [ userListController release ]; [ view_contents release ]; view_contents = nil ; [ undoManager release ]; undoManager = nil ; - ( void ) saveUserListMinimumSize [ adium . preferenceController setPreference : [ NSNumber numberWithBool : [ self userListVisible ]] forKey :[ KEY_USER_LIST_VISIBLE_PREFIX stringByAppendingFormat : @"%@.%@" , chat . account . internalObjectID , group : PREF_GROUP_DUAL_WINDOW_INTERFACE ]; [ adium . preferenceController setPreference : [ NSNumber numberWithDouble : userListMinWidth ] forKey : KEY_ENTRY_USER_LIST_MIN_WIDTH group : PREF_GROUP_DUAL_WINDOW_INTERFACE ]; - ( void ) updateGradientColors NSColor * darkerColor = [ NSColor colorWithCalibratedWhite : 0.90f alpha : 1.0f ]; NSColor * lighterColor = [ NSColor colorWithCalibratedWhite : 0.92f alpha : 1.0f ]; NSColor * leftColor = nil , * rightColor = nil ; switch ([ messageWindowController tabPosition ]) { case AdiumTabPositionBottom : case AdiumTabPositionTop : case AdiumTabPositionLeft : leftColor = lighterColor ; rightColor = darkerColor ; case AdiumTabPositionRight : rightColor = lighterColor ; [ view_accountSelection setLeftColor : leftColor rightColor : rightColor ]; * @brief Invoked before the message view closes * This method is invoked before our message view controller's message view leaves a window. * We need to clean up our user list to invalidate cursor tracking before the view closes. - ( void ) messageViewWillLeaveWindowController: ( AIMessageWindowController * ) inWindowController if ( inWindowController ) { [ userListController contactListWillBeRemovedFromWindow ]; [ messageWindowController release ]; messageWindowController = nil ; - ( void ) messageViewAddedToWindowController: ( AIMessageWindowController * ) inWindowController if ( inWindowController ) { [ userListController contactListWasAddedBackToWindow ]; if ( inWindowController != messageWindowController ) { [ messageWindowController release ]; messageWindowController = [ inWindowController retain ]; [ self updateGradientColors ]; * @brief Retrieve the chat represented by this message view * @brief Retrieve the source account associated with this chat * @brief Retrieve the destination list object associated with this chat - ( AIListContact * ) listObject * @brief Returns the selected list object in our participants list - ( AIListObject * ) preferredListObject return [ userListView itemAtRow : [ userListView selectedRow ]]; * @brief Invoked when the status of our chat changes * The only chat status change we're interested in is one to the disallow account switching flag. When this flag * changes we update the visibility of our account status menus accordingly. - ( void ) chatStatusChanged: ( NSNotification * ) notification NSArray * modifiedKeys = [[ notification userInfo ] objectForKey : @"Keys" ]; if ( notification == nil || [ modifiedKeys containsObject : @"DisallowAccountSwitching" ]) { [ self setAccountSelectionMenuVisibleIfNeeded : YES ]; //Message Display ------------------------------------------------------------------------------------------------------ #pragma mark Message Display * @brief Configure the message display view - ( void ) _configureMessageDisplay //Create the message view messageDisplayController = [[ adium . interfaceController messageDisplayControllerForChat : chat ] retain ]; [ scrollView_messages setDocumentView : [ messageDisplayController messageView ]]; [[ scrollView_messages documentView ] setFrame : [ scrollView_messages visibleRect ]]; [ scrollView_messages setAccessibilityChild : [ scrollView_messages documentView ]]; [ textView_outgoing setNextResponder : view_contents ]; [[ scrollView_messages documentView ] setNextResponder : textView_outgoing ]; * @brief The message display controller - ( NSObject < AIMessageDisplayController > * ) messageDisplayController return messageDisplayController ; * @brief Access to our view - ( NSScrollView * ) messagesScrollView return scrollView_messages ; * @brief Support for printing. Forward the print command to our message display view - ( void ) adiumPrint: ( id ) sender if ([ messageDisplayController respondsToSelector : @selector ( adiumPrint : )]) { [ messageDisplayController adiumPrint : sender ]; //Messaging ------------------------------------------------------------------------------------------------------------ * @brief Send the entered message - ( IBAction ) sendMessage: ( id ) sender NSAttributedString * attributedString = [ textView_outgoing textStorage ]; //Only send if we have a non-zero-length string if ([ attributedString length ] != 0 ) { AIListObject * listObject = chat . listObject ; //If user typed command /clear, reset the content of the view 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 ) { //Reset the content of the view [ messageDisplayController clearView ]; //Reset the content of the text field, removing the command as it has been executed [ self clearTextEntryView ]; //Commands are not messages, so they don't have to be sent if ( chat . isGroupChat && ! chat . account . online ) { //Refuse to do anything with a group chat for an offline account. AIChatSendingAbilityType messageSendingAbility = chat . messageSendingAbility ; if ( suppressSendLaterPrompt || ( messageSendingAbility == AIChatCanSendMessageNow ) || (( messageSendingAbility == AIChatCanSendViaServersideOfflineMessage ) && chat . account . sendOfflineMessagesWithoutPrompting )) { AIContentMessage * message ; NSAttributedString * outgoingAttributedString ; AIAccount * account = chat . account ; [[ NSNotificationCenter defaultCenter ] postNotificationName : Interface_WillSendEnteredMessage outgoingAttributedString = [ attributedString copy ]; message = [ AIContentMessage messageInChat : chat destination : chat . listObject date : nil //created for us by AIContentMessage message : outgoingAttributedString [ outgoingAttributedString release ]; if ([ adium . contentController sendContentObject : message ]) { [[ NSNotificationCenter defaultCenter ] postNotificationName : Interface_DidSendEnteredMessage /* If we sent with AIChatCanSendViaServersideOfflineMessage, we should probably show a status message to * the effect AILocalizedString(@"Your message has been sent. %@ will receive it when online.", nil) NSString * formattedUID = listObject . formattedUID ; NSAlert * alert = [[ NSAlert alloc ] init ]; NSImage * icon = ([ listObject userIcon ] ? [ listObject userIcon ] : [ AIServiceIcons serviceIconForObject : listObject direction : AIIconNormal ]); icon = [[ icon copy ] autorelease ]; [ icon setScalesWhenResized : NO ]; [ alert setAlertStyle : NSInformationalAlertStyle ]; [ alert setMessageText : [ NSString stringWithFormat : AILocalizedString ( @"%@ appears to be offline. How do you want to send this message?" , nil ), switch ( messageSendingAbility ) { case AIChatCanSendViaServersideOfflineMessage : [ alert setInformativeText : [ NSString stringWithFormat : 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\n Send 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." ), formattedUID , formattedUID ]]; [ alert addButtonWithTitle : AILocalizedString ( @"Send Now" , nil )]; [ alert addButtonWithTitle : AILocalizedString ( @"Send When Both Online" , nil )]; [[[ alert buttons ] objectAtIndex : 1 ] setKeyEquivalent : @"b" ]; [[[ alert buttons ] objectAtIndex : 1 ] setKeyEquivalentModifierMask : 0 ]; case AIChatMayNotBeAbleToSendMessage : [ alert setInformativeText : [ NSString stringWithFormat : 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" ), formattedUID , formattedUID , formattedUID ]]; [ alert addButtonWithTitle : AILocalizedString ( @"Send Now" , nil )]; [ alert addButtonWithTitle : AILocalizedString ( @"Send Later" , nil )]; [[[ alert buttons ] objectAtIndex : 1 ] setKeyEquivalent : @"l" ]; [[[ alert buttons ] objectAtIndex : 1 ] setKeyEquivalentModifierMask : 0 ]; case AIChatCanNotSendMessage : [ alert setInformativeText : [ NSString stringWithFormat : AILocalizedString ( @"Send Later will send the message the next time both you and %@ are online." , "Send Later dialogue explanation text" ), formattedUID , formattedUID , formattedUID ]]; [ alert addButtonWithTitle : AILocalizedString ( @"Send Later" , nil )]; [[[ alert buttons ] objectAtIndex : 0 ] setKeyEquivalent : @"l" ]; [[[ alert buttons ] objectAtIndex : 0 ] setKeyEquivalentModifierMask : 0 ]; case AIChatCanSendMessageNow : //We will never get here. [ alert addButtonWithTitle : AILocalizedString ( @"Don't Send" , nil )]; NSButton * dontSendButton = (( messageSendingAbility == AIChatCanNotSendMessage ) ? [[ alert buttons ] objectAtIndex : 1 ] : [[ alert buttons ] objectAtIndex : 2 ]); [ dontSendButton setKeyEquivalent : @"\E" ]; [ dontSendButton setKeyEquivalentModifierMask : 0 ]; [ alert beginSheetModalForWindow : [ view_contents window ] modalDelegate :[ self retain ] /* Will release after the sheet ends */ didEndSelector : @selector ( alertDidEnd : returnCode : contextInfo : ) contextInfo :[[ NSNumber numberWithInteger : messageSendingAbility ] retain ] /* Will release after the sheet ends */ ]; * @brief Send Later button was pressed - ( void ) alertDidEnd: ( NSAlert * ) alert returnCode: ( NSInteger ) returnCode contextInfo: ( void * ) contextInfo AIChatSendingAbilityType messageSendingAbility = [( NSNumber * ) contextInfo intValue ]; case NSAlertFirstButtonReturn : /* The AIChatCanNotSendMessage dalogue has Send Later as the first choice; * all others have Send Now as the first choice. if ( messageSendingAbility == AIChatCanNotSendMessage ) { [ self sendMessageLater : nil ]; suppressSendLaterPrompt = YES ; case NSAlertSecondButtonReturn : /* The AIChatCanNotSendMessage dalogue has Cancel as the second choice; * all others have Send Later as the first choice. if ( messageSendingAbility != AIChatCanNotSendMessage ) { [ self sendMessageLater : nil ]; case NSAlertThirdButtonReturn : /* Don't Send */ //Retained when the alert was created to guard against a crash if the chat tab being closed while we are open [( NSNumber * ) contextInfo release ]; * @brief Invoked after our entered message sends * This method hides the account selection view and clears the entered message after our message sends - ( IBAction ) didSendMessage: ( id ) sender [ self setAccountSelectionMenuVisibleIfNeeded : NO ]; [ self clearTextEntryView ]; * @brief Offline messaging - ( IBAction ) sendMessageLater: ( id ) sender //If the chat can _now_ send a message, send it immediately instead of waiting for "later". if ([ chat messageSendingAbility ] == AIChatCanSendMessageNow ) { [ self sendMessage : sender ]; //Put the alert on the metaContact containing this listContact if applicable AIMetaContact * listContact = chat . listObject . metaContact ; NSMutableDictionary * detailsDict , * alertDict ; detailsDict = [ NSMutableDictionary dictionary ]; [ detailsDict setObject : chat . account . internalObjectID forKey : @"Account ID" ]; [ detailsDict setObject : [ NSNumber numberWithBool : YES ] forKey : @"Allow Other" ]; [ detailsDict setObject : listContact . internalObjectID forKey : @"Destination ID" ]; alertDict = [ NSMutableDictionary dictionary ]; [ alertDict setObject : detailsDict forKey : @"ActionDetails" ]; [ alertDict setObject : CONTACT_SEEN_ONLINE_YES forKey : @"EventID" ]; [ alertDict setObject : @"SendMessage" forKey : @"ActionID" ]; [ alertDict setObject : [ NSNumber numberWithBool : YES ] forKey : @"OneTime" ]; [ alertDict setObject : listContact forKey : @"TEMP-ListContact" ]; [ adium . contentController filterAttributedString : [[[ textView_outgoing textStorage ] copy ] autorelease ] usingFilterType : AIFilterContent direction : AIFilterOutgoing filterContext : listContact selector : @selector ( gotFilteredMessageToSendLater : receivingContext : ) [ self didSendMessage : nil ]; * @brief Offline messaging //XXX - Offline messaging code SHOULD NOT BE IN HERE! -ai - ( void ) gotFilteredMessageToSendLater: ( NSAttributedString * ) filteredMessage receivingContext: ( NSMutableDictionary * ) alertDict NSMutableDictionary * detailsDict ; AIListContact * listContact ; detailsDict = [ alertDict objectForKey : @"ActionDetails" ]; [ detailsDict setObject : [ filteredMessage dataRepresentation ] forKey : @"Message" ]; listContact = [[ alertDict objectForKey : @"TEMP-ListContact" ] retain ]; [ alertDict removeObjectForKey : @"TEMP-ListContact" ]; [ adium . contactAlertsController addAlert : alertDict //Account Selection ---------------------------------------------------------------------------------------------------- #pragma mark Account Selection - ( void ) accountSelectionViewFrameDidChange: ( NSNotification * ) notification [ self updateFramesForAccountSelectionView ]; * @brief Redisplay the source/destination account selector - ( void ) redisplaySourceAndDestinationSelector: ( NSNotification * ) notification // Update the textView's chat source, in case any attributes it monitors changed. [ textView_outgoing setChat : chat ]; [ self setAccountSelectionMenuVisibleIfNeeded : YES ]; * @brief Toggle visibility of the account selection menus * Invoking this method with NO will hide the account selection menus. Invoking it with YES will show the account * selection menus if they are needed. - ( void ) setAccountSelectionMenuVisibleIfNeeded: ( BOOL ) makeVisible //Hide or show the account selection view as requested [ self _createAccountSelectionView ]; [ self _destroyAccountSelectionView ]; * @brief Show the account selection view - ( void ) _createAccountSelectionView if ( ! accountSelectionVisible ) { //Setup the account selection view [ view_accountSelection setChat : chat ]; [ self updateGradientColors ]; //Insert the account selection view at the top of our view accountSelectionVisible = YES ; [[ NSNotificationCenter defaultCenter ] addObserver : self selector : @selector ( accountSelectionViewFrameDidChange : ) name : AIViewFrameDidChangeNotification object : view_accountSelection ]; [ self updateFramesForAccountSelectionView ]; [ view_accountSelection setChat : chat ]; * @brief Hide the account selection view - ( void ) _destroyAccountSelectionView if ( accountSelectionVisible ) { [[ NSNotificationCenter defaultCenter ] removeObserver : self name : AIViewFrameDidChangeNotification object : view_accountSelection ]; accountSelectionVisible = NO ; [ self updateFramesForAccountSelectionView ]; * @brief Position the account selection view, if it is present, and the messages/text entry splitview appropriately - ( void ) updateFramesForAccountSelectionView CGFloat accountSelectionHeight = ( accountSelectionVisible ? NSHeight ( view_accountSelection . frame ) : 0.0f ); NSRect verticalFrame = splitView_verticalSplit . frame ; verticalFrame . size . height = NSHeight ( view_contents . frame ) - accountSelectionHeight - NSMinY ( verticalFrame ) - 2 ; verticalFrame . size . width = NSWidth ( view_contents . frame ); [ splitView_verticalSplit setFrame : verticalFrame ]; [ view_accountSelection setFrameOrigin : NSMakePoint ( NSMinX ( splitView_verticalSplit . frame ), NSMaxY ( splitView_verticalSplit . frame ))]; [ view_accountSelection setHidden :! accountSelectionVisible ]; [ self _updateTextEntryViewHeight ]; //Text Entry ----------------------------------------------------------------------------------------------------------- * @brief Preferences changed, update sending keys - ( void ) preferencesChangedForGroup: ( NSString * ) group key: ( NSString * ) key object: ( AIListObject * ) object preferenceDict :( NSDictionary * ) prefDict firstTime: ( BOOL ) firstTime if ([ group isEqualToString : PREF_GROUP_GENERAL ]) { [ textView_outgoing setSendOnReturn : [[ prefDict objectForKey : SEND_ON_RETURN ] boolValue ]]; [ textView_outgoing setSendOnEnter : [[ prefDict objectForKey : SEND_ON_ENTER ] boolValue ]]; } else if ([ group isEqualToString : PREF_GROUP_DUAL_WINDOW_INTERFACE ]) { if ( firstTime || [ key isEqualToString : KEY_USER_LIST_ON_RIGHT ]) { userListOnRight = [[ prefDict objectForKey : KEY_USER_LIST_ON_RIGHT ] boolValue ]; NSRect userListFrame = view_userList . frame ; //Rearrange the splitviews [ view_userList removeFromSuperviewWithoutNeedingDisplay ]; [ splitView_verticalSplit addSubview : view_userList ]; userListFrame . origin . x = splitView_textEntryHorizontal . frame . size . width ; [[ splitView_textEntryHorizontal superview ] removeFromSuperviewWithoutNeedingDisplay ]; [ splitView_verticalSplit addSubview : [ splitView_textEntryHorizontal superview ]]; userListFrame . origin . x = 0.0f ; [ view_userList setFrame : userListFrame ]; [ splitView_verticalSplit adjustSubviews ]; if ( firstTime || [ key isEqualToString : KEY_ENTRY_USER_LIST_MIN_WIDTH ]) { userListMinWidth = [[ prefDict objectForKey : KEY_ENTRY_USER_LIST_MIN_WIDTH ] doubleValue ]; * @brief Configure the text entry view - ( void ) _configureTextEntryView //Configure the text entry view [ textView_outgoing setTarget : self action : @selector ( sendMessage : )]; //This is necessary for tab completion. [ textView_outgoing setDelegate : self ]; [ textView_outgoing setTextContainerInset : NSMakeSize ( 0 , 2 )]; if ([ textView_outgoing respondsToSelector : @selector ( setUsesFindPanel : )]) { [ textView_outgoing setUsesFindPanel : YES ]; [ textView_outgoing setClearOnEscape : YES ]; [ textView_outgoing setTypingAttributes : [ adium . contentController defaultFormattingAttributes ]]; //User's choice of mininum height for their text entry view entryMinHeight = [[ adium . preferenceController preferenceForKey : KEY_ENTRY_TEXTVIEW_MIN_HEIGHT group : PREF_GROUP_DUAL_WINDOW_INTERFACE ] doubleValue ]; entryMinHeight = ENTRY_TEXTVIEW_MIN_HEIGHT ; //Associate the view with our message view so it knows which view to scroll in response to page up/down //and other special key-presses. [ textView_outgoing setAssociatedView : [ messageDisplayController messageScrollView ]]; //Associate the text entry view with our chat and inform Adium that it exists. //This is necessary for text entry filters to work correctly. [ textView_outgoing setChat : chat ]; //Observe text entry view size changes so we can dynamically resize as the user enters text [[ NSNotificationCenter defaultCenter ] addObserver : self selector : @selector ( outgoingTextViewDesiredSizeDidChange : ) name : AIViewDesiredSizeDidChangeNotification object : textView_outgoing ]; [ self _updateTextEntryViewHeight ]; // Disable elastic scroll // Remove the check on 10.7+ // Not sure why it won't work in AIMessageEntryTextView if ([[ textView_outgoing enclosingScrollView ] respondsToSelector : @selector ( setVerticalScrollElasticity : )]) { [[ textView_outgoing enclosingScrollView ] setVerticalScrollElasticity : 1 ]; // Swap 1 with NSScrollElasticityNone on 10.7+ * @brief Sets our text entry view as the first responder - ( void ) makeTextEntryViewFirstResponder [[ textView_outgoing window ] makeFirstResponder : textView_outgoing ]; [ self makeTextEntryViewFirstResponder ]; /* When we're selected, it's as if the user list controller is back in the window */ [ userListController contactListWasAddedBackToWindow ]; /* When we're deselected (backgrounded), the user list controller is effectively out of the window */ [ userListController contactListWillBeRemovedFromWindow ]; // Mark the current location in the message display for this change, if it's not an inactive-switch. if ( messageWindowController . window . isKeyWindow ) { [ messageDisplayController markForFocusChange ]; * @brief Returns the Text Entry View * Make sure you need to use this. If you just need to enter text, see -addToTextEntryView: - ( AIMessageEntryTextView * ) textEntryView return textView_outgoing ; * @brief Clear the message entry text view - ( void ) clearTextEntryView NSWritingDirection writingDirection ; writingDirection = [ textView_outgoing baseWritingDirection ]; [ textView_outgoing setString : @"" ]; [ textView_outgoing setTypingAttributes : [ adium . contentController defaultFormattingAttributes ]]; [ textView_outgoing setBaseWritingDirection : writingDirection ]; //Preserve the writing diraction [[ NSNotificationCenter defaultCenter ] postNotificationName : NSTextDidChangeNotification object : textView_outgoing ]; [ self _updateTextEntryViewHeight ]; * @brief Add text to the message entry text view * Adds the passed string to the entry text view at the insertion point. If there is selected text in the view, it - ( void ) addToTextEntryView: ( NSAttributedString * ) inString [ textView_outgoing insertText : inString ]; [[ NSNotificationCenter defaultCenter ] postNotificationName : NSTextDidChangeNotification object : textView_outgoing ]; * @brief Add data to the message entry text view * Adds the passed pasteboard data to the entry text view at the insertion point. If there is selected text in the * view, it will be replaced. - ( void ) addDraggedDataToTextEntryView: ( id < NSDraggingInfo > ) draggingInfo [ textView_outgoing performDragOperation : draggingInfo ]; [[ NSNotificationCenter defaultCenter ] postNotificationName : NSTextDidChangeNotification object : textView_outgoing ]; * @brief Update the text entry view's height when its desired size changes - ( void ) outgoingTextViewDesiredSizeDidChange: ( NSNotification * ) notification [ self _updateTextEntryViewHeight ]; - ( void ) tabViewDidChangeVisibility [ self _updateTextEntryViewHeight ]; * @brief Update the height of our text entry view - ( void ) _updateTextEntryViewHeight //Store the user's height so that autoresizing isn't messed up CGFloat oldeHeight = entryMinHeight ; [ splitView_textEntryHorizontal setPosition : [ self _textEntryViewProperHeightIgnoringUserMininum : NO ] entryMinHeight = oldeHeight ; * @brief Returns the height our text entry view should be * This method takes into account user preference, the amount of entered text, and the current window size to return * a height which is most ideal for the text entry view. * @param ignoreUserMininum If YES, the user's preference for mininum height will be ignored - ( CGFloat ) _textEntryViewProperHeightIgnoringUserMininum: ( BOOL ) ignoreUserMinimum //Our primary goal is to display all of the entered text CGFloat desiredHeight = [ textView_outgoing desiredSize ]. height ; //But we must never fall below the user's prefered minimum if ( ! ignoreUserMinimum && ( desiredHeight < entryMinHeight )) desiredHeight = entryMinHeight ; if ( desiredHeight < ENTRY_TEXTVIEW_MIN_HEIGHT ) desiredHeight = ENTRY_TEXTVIEW_MIN_HEIGHT ; //Or above the allowed height if ( desiredHeight >= ( splitView_textEntryHorizontal . frame . size . height * MESSAGE_VIEW_MIN_HEIGHT_RATIO )) return ( splitView_textEntryHorizontal . frame . size . height * MESSAGE_VIEW_MIN_HEIGHT_RATIO ); CGFloat splitViewHeight = NSHeight ( splitView_textEntryHorizontal . frame ); CGFloat dividerThickness = [ splitView_textEntryHorizontal dividerThickness ]; return splitViewHeight - desiredHeight - dividerThickness ; #pragma mark Autocompletion - ( BOOL ) canTabCompleteForPartialWord: ( NSString * ) partialWord return ([ self contactsMatchingBeginningString : partialWord ]. count > 0 || [ self . chat . displayName rangeOfString : partialWord options : ( NSDiacriticInsensitiveSearch | NSCaseInsensitiveSearch | NSAnchoredSearch )]. location != NSNotFound ); * @brief Should the tab key cause an autocompletion if possible? * We only tab to autocomplete for a group chat - ( BOOL ) textViewShouldTabComplete: ( NSTextView * ) inTextView if ( self . chat . isGroupChat ) { NSRange completionRange = inTextView . rangeForUserCompletion ; NSString * partialWord = [ inTextView . textStorage . string substringWithRange : completionRange ]; return [ self canTabCompleteForPartialWord : partialWord ]; - ( NSRange ) textView: ( NSTextView * ) inTextView rangeForCompletion: ( NSRange ) charRange if ( self . chat . isGroupChat && charRange . location > 0 ) { NSString * partialWord = nil ; NSString * allText = [ inTextView . textStorage . string substringWithRange : NSMakeRange ( 0 , NSMaxRange ( charRange ))]; NSRange whitespacePosition = [ allText rangeOfCharacterFromSet : [ NSCharacterSet whitespaceCharacterSet ] options : NSBackwardsSearch ]; if ( whitespacePosition . location == NSNotFound ) { // We went back to the beginning of the string and still didn't find a whitespace; use the whole thing. whitespacePosition = NSMakeRange ( 0 , 0 ); // We found a whitespace, use from it until our current position. partialWord = [ allText substringWithRange : NSMakeRange ( NSMaxRange ( whitespacePosition ), allText . length - NSMaxRange ( whitespacePosition ))]; // If this matches any contacts or the room name, use this new range for autocompletion. if ([ self canTabCompleteForPartialWord : partialWord ]) { charRange = NSMakeRange ( NSMaxRange ( whitespacePosition ), allText . length - NSMaxRange ( whitespacePosition )); - ( NSArray * ) contactsMatchingBeginningString: ( NSString * ) partialWord NSMutableArray * contacts = [ NSMutableArray array ]; for ( AIListContact * listContact in self . chat ) { // Add to the list if it matches: (1) The display name for the chat (alias fallback to default display name), // (2) The UID, or (3) the display name if ([[ self . chat displayNameForContact : listContact ] rangeOfString : partialWord options : ( NSDiacriticInsensitiveSearch | NSCaseInsensitiveSearch | NSAnchoredSearch )]. location != NSNotFound || [ listContact . UID rangeOfString : partialWord options : ( NSDiacriticInsensitiveSearch | NSCaseInsensitiveSearch | NSAnchoredSearch )]. location != NSNotFound || [ listContact . displayName rangeOfString : partialWord options : ( NSDiacriticInsensitiveSearch | NSCaseInsensitiveSearch | NSAnchoredSearch )]. location != NSNotFound ) { [ contacts addObject : listContact ]; AILogWithSignature ( @"Added match %@ with nick %@; UID: %@; formattedUID: %@; displayName: %@" , listContact , [ self . chat aliasForContact : listContact ], listContact . UID , listContact . formattedUID , listContact . displayName ); - ( NSArray * ) textView: ( NSTextView * ) textView completions: ( NSArray * ) words forPartialWordRange: ( NSRange ) charRange indexOfSelectedItem: ( NSInteger * ) idx NSMutableArray * completions = nil ; if ( self . chat . isGroupChat ) { NSString * suffix = [ self . chat . account suffixForAutocomplete : self . chat forPartialWordRange : charRange ]; NSString * prefix = [ self . chat . account prefixForAutocomplete : self . chat forPartialWordRange : charRange ]; NSString * partialWord = [ textView . textStorage . string substringWithRange : charRange ]; BOOL autoCompleteUID = [ self . chat . account chatShouldAutocompleteUID : self . chat ]; // Check to see if the prefix is already present if ( charRange . location != 0 && charRange . location >= prefix . length ) { prefix = [[ textView . textStorage . string substringWithRange : NSMakeRange ( charRange . location - prefix . length , prefix . length )] isEqualToString : prefix ] ? nil : prefix ; // If we need to add a prefix, insert it into the text, then call [textView complete:] again; return early with no completions. [ textView . textStorage insertAttributedString : [[[ NSAttributedString alloc ] initWithString : prefix ] autorelease ] atIndex : charRange . location ]; // Check to see if the suffix is already present if ( charRange . location + charRange . length + suffix . length <= textView . textStorage . string . length ) { suffix = [[ textView . textStorage . string substringWithRange : NSMakeRange ( charRange . location + charRange . length , suffix . length )] isEqualToString : suffix ] ? nil : suffix ; completions = [ NSMutableArray array ]; // For each matching contact: for ( AIListContact * listContact in [ self contactsMatchingBeginningString : partialWord ]) { // Complete the chat alias. NSString * completion = [ self . chat aliasForContact : listContact ]; // Otherwise, complete the UID (if we're completing UIDs for this chat) or the display name. completion = autoCompleteUID ? listContact . formattedUID : listContact . displayName ; [ completions addObject : ( suffix ? [ completion stringByAppendingString : suffix ] : completion )]; // Add the name of this chat to the completions if it matches. if ([ self . chat . displayName rangeOfString : partialWord options : ( NSDiacriticInsensitiveSearch | NSCaseInsensitiveSearch | NSAnchoredSearch )]. location != NSNotFound ) { [ completions addObject : self . chat . displayName ]; // Select the first completion by default. if ([ completions count ]) { return [ completions count ] ? completions : words ; //User List ------------------------------------------------------------------------------------------------------------ * @brief Selected list objects * An array of the list objects selected in the user list. - ( NSArray * ) selectedListObjects return [ userListView arrayOfListObjects ]; * @brief Is the user list initially visible? - ( BOOL ) userListInitiallyVisible NSNumber * visibility = [ adium . preferenceController preferenceForKey : [ KEY_USER_LIST_VISIBLE_PREFIX stringByAppendingFormat : @"%@.%@" , chat . account . internalObjectID , group : PREF_GROUP_DUAL_WINDOW_INTERFACE ]; return visibility ? [ visibility boolValue ] : YES ; * @brief Set visibility of the user list - ( void ) setUserListVisible: ( BOOL ) inVisible [ self _showUserListView ]; [ self _hideUserListView ]; [ adium . preferenceController setPreference : [ NSNumber numberWithBool : inVisible ] forKey :[ KEY_USER_LIST_VISIBLE_PREFIX stringByAppendingFormat : @"%@.%@" , chat . account . internalObjectID , group : PREF_GROUP_DUAL_WINDOW_INTERFACE ]; * @brief Returns YES if the user list is currently visible return ! [ view_userList isHidden ]; * @brief toggles the state of the userlist shelf [ self setUserListVisible :! [ self userListVisible ]]; - ( void ) toggleUserListSide userListOnRight = ! userListOnRight ; // We'll update the actual side when this preference change is told to us. [ adium . preferenceController setPreference : [ NSNumber numberWithInteger : userListOnRight ] forKey : KEY_USER_LIST_ON_RIGHT group : PREF_GROUP_DUAL_WINDOW_INTERFACE ]; * @brief Show the user list - ( void ) _showUserListView if ( chat . isGroupChat && view_userList . superview == nil ) { [ splitView_verticalSplit addSubview : [ view_userList autorelease ]]; [ userListController reloadData ]; [ view_userList setHidden : NO ]; //Manually set the divider's position otherwise view_userList will shrink [ splitView_verticalSplit setPosition : [ self _userListViewDividerPositionIgnoringUserMinimum : NO ] [ splitView_verticalSplit adjustSubviews ]; * @brief Hide the user list. - ( void ) _hideUserListView NSRect frame = view_userList . frame ; view_userList . frame = frame ; [ view_userList removeFromSuperview ]; [ view_userList setHidden : YES ]; [ splitView_verticalSplit adjustSubviews ]; * @brief Configure the user list * Configures the user list view and prepares it for display. If the user list is not being shown, this configuration * should be avoided for performance. - ( void ) _configureUserList NSDictionary * themeDict = [ NSDictionary dictionaryNamed : USERLIST_THEME forClass : [ self class ]]; NSDictionary * layoutDict = [ NSDictionary dictionaryNamed : USERLIST_LAYOUT forClass : [ self class ]]; //Create and configure a controller to manage the user list userListController = [[ ESChatUserListController alloc ] initWithContactListView : userListView inScrollView : scrollView_userList [ userListController setContactListRoot : chat ]; [ userListController updateLayoutFromPrefDict : layoutDict andThemeFromPrefDict : themeDict ]; [ userListController setHideRoot : YES ]; * @brief Update the user list in response to changes * This method is invoked when the chat's participating contacts change. In resopnse, it sets correct visibility of * the user list, and updates the displayed users. - ( void ) chatParticipatingListObjectsChanged: ( NSNotification * ) notification [ chat resortParticipants ]; /* Even if we're not viewing the user list, we can't risk it keeping stale information about potentially released objects */ [ userListController reloadData ]; if ([ self userListVisible ]) { NSString * userCount = nil ; if ( self . chat . containedObjects . count == 1 ) { userCount = AILocalizedString ( @"1 user" , nil ); userCount = AILocalizedString ( @"%u users" , nil ); [ label_userCount setStringValue : [ NSString stringWithFormat : userCount , self . chat . containedObjects . count ]]; * @brief The selection in the user list changed * When the user list selection changes, we update the chat's "preferred list object", which is used * elsewhere to identify the currently 'selected' contact for Get Info, Messaging, etc. - ( void ) outlineViewSelectionDidChange: ( NSNotification * ) notification if ([ notification object ] == userListView ) { [ chat setPreferredListObject : ( AIListContact * )[ userListView listObject ]]; * @brief Perform default action on the selected user list object * Here we could open a private message or display info for the user, however we perform no action - ( void ) performDefaultActionOnSelectedObject: ( AIListObject * ) listObject sender: ( NSOutlineView * ) sender if ([ listObject isKindOfClass : [ AIListContact class ]]) { // We should default to this contact's account [ adium . interfaceController setActiveChat : [ adium . chatController openChatWithContact : ( AIListContact * ) listObject * @brief Capture all text input * Capture all text input in our user list and forward it to the text entry view. * This prevents the user list from becoming a black hole if it's clicked on. - ( BOOL ) forwardKeyEventToFindPanel: ( NSEvent * ) theEvent [ self makeTextEntryViewFirstResponder ]; [ self . textEntryView keyDown : theEvent ]; * @brief Returns the width our user list view should be * This method takes into account user preference and the current window size to return a width which is most * ideal for the user list view. - ( CGFloat ) _userListViewDividerPositionIgnoringUserMinimum: ( BOOL ) ignoreUserMinimum CGFloat splitViewWidth = splitView_verticalSplit . frame . size . width ; CGFloat allowedWidth = AIfloor ( splitViewWidth / 2 ) - [ splitView_verticalSplit dividerThickness ]; CGFloat width = ignoreUserMinimum ? USER_LIST_DEFAULT_WIDTH : userListMinWidth ; if ( width < USER_LIST_DEFAULT_WIDTH ) width = USER_LIST_DEFAULT_WIDTH ; if ( width > allowedWidth ) return splitViewWidth - width ; - ( IBAction ) showActionMenu: ( id ) sender { [ chat . actionMenu popUpMenuPositioningItem : nil atLocation : performAction . frame . origin inView : actionBarView ]; //Split Views -------------------------------------------------------------------------------------------------- * @brief Update the sizes of our user splitviews - ( void ) splitViewWillResizeSubviews: ( NSNotification * ) aNotification if ([ aNotification object ] == splitView_verticalSplit ) { if ( NSWidth ( view_userList . frame ) > 0 ) { userListMinWidth = NSWidth ( view_userList . frame ); [ NSObject cancelPreviousPerformRequestsWithTarget : self selector : @selector ( saveUserListMinimumSize ) object : nil ]; [ self performSelector : @selector ( saveUserListMinimumSize ) withObject : nil afterDelay : 0.5 ]; } else if ([ aNotification object ] == splitView_textEntryHorizontal && [ splitView_textEntryHorizontal inLiveResize ]) { entryMinHeight = NSHeight ( textView_outgoing . frame ); * @brief Set the appropriate preference when the user list is dragged open or closed and update user count. - ( void ) splitViewDidResizeSubviews: ( NSNotification * ) aNotification if ([ aNotification object ] == splitView_verticalSplit ) { NSRect userListFrame = view_userList . frame ; if ( NSWidth ( userListFrame ) > 0 ) { * @brief Keep the userlist and text entry view the same size when the window is resized. - ( void ) splitView: ( NSSplitView * ) splitView resizeSubviewsWithOldSize: ( NSSize ) oldSize if ([ splitView inLiveResize ] || adium . interfaceController . activeChat != chat ) { // division between user list and message view if ( splitView == splitView_verticalSplit ) { NSRect currentFrame = splitView . frame ; NSRect msgFrame = [ splitView_textEntryHorizontal superview ]. frame ; NSRect userFrame = view_userList . frame ; CGFloat dividerThickness = [ splitView dividerThickness ]; BOOL userListVisible = [ self userListVisible ]; BOOL userListAttached = view_userList . superview != nil ; msgFrame . size . height = currentFrame . size . height ; userFrame . size . height = currentFrame . size . height ; userFrame . size . width = currentFrame . size . width - [ self _userListViewDividerPositionIgnoringUserMinimum : NO ]; userFrame . size . width = [ self _userListViewDividerPositionIgnoringUserMinimum : NO ]; userFrame . size . width = 0 ; if ([ view_userList isHidden ]) { msgFrame . size . width += 1 - dividerThickness ; if ( userListOnRight && userListAttached ){ msgFrame . size . width = currentFrame . size . width - userFrame . size . width - dividerThickness ; userFrame . origin . x = msgFrame . size . width + dividerThickness ; } else if ( userListAttached ) { msgFrame . origin . x = NSMaxX ( userFrame ) + dividerThickness ; msgFrame . size . width = currentFrame . size . width - userFrame . size . width - dividerThickness ; msgFrame . size . width = currentFrame . size . width ; userFrame . origin . x = userListOnRight ? currentFrame . size . width + dividerThickness : -1 ; [ view_userList setFrame : userFrame ]; [[ splitView_textEntryHorizontal superview ] setFrame : msgFrame ]; // divition between text entry and message view } else if ( splitView == splitView_textEntryHorizontal ) { NSRect currentFrame = splitView . frame ; NSRect msgFrame = view_messages . frame ; NSRect textFrame = [ scrollView_textEntry superview ]. frame ; CGFloat dividerThickness = [ splitView dividerThickness ]; textFrame . size . width = currentFrame . size . width ; msgFrame . size . width = currentFrame . size . width ; msgFrame . size . height = currentFrame . size . height - textFrame . size . height - dividerThickness ; textFrame . origin . y = msgFrame . size . height + dividerThickness ; [ view_messages setFrame : msgFrame ]; [[ scrollView_textEntry superview ] setFrame : textFrame ]; [ splitView adjustSubviews ]; [ splitView adjustSubviews ]; * @brief Don't allow the text entry or message view to be collapsed - ( BOOL ) splitView: ( NSSplitView * ) splitView canCollapseSubview: ( NSView * ) subview if ( subview == view_messages || subview == [ splitView_textEntryHorizontal superview ]) else if ( subview == [ scrollView_textEntry superview ]) - ( BOOL ) splitView: ( NSSplitView * ) splitView shouldCollapseSubview: ( NSView * ) subview forDoubleClickOnDividerAtIndex: ( NSInteger ) dividerIndex if ( splitView == splitView_verticalSplit ) { if ( subview == view_userList ) * @brief Set the min size of the text entry view and the userlist - ( CGFloat ) splitView: ( NSSplitView * ) splitView constrainMaxCoordinate: ( CGFloat ) proposedMax ofSubviewAt: ( NSInteger ) dividerIndex if ( splitView == splitView_textEntryHorizontal ) { //Min size of text entry view return [ self _textEntryViewProperHeightIgnoringUserMininum : YES ]; } else if ( splitView == splitView_verticalSplit ) { //On the right: min size of user list //On the left: max size of user list return AIfloor ([ self _userListViewDividerPositionIgnoringUserMinimum : YES ] + 0.5f ); return AIfloor (( splitView_verticalSplit . frame . size . width / 2 ) + 0.5f ); return AIfloor ( splitView_verticalSplit . frame . size . width + 0.5f ); * @brief Set the max size of the text entry view and userlist * Lock the user list on non-MUCs; this is done here instead of hiding the divider so that there isn't * a visual change of positioning when changing tabs between an MUC and 1v1 - ( CGFloat ) splitView: ( NSSplitView * ) splitView constrainMinCoordinate: ( CGFloat ) proposedMin ofSubviewAt: ( NSInteger ) dividerIndex if ( splitView == splitView_textEntryHorizontal ) { //Max size of text entry view return AIfloor ( splitView_textEntryHorizontal . frame . size . height * MESSAGE_VIEW_MIN_HEIGHT_RATIO + 0.5f ); } else if ( splitView == splitView_verticalSplit ) { //On the right: max size of user list //On the left: min size of the user list return AIfloor ( splitView_verticalSplit . frame . size . width / 2 ); return [ self _userListViewDividerPositionIgnoringUserMinimum : YES ]; return splitView_verticalSplit . frame . size . width ; - ( NSUndoManager * ) undoManagerForTextView: ( NSTextView * ) aTextView undoManager = [[ NSUndoManager alloc ] init ];