| File: | Plugins/Dual Window Interface/AIMessageViewController.m |
| Location: | line 528, column 4 |
| Description: | Memory Leak |
| Code is compiled without garbage collection. |
| 1 | /* |
| 2 | * Adium is the legal property of its developers, whose names are listed in the copyright file included |
| 3 | * with this source distribution. |
| 4 | * |
| 5 | * This program is free software; you can redistribute it and/or modify it under the terms of the GNU |
| 6 | * General Public License as published by the Free Software Foundation; either version 2 of the License, |
| 7 | * or (at your option) any later version. |
| 8 | * |
| 9 | * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even |
| 10 | * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General |
| 11 | * Public License for more details. |
| 12 | * |
| 13 | * You should have received a copy of the GNU General Public License along with this program; if not, |
| 14 | * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. |
| 15 | */ |
| 16 | |
| 17 | #import "AIMessageViewController.h" |
| 18 | #import "AIAccountSelectionView.h" |
| 19 | #import "AIMessageWindowController.h" |
| 20 | #import "ESGeneralPreferencesPlugin.h" |
| 21 | #import "AIDualWindowInterfacePlugin.h" |
| 22 | #import "AIContactInfoWindowController.h" |
| 23 | #import "AIMessageTabSplitView.h" |
| 24 | #import "AIMessageWindowOutgoingScrollView.h" |
| 25 | #import "KNShelfSplitView.h" |
| 26 | #import "ESChatUserListController.h" |
| 27 | |
| 28 | #import <Adium/AIChatControllerProtocol.h> |
| 29 | #import <Adium/AIContactAlertsControllerProtocol.h> |
| 30 | #import <Adium/AIContactControllerProtocol.h> |
| 31 | #import <Adium/AIContentControllerProtocol.h> |
| 32 | #import <Adium/AIContentControllerProtocol.h> |
| 33 | #import <Adium/AIInterfaceControllerProtocol.h> |
| 34 | #import <Adium/AIMenuControllerProtocol.h> |
| 35 | #import <Adium/AIPreferenceControllerProtocol.h> |
| 36 | #import <Adium/AIToolbarControllerProtocol.h> |
| 37 | #import <Adium/AIAccount.h> |
| 38 | #import <Adium/AIChat.h> |
| 39 | #import <Adium/AIContentMessage.h> |
| 40 | #import <Adium/AIListContact.h> |
| 41 | #import <Adium/AIListObject.h> |
| 42 | #import <Adium/AIListOutlineView.h> |
| 43 | #import <Adium/AIMessageEntryTextView.h> |
| 44 | #import <Adium/ESTextAndButtonsWindowController.h> |
| 45 | |
| 46 | #import <AIUtilities/AIApplicationAdditions.h> |
| 47 | #import <AIUtilities/AIAttributedStringAdditions.h> |
| 48 | #import <AIUtilities/AIAutoScrollView.h> |
| 49 | #import <AIUtilities/AIDictionaryAdditions.h> |
| 50 | #import <AIUtilities/AISplitView.h> |
| 51 | |
| 52 | #import <AIUtilities/AITigerCompatibility.h> |
| 53 | |
| 54 | #import <PSMTabBarControl/NSBezierPath_AMShading.h> |
| 55 | |
| 56 | #import "RBSplitView.h" |
| 57 | |
| 58 | //Heights and Widths |
| 59 | #define MESSAGE_VIEW_MIN_HEIGHT_RATIO .50 //Mininum height ratio of the message view |
| 60 | #define MESSAGE_VIEW_MIN_WIDTH_RATIO .50 //Mininum width ratio of the message view |
| 61 | #define ENTRY_TEXTVIEW_MIN_HEIGHT 20 //Mininum height of the text entry view |
| 62 | #define USER_LIST_MIN_WIDTH 24 //Mininum width of the user list |
| 63 | #define USER_LIST_DEFAULT_WIDTH 120 //Default width of the user list |
| 64 | |
| 65 | //Preferences and files |
| 66 | #define MESSAGE_VIEW_NIB @"MessageView" //Filename of the message view nib |
| 67 | #define USERLIST_THEME @"UserList Theme" //File name of the user list theme |
| 68 | #define USERLIST_LAYOUT @"UserList Layout" //File name of the user list layout |
| 69 | #define KEY_ENTRY_TEXTVIEW_MIN_HEIGHT @"Minimum Text Height" //Preference key for text entry height |
| 70 | #define KEY_ENTRY_USER_LIST_MIN_WIDTH @"UserList Width" //Preference key for user list width |
| 71 | |
| 72 | #define TEXTVIEW_HEIGHT_DEBUG |
| 73 | |
| 74 | @interface AIMessageViewController (PRIVATE) |
| 75 | - (id)initForChat:(AIChat *)inChat; |
| 76 | - (void)chatStatusChanged:(NSNotification *)notification; |
| 77 | - (void)chatParticipatingListObjectsChanged:(NSNotification *)notification; |
| 78 | - (void)_configureMessageDisplay; |
| 79 | - (void)_createAccountSelectionView; |
| 80 | - (void)_destroyAccountSelectionView; |
| 81 | - (void)_configureTextEntryView; |
| 82 | - (void)_updateTextEntryViewHeight; |
| 83 | - (int)_textEntryViewProperHeightIgnoringUserMininum:(BOOL)ignoreUserMininum; |
| 84 | - (void)_showUserListView; |
| 85 | - (void)_hideUserListView; |
| 86 | - (void)_configureUserList; |
| 87 | - (void)_updateUserListViewWidth; |
| 88 | - (int)_userListViewProperWidthIgnoringUserMininum:(BOOL)ignoreUserMininum; |
| 89 | - (void)updateFramesForAccountSelectionView; |
| 90 | - (void)saveUserListMinimumSize; |
| 91 | @end |
| 92 | |
| 93 | @implementation AIMessageViewController |
| 94 | |
| 95 | /*! |
| 96 | * @brief Create a new message view controller |
| 97 | */ |
| 98 | + (AIMessageViewController *)messageDisplayControllerForChat:(AIChat *)inChat |
| 99 | { |
| 100 | return [[[self alloc] initForChat:inChat] autorelease]; |
| 101 | } |
| 102 | |
| 103 | |
| 104 | /*! |
| 105 | * @brief Initialize |
| 106 | */ |
| 107 | - (id)initForChat:(AIChat *)inChat |
| 108 | { |
| 109 | if ((self = [super init])) { |
| 110 | AIListContact *contact; |
| 111 | //Init |
| 112 | chat = [inChat retain]; |
| 113 | contact = [chat listObject]; |
| 114 | view_accountSelection = nil0; |
| 115 | userListController = nil0; |
| 116 | suppressSendLaterPrompt = NO( BOOL ) 0; |
| 117 | retainingScrollViewUserList = NO( BOOL ) 0; |
| 118 | |
| 119 | //Load the view containing our controls |
| 120 | [NSBundle loadNibNamed:MESSAGE_VIEW_NIB@ "MessageView" owner:self]; |
| 121 | |
| 122 | //Register for the various notification we need |
| 123 | [[adium notificationCenter] addObserver:self |
| 124 | selector:@selector(sendMessage:) |
| 125 | name:Interface_SendEnteredMessage@ "Interface_SendEnteredMessage" |
| 126 | object:chat]; |
| 127 | [[adium notificationCenter] addObserver:self |
| 128 | selector:@selector(didSendMessage:) |
| 129 | name:Interface_DidSendEnteredMessage@ "Interface_DidSendEnteredMessage" |
| 130 | object:chat]; |
| 131 | [[adium notificationCenter] addObserver:self |
| 132 | selector:@selector(chatStatusChanged:) |
| 133 | name:Chat_StatusChanged@ "Chat_StatusChagned" |
| 134 | object:chat]; |
| 135 | [[adium notificationCenter] addObserver:self |
| 136 | selector:@selector(chatParticipatingListObjectsChanged:) |
| 137 | name:Chat_ParticipatingListObjectsChanged@ "Chat_ParticipatingListObjectsChanged" |
| 138 | object:chat]; |
| 139 | [[adium notificationCenter] addObserver:self |
| 140 | selector:@selector(redisplaySourceAndDestinationSelector:) |
| 141 | name:Chat_SourceChanged@ "Chat_SourceChanged" |
| 142 | object:chat]; |
| 143 | [[adium notificationCenter] addObserver:self |
| 144 | selector:@selector(redisplaySourceAndDestinationSelector:) |
| 145 | name:Chat_DestinationChanged@ "Chat_DestinationChanged" |
| 146 | object:chat]; |
| 147 | [[adium notificationCenter] addObserver:self |
| 148 | selector:@selector(toggleUserlist:) |
| 149 | name:@"toggleUserlist" |
| 150 | object:nil0]; |
| 151 | |
| 152 | //Observe general preferences for sending keys |
| 153 | [[adium preferenceController] registerPreferenceObserver:self forGroup:PREF_GROUP_GENERAL@ "General"]; |
| 154 | |
| 155 | /* Update chat status and participating list objects to configure the user list if necessary |
| 156 | * Call chatParticipatingListObjectsChanged first, which will set up the user list. This allows other sizing to match. |
| 157 | */ |
| 158 | [self setUserListVisible:[chat isGroupChat]]; |
| 159 | |
| 160 | [self chatParticipatingListObjectsChanged:nil0]; |
| 161 | [self chatStatusChanged:nil0]; |
| 162 | |
| 163 | //Configure our views |
| 164 | [self _configureMessageDisplay]; |
| 165 | [self _configureTextEntryView]; |
| 166 | |
| 167 | //Set our base writing direction |
| 168 | if (contact) { |
| 169 | initialBaseWritingDirection = [contact baseWritingDirection]; |
| 170 | [textView_outgoing setBaseWritingDirection:initialBaseWritingDirection]; |
| 171 | AILogWithSignatureAILogWithPrefix ( __PRETTY_FUNCTION__ , @ "initialBaseWritingDirection is %i" , initialBaseWritingDirection ) ;(@"initialBaseWritingDirection is %i", initialBaseWritingDirection); |
| 172 | } |
| 173 | } |
| 174 | |
| 175 | return self; |
| 176 | } |
| 177 | |
| 178 | /*! |
| 179 | * @brief Deallocate |
| 180 | */ |
| 181 | - (void)dealloc |
| 182 | { |
| 183 | AIListContact *contact = [chat listObject]; |
| 184 | |
| 185 | [[adium preferenceController] unregisterPreferenceObserver:self]; |
| 186 | |
| 187 | //Store our minimum height for the text entry area, and minimim width for the user list |
| 188 | [[adium preferenceController] setPreference:[NSNumber numberWithInt:entryMinHeight] |
| 189 | forKey:KEY_ENTRY_TEXTVIEW_MIN_HEIGHT@ "Minimum Text Height" |
| 190 | group:PREF_GROUP_DUAL_WINDOW_INTERFACE@ "Dual Window Interface"]; |
| 191 | |
| 192 | if (userListController) { |
| 193 | [self saveUserListMinimumSize]; |
| 194 | } |
| 195 | |
| 196 | //Save the base writing direction |
| 197 | if (contact && initialBaseWritingDirection != [textView_outgoing baseWritingDirection]) |
| 198 | [contact setBaseWritingDirection:[textView_outgoing baseWritingDirection]]; |
| 199 | |
| 200 | [chat release]; chat = nil0; |
| 201 | |
| 202 | //remove observers |
| 203 | [[adium notificationCenter] removeObserver:self]; |
| 204 | [[NSNotificationCenter defaultCenter] removeObserver:self]; |
| 205 | |
| 206 | //Account selection view |
| 207 | [self _destroyAccountSelectionView]; |
| 208 | |
| 209 | [messageDisplayController messageViewIsClosing]; |
| 210 | [messageDisplayController release]; |
| 211 | [userListController release]; |
| 212 | |
| 213 | [controllerView_messages release]; |
| 214 | |
| 215 | //Release the views for which we are responsible (because we loaded them via -[NSBundle loadNibNamed:owner]) |
| 216 | [nibrootView_messageView release]; |
| 217 | [nibrootView_shelfVew release]; |
| 218 | [nibrootView_userList release]; |
| 219 | |
| 220 | //Release the hidden user list view |
| 221 | if (retainingScrollViewUserList) { |
| 222 | [scrollView_userList release]; |
| 223 | } |
| 224 | //release menuItem |
| 225 | [showHide release]; |
| 226 | |
| 227 | [undoManager release]; undoManager = nil0; |
| 228 | |
| 229 | [super dealloc]; |
| 230 | } |
| 231 | |
| 232 | - (void)saveUserListMinimumSize |
| 233 | { |
| 234 | [[adium preferenceController] setPreference:[NSNumber numberWithInt:userListMinWidth] |
| 235 | forKey:KEY_ENTRY_USER_LIST_MIN_WIDTH@ "UserList Width" |
| 236 | group:PREF_GROUP_DUAL_WINDOW_INTERFACE@ "Dual Window Interface"]; |
| 237 | } |
| 238 | |
| 239 | - (void)updateGradientColors |
| 240 | { |
| 241 | NSColor *darkerColor = [NSColor colorWithCalibratedWhite:0.90 alpha:1.0]; |
| 242 | NSColor *lighterColor = [NSColor colorWithCalibratedWhite:0.92 alpha:1.0]; |
| 243 | NSColor *leftColor = nil0, *rightColor = nil0; |
| 244 | |
| 245 | switch ([messageWindowController tabPosition]) { |
| 246 | case AdiumTabPositionBottom: |
| 247 | case AdiumTabPositionTop: |
| 248 | case AdiumTabPositionLeft: |
| 249 | leftColor = lighterColor; |
| 250 | rightColor = darkerColor; |
| 251 | break; |
| 252 | case AdiumTabPositionRight: |
| 253 | leftColor = darkerColor; |
| 254 | rightColor = lighterColor; |
| 255 | break; |
| 256 | } |
| 257 | |
| 258 | [view_accountSelection setLeftColor:leftColor rightColor:rightColor]; |
| 259 | //XXX |
| 260 | // [splitView_textEntryHorizontal setLeftColor:leftColor rightColor:rightColor]; |
| 261 | } |
| 262 | |
| 263 | /*! |
| 264 | * @brief Invoked before the message view closes |
| 265 | * |
| 266 | * This method is invoked before our message view controller's message view leaves a window. |
| 267 | * We need to clean up our user list to invalidate cursor tracking before the view closes. |
| 268 | */ |
| 269 | - (void)messageViewWillLeaveWindowController:(AIMessageWindowController *)inWindowController |
| 270 | { |
| 271 | if (inWindowController) { |
| 272 | [userListController contactListWillBeRemovedFromWindow]; |
| 273 | } |
| 274 | |
| 275 | [messageWindowController release]; messageWindowController = nil0; |
| 276 | } |
| 277 | |
| 278 | - (void)messageViewAddedToWindowController:(AIMessageWindowController *)inWindowController |
| 279 | { |
| 280 | if (inWindowController) { |
| 281 | [userListController contactListWasAddedBackToWindow]; |
| 282 | } |
| 283 | |
| 284 | if (inWindowController != messageWindowController) { |
| 285 | [messageWindowController release]; |
| 286 | messageWindowController = [inWindowController retain]; |
| 287 | |
| 288 | [self updateGradientColors]; |
| 289 | } |
| 290 | } |
| 291 | |
| 292 | /*! |
| 293 | * @brief Retrieve the chat represented by this message view |
| 294 | */ |
| 295 | - (AIChat *)chat |
| 296 | { |
| 297 | return chat; |
| 298 | } |
| 299 | |
| 300 | /*! |
| 301 | * @brief Retrieve the source account associated with this chat |
| 302 | */ |
| 303 | - (AIAccount *)account |
| 304 | { |
| 305 | return [chat account]; |
| 306 | } |
| 307 | |
| 308 | /*! |
| 309 | * @brief Retrieve the destination list object associated with this chat |
| 310 | */ |
| 311 | - (AIListContact *)listObject |
| 312 | { |
| 313 | return [chat listObject]; |
| 314 | } |
| 315 | |
| 316 | /*! |
| 317 | * @brief Returns the selected list object in our participants list |
| 318 | */ |
| 319 | - (AIListObject *)preferredListObject |
| 320 | { |
| 321 | if (userListView) { //[[shelfView subviews] containsObject:scrollView_userList] && ([userListView selectedRow] != -1) |
| 322 | return [userListView itemAtRow:[userListView selectedRow]]; |
| 323 | } |
| 324 | |
| 325 | return nil0; |
| 326 | } |
| 327 | |
| 328 | /*! |
| 329 | * @brief Invoked when the status of our chat changes |
| 330 | * |
| 331 | * The only chat status change we're interested in is one to the disallow account switching flag. When this flag |
| 332 | * changes we update the visibility of our account status menus accordingly. |
| 333 | */ |
| 334 | - (void)chatStatusChanged:(NSNotification *)notification |
| 335 | { |
| 336 | NSArray *modifiedKeys = [[notification userInfo] objectForKey:@"Keys"]; |
| 337 | |
| 338 | if (notification == nil0 || [modifiedKeys containsObject:@"DisallowAccountSwitching"]) { |
| 339 | [self setAccountSelectionMenuVisibleIfNeeded:YES( BOOL ) 1]; |
| 340 | } |
| 341 | } |
| 342 | |
| 343 | |
| 344 | //Message Display ------------------------------------------------------------------------------------------------------ |
| 345 | #pragma mark Message Display |
| 346 | /*! |
| 347 | * @brief Configure the message display view |
| 348 | */ |
| 349 | - (void)_configureMessageDisplay |
| 350 | { |
| 351 | //Create the message view |
| 352 | messageDisplayController = [[[adium interfaceController] messageDisplayControllerForChat:chat] retain]; |
| 353 | //Get the messageView from the controller |
| 354 | controllerView_messages = [[messageDisplayController messageView] retain]; |
| 355 | |
| 356 | /* customView_messages is really just a placeholder. It's a subview of scrollView_messages, which exists just |
| 357 | * to draw a box around itself to give the desired border. NSBox could be used for the same purpose. |
| 358 | * We replace customView_messages with the actual message view we want to use, controllerView_messages. |
| 359 | * |
| 360 | * Note that this does -not- change the documentView of scrollView_messages, which remains NULL. |
| 361 | * This is because the controllerView_messages supplies its own scroll view (within the WebView). |
| 362 | * We therefore use -[AIMessageWindowOutgoingScrollView setAccessibilityChild:] to manage the accessibility |
| 363 | * heirarchy. |
| 364 | */ |
| 365 | [controllerView_messages setFrame:[scrollView_messages documentVisibleRect]]; |
| 366 | [scrollView_messages setAccessibilityChild:controllerView_messages]; |
| 367 | [[customView_messages superview] replaceSubview:customView_messages with:controllerView_messages]; |
| 368 | |
| 369 | //This is what draws our transparent background |
| 370 | //Technically, it could be set in MessageView.nib, too |
| 371 | [scrollView_messages setBackgroundColor:[NSColor clearColor]]; |
| 372 | |
| 373 | [controllerView_messages setNextResponder:textView_outgoing]; |
| 374 | } |
| 375 | |
| 376 | /*! |
| 377 | * @brief The message display controller |
| 378 | */ |
| 379 | - (NSObject<AIMessageDisplayController> *)messageDisplayController |
| 380 | { |
| 381 | return messageDisplayController; |
| 382 | } |
| 383 | |
| 384 | /*! |
| 385 | * @brief Access to our view |
| 386 | */ |
| 387 | - (NSView *)view |
| 388 | { |
| 389 | return view_contents; |
| 390 | } |
| 391 | |
| 392 | /*! |
| 393 | * @brief Support for printing. Forward the print command to our message display view |
| 394 | */ |
| 395 | - (void)adiumPrint:(id)sender |
| 396 | { |
| 397 | if ([messageDisplayController respondsToSelector:@selector(adiumPrint:)]) { |
| 398 | [messageDisplayController adiumPrint:sender]; |
| 399 | } |
| 400 | } |
| 401 | |
| 402 | |
| 403 | //Messaging ------------------------------------------------------------------------------------------------------------ |
| 404 | #pragma mark Messaging |
| 405 | /*! |
| 406 | * @brief Send the entered message |
| 407 | */ |
| 408 | - (IBActionvoid)sendMessage:(id)sender |
| 409 | { |
| 410 | NSAttributedString *attributedString = [textView_outgoing textStorage]; |
| 411 | |
| 412 | //Only send if we have a non-zero-length string |
[1] Taking true branch. | |
| 413 | if ([attributedString length] != 0) { |
| 414 | AIListObject *listObject = [chat listObject]; |
| 415 | |
| 416 | //If user typed command /clear, reset the content of the view |
[2] Taking false branch. | |
| 417 | if ([[attributedString string] caseInsensitiveCompare:AILocalizedString[ [ NSBundle bundleForClass : [ self class ] ] localizedStringForKey : ( @ "/clear" ) value : @ "" table : ( 0 ) ](@"/clear", "Command which will clear the message area of a chat. Please include the '/' at the front of your translation.")] == NSOrderedSame) { |
| 418 | //Reset the content of the view |
| 419 | [messageDisplayController clearView]; |
| 420 | |
| 421 | //Reset the content of the text field, removing the command as it has been executed |
| 422 | [self clearTextEntryView]; |
| 423 | |
| 424 | //Commands are not messages, so they don't have to be sent |
| 425 | return; |
| 426 | } |
| 427 | |
[3] Taking false branch. | |
| 428 | if ([chat isGroupChat] && ![[chat account] online]) { |
| 429 | //Refuse to do anything with a group chat for an offline account. |
| 430 | NSBeep(); |
| 431 | return; |
| 432 | } |
| 433 | |
| 434 | AIChatSendingAbilityType messageSendingAbility = [chat messageSendingAbility]; |
[4] Taking false branch. | |
| 435 | if (suppressSendLaterPrompt || (messageSendingAbility == AIChatCanSendMessageNow) || |
| 436 | ((messageSendingAbility == AIChatCanSendViaServersideOfflineMessage) && [[chat account] sendOfflineMessagesWithoutPrompting])) { |
| 437 | AIContentMessage *message; |
| 438 | NSAttributedString *outgoingAttributedString; |
| 439 | AIAccount *account = [chat account]; |
| 440 | //Send the message |
| 441 | [[adium notificationCenter] postNotificationName:Interface_WillSendEnteredMessage@ "Interface_WillSendEnteredMessage" |
| 442 | object:chat |
| 443 | userInfo:nil0]; |
| 444 | |
| 445 | outgoingAttributedString = [attributedString copy]; |
| 446 | message = [AIContentMessage messageInChat:chat |
| 447 | withSource:account |
| 448 | destination:[chat listObject] |
| 449 | date:nil0 //created for us by AIContentMessage |
| 450 | message:outgoingAttributedString |
| 451 | autoreply:NO( BOOL ) 0]; |
| 452 | [outgoingAttributedString release]; |
| 453 | |
| 454 | if ([[adium contentController] sendContentObject:message]) { |
| 455 | [[adium notificationCenter] postNotificationName:Interface_DidSendEnteredMessage@ "Interface_DidSendEnteredMessage" |
| 456 | object:chat |
| 457 | userInfo:nil0]; |
| 458 | } |
| 459 | |
| 460 | } else { |
| 461 | NSString *formattedUID = [listObject formattedUID]; |
| 462 | |
| 463 | NSAlert *alert = [[NSAlert alloc] init]; |
[5] '?' condition evaluates to true. | |
| 464 | NSImage *icon = ([listObject userIcon] ? [listObject userIcon] : [AIServiceIcons serviceIconForObject:listObject |
| 465 | type:AIServiceIconLarge |
| 466 | direction:AIIconNormal]); |
| 467 | icon = [[icon copy] autorelease]; |
| 468 | [icon setScalesWhenResized:NO( BOOL ) 0]; |
| 469 | [alert setIcon:icon]; |
| 470 | [alert setAlertStyle:NSInformationalAlertStyle]; |
| 471 | |
| 472 | [alert setMessageText:[NSString stringWithFormat:AILocalizedString[ [ NSBundle bundleForClass : [ self class ] ] localizedStringForKey : ( @ "%@ appears to be offline. How do you want to send this message?" ) value : @ "" table : ( 0 ) ](@"%@ appears to be offline. How do you want to send this message?", nil), |
| 473 | formattedUID]]; |
| 474 | |
[6] 'Default' branch taken.Execution continues on line 520. | |
| 475 | switch (messageSendingAbility) { |
| 476 | case AIChatCanSendViaServersideOfflineMessage: |
| 477 | { |
| 478 | [alert setInformativeText:[NSString stringWithFormat: |
| 479 | AILocalizedString[ [ NSBundle bundleForClass : [ self class ] ] localizedStringForKey : ( @ "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." ) value : @ "" table : ( 0 ) ](@"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."), |
| 480 | formattedUID, formattedUID]]; |
| 481 | [alert addButtonWithTitle:AILocalizedString[ [ NSBundle bundleForClass : [ self class ] ] localizedStringForKey : ( @ "Send Now" ) value : @ "" table : ( 0 ) ](@"Send Now", nil)]; |
| 482 | |
| 483 | [alert addButtonWithTitle:AILocalizedString[ [ NSBundle bundleForClass : [ self class ] ] localizedStringForKey : ( @ "Send When Both Online" ) value : @ "" table : ( 0 ) ](@"Send When Both Online", nil)]; |
| 484 | [[[alert buttons] objectAtIndex:1] setKeyEquivalent:@"b"]; |
| 485 | [[[alert buttons] objectAtIndex:1] setKeyEquivalentModifierMask:0]; |
| 486 | |
| 487 | break; |
| 488 | } |
| 489 | case AIChatMayNotBeAbleToSendMessage: |
| 490 | { |
| 491 | [alert setInformativeText:[NSString stringWithFormat: |
| 492 | AILocalizedString[ [ NSBundle bundleForClass : [ self class ] ] localizedStringForKey : ( @ "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." ) value : @ "" table : ( 0 ) ](@"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"), |
| 493 | formattedUID, formattedUID, formattedUID]]; |
| 494 | [alert addButtonWithTitle:AILocalizedString[ [ NSBundle bundleForClass : [ self class ] ] localizedStringForKey : ( @ "Send Now" ) value : @ "" table : ( 0 ) ](@"Send Now", nil)]; |
| 495 | |
| 496 | [alert addButtonWithTitle:AILocalizedString[ [ NSBundle bundleForClass : [ self class ] ] localizedStringForKey : ( @ "Send Later" ) value : @ "" table : ( 0 ) ](@"Send Later", nil)]; |
| 497 | [[[alert buttons] objectAtIndex:1] setKeyEquivalent:@"l"]; |
| 498 | [[[alert buttons] objectAtIndex:1] setKeyEquivalentModifierMask:0]; |
| 499 | |
| 500 | break; |
| 501 | } |
| 502 | case AIChatCanNotSendMessage: |
| 503 | { |
| 504 | [alert setInformativeText:[NSString stringWithFormat: |
| 505 | AILocalizedString[ [ NSBundle bundleForClass : [ self class ] ] localizedStringForKey : ( @ "Send Later will send the message the next time both you and %@ are online." ) value : @ "" table : ( 0 ) ](@"Send Later will send the message the next time both you and %@ are online.", "Send Later dialogue explanation text"), |
| 506 | formattedUID, formattedUID, formattedUID]]; |
| 507 | [alert addButtonWithTitle:AILocalizedString[ [ NSBundle bundleForClass : [ self class ] ] localizedStringForKey : ( @ "Send Later" ) value : @ "" table : ( 0 ) ](@"Send Later", nil)]; |
| 508 | [[[alert buttons] objectAtIndex:0] setKeyEquivalent:@"l"]; |
| 509 | [[[alert buttons] objectAtIndex:0] setKeyEquivalentModifierMask:0]; |
| 510 | |
| 511 | break; |
| 512 | } |
| 513 | case AIChatCanSendMessageNow: |
| 514 | { |
| 515 | //We will never get here. |
| 516 | break; |
| 517 | } |
| 518 | } |
| 519 | |
| 520 | [alert addButtonWithTitle:AILocalizedString[ [ NSBundle bundleForClass : [ self class ] ] localizedStringForKey : ( @ "Don't Send" ) value : @ "" table : ( 0 ) ](@"Don't Send", nil)]; |
| 521 | |
[7] '?' condition evaluates to false. | |
| 522 | NSButton *dontSendButton = ((messageSendingAbility == AIChatCanNotSendMessage) ? |
| 523 | [[alert buttons] objectAtIndex:1] : |
| 524 | [[alert buttons] objectAtIndex:2]); |
| 525 | [dontSendButton setKeyEquivalent:@"\E"]; |
| 526 | [dontSendButton setKeyEquivalentModifierMask:0]; |
| 527 | |
| 528 | [alert beginSheetModalForWindow:[view_contents window] |
[9] Object allocated on line 531 is no longer referenced after this point and has a retain count of +1 (object leaked). | |
| 529 | modalDelegate:[self retain] /* Will release after the sheet ends */ |
| 530 | didEndSelector:@selector(alertDidEnd:returnCode:contextInfo:) |
[8] Method returns an object with a +1 retain count (owning reference). | |
| 531 | contextInfo:[[NSNumber alloc] initWithInt:messageSendingAbility] /* Will release after the sheet ends */]; |
| 532 | [alert release]; |
| 533 | } |
| 534 | } |
| 535 | } |
| 536 | |
| 537 | /*! |
| 538 | * @brief Send Later button was pressed |
| 539 | */ |
| 540 | - (void)alertDidEnd:(NSAlert *)alert returnCode:(int)returnCode contextInfo:(void *)contextInfo |
| 541 | { |
| 542 | AIChatSendingAbilityType messageSendingAbility = [(NSNumber *)contextInfo intValue]; |
| 543 | |
| 544 | switch (returnCode) { |
| 545 | case NSAlertFirstButtonReturn: |
| 546 | /* The AIChatCanNotSendMessage dalogue has Send Later as the first choice; |
| 547 | * all others have Send Now as the first choice. |
| 548 | */ |
| 549 | if (messageSendingAbility == AIChatCanNotSendMessage) { |
| 550 | /* Send Later */ |
| 551 | [self sendMessageLater:nil0]; |
| 552 | |
| 553 | } else { |
| 554 | /* Send Now */ |
| 555 | suppressSendLaterPrompt = YES( BOOL ) 1; |
| 556 | [self sendMessage:nil0]; |
| 557 | } |
| 558 | break; |
| 559 | |
| 560 | case NSAlertSecondButtonReturn: |
| 561 | /* The AIChatCanNotSendMessage dalogue has Cancel as the second choice; |
| 562 | * all others have Send Later as the first choice. |
| 563 | */ |
| 564 | if (messageSendingAbility != AIChatCanNotSendMessage) { |
| 565 | /* Send Later */ |
| 566 | [self sendMessageLater:nil0]; |
| 567 | } |
| 568 | break; |
| 569 | |
| 570 | case NSAlertThirdButtonReturn: /* Don't Send */ |
| 571 | break; |
| 572 | } |
| 573 | |
| 574 | //Retained when the alert was created to guard against a crash if the chat tab being closed while we are open |
| 575 | [self release]; |
| 576 | [(NSNumber *)contextInfo release]; |
| 577 | } |
| 578 | |
| 579 | /*! |
| 580 | * @brief Invoked after our entered message sends |
| 581 | * |
| 582 | * This method hides the account selection view and clears the entered message after our message sends |
| 583 | */ |
| 584 | - (IBActionvoid)didSendMessage:(id)sender |
| 585 | { |
| 586 | [self setAccountSelectionMenuVisibleIfNeeded:NO( BOOL ) 0]; |
| 587 | [self clearTextEntryView]; |
| 588 | } |
| 589 | |
| 590 | /*! |
| 591 | * @brief Offline messaging |
| 592 | */ |
| 593 | - (IBActionvoid)sendMessageLater:(id)sender |
| 594 | { |
| 595 | AIListContact *listContact; |
| 596 | |
| 597 | //If the chat can _now_ send a message, send it immediately instead of waiting for "later". |
| 598 | if ([chat messageSendingAbility] == AIChatCanSendMessageNow) { |
| 599 | [self sendMessage:sender]; |
| 600 | return; |
| 601 | } |
| 602 | |
| 603 | //Put the alert on the metaContact containing this listContact if applicable |
| 604 | listContact = [[chat listObject] parentContact]; |
| 605 | |
| 606 | if (listContact) { |
| 607 | NSMutableDictionary *detailsDict, *alertDict; |
| 608 | |
| 609 | detailsDict = [NSMutableDictionary dictionary]; |
| 610 | [detailsDict setObject:[[chat account] internalObjectID] forKey:@"Account ID"]; |
| 611 | [detailsDict setObject:[NSNumber numberWithBool:YES( BOOL ) 1] forKey:@"Allow Other"]; |
| 612 | [detailsDict setObject:[listContact internalObjectID] forKey:@"Destination ID"]; |
| 613 | |
| 614 | alertDict = [NSMutableDictionary dictionary]; |
| 615 | [alertDict setObject:detailsDict forKey:@"ActionDetails"]; |
| 616 | [alertDict setObject:CONTACT_SEEN_ONLINE_YES@ "Contact_SeenOnlineYes" forKey:@"EventID"]; |
| 617 | [alertDict setObject:@"SendMessage" forKey:@"ActionID"]; |
| 618 | [alertDict setObject:[NSNumber numberWithBool:YES( BOOL ) 1] forKey:@"OneTime"]; |
| 619 | |
| 620 | [alertDict setObject:listContact forKey:@"TEMP-ListContact"]; |
| 621 | |
| 622 | [[adium contentController] filterAttributedString:[[[textView_outgoing textStorage] copy] autorelease] |
| 623 | usingFilterType:AIFilterContent |
| 624 | direction:AIFilterOutgoing |
| 625 | filterContext:listContact |
| 626 | notifyingTarget:self |
| 627 | selector:@selector(gotFilteredMessageToSendLater:receivingContext:) |
| 628 | context:alertDict]; |
| 629 | |
| 630 | [self didSendMessage:nil0]; |
| 631 | } |
| 632 | } |
| 633 | |
| 634 | /*! |
| 635 | * @brief Offline messaging |
| 636 | */ |
| 637 | //XXX - Offline messaging code SHOULD NOT BE IN HERE! -ai |
| 638 | - (void)gotFilteredMessageToSendLater:(NSAttributedString *)filteredMessage receivingContext:(NSMutableDictionary *)alertDict |
| 639 | { |
| 640 | NSMutableDictionary *detailsDict; |
| 641 | AIListContact *listContact; |
| 642 | |
| 643 | detailsDict = [alertDict objectForKey:@"ActionDetails"]; |
| 644 | [detailsDict setObject:[filteredMessage dataRepresentation] forKey:@"Message"]; |
| 645 | |
| 646 | listContact = [[alertDict objectForKey:@"TEMP-ListContact"] retain]; |
| 647 | [alertDict removeObjectForKey:@"TEMP-ListContact"]; |
| 648 | |
| 649 | [[adium contactAlertsController] addAlert:alertDict |
| 650 | toListObject:listContact |
| 651 | setAsNewDefaults:NO( BOOL ) 0]; |
| 652 | [listContact release]; |
| 653 | } |
| 654 | |
| 655 | //Account Selection ---------------------------------------------------------------------------------------------------- |
| 656 | #pragma mark Account Selection |
| 657 | /*! |
| 658 | * @brief |
| 659 | */ |
| 660 | - (void)accountSelectionViewFrameDidChange:(NSNotification *)notification |
| 661 | { |
| 662 | [self updateFramesForAccountSelectionView]; |
| 663 | } |
| 664 | |
| 665 | /*! |
| 666 | * @brief Redisplay the source/destination account selector |
| 667 | */ |
| 668 | - (void)redisplaySourceAndDestinationSelector:(NSNotification *)notification |
| 669 | { |
| 670 | [self setAccountSelectionMenuVisibleIfNeeded:YES( BOOL ) 1]; |
| 671 | } |
| 672 | |
| 673 | /*! |
| 674 | * @brief Toggle visibility of the account selection menus |
| 675 | * |
| 676 | * Invoking this method with NO will hide the account selection menus. Invoking it with YES will show the account |
| 677 | * selection menus if they are needed. |
| 678 | */ |
| 679 | - (void)setAccountSelectionMenuVisibleIfNeeded:(BOOL)makeVisible |
| 680 | { |
| 681 | //Hide or show the account selection view as requested |
| 682 | if (makeVisible) { |
| 683 | [self _createAccountSelectionView]; |
| 684 | } else { |
| 685 | [self _destroyAccountSelectionView]; |
| 686 | } |
| 687 | } |
| 688 | |
| 689 | /*! |
| 690 | * @brief Show the account selection view |
| 691 | */ |
| 692 | - (void)_createAccountSelectionView |
| 693 | { |
| 694 | if (!view_accountSelection) { |
| 695 | NSRect contentFrame = [splitView_textEntryHorizontal frame]; |
| 696 | |
| 697 | //Create the account selection view and insert it into our window |
| 698 | view_accountSelection = [[AIAccountSelectionView alloc] initWithFrame:contentFrame]; |
| 699 | |
| 700 | [view_accountSelection setAutoresizingMask:(NSViewWidthSizable | NSViewMinYMargin)]; |
| 701 | |
| 702 | [self updateGradientColors]; |
| 703 | |
| 704 | //Insert the account selection view at the top of our view |
| 705 | [[shelfView contentView] addSubview:view_accountSelection]; |
| 706 | [view_accountSelection setChat:chat]; |
| 707 | |
| 708 | [[NSNotificationCenter defaultCenter] addObserver:self |
| 709 | selector:@selector(accountSelectionViewFrameDidChange:) |
| 710 | name:AIViewFrameDidChangeNotification@ "AIViewFrameDidChangeNotification" |
| 711 | object:view_accountSelection]; |
| 712 | |
| 713 | [self updateFramesForAccountSelectionView]; |
| 714 | |
| 715 | //Redisplay everything |
| 716 | [[shelfView contentView] setNeedsDisplay:YES( BOOL ) 1]; |
| 717 | } else { |
| 718 | [view_accountSelection setChat:chat]; |
| 719 | } |
| 720 | } |
| 721 | |
| 722 | /*! |
| 723 | * @brief Hide the account selection view |
| 724 | */ |
| 725 | - (void)_destroyAccountSelectionView |
| 726 | { |
| 727 | if (view_accountSelection) { |
| 728 | //Remove the observer |
| 729 | [[NSNotificationCenter defaultCenter] removeObserver:self |
| 730 | name:AIViewFrameDidChangeNotification@ "AIViewFrameDidChangeNotification" |
| 731 | object:view_accountSelection]; |
| 732 | |
| 733 | //Remove the account selection view from our window, clean it up |
| 734 | [view_accountSelection removeFromSuperview]; |
| 735 | [view_accountSelection release]; view_accountSelection = nil0; |
| 736 | |
| 737 | //Redisplay everything |
| 738 | [self updateFramesForAccountSelectionView]; |
| 739 | } |
| 740 | } |
| 741 | |
| 742 | /*! |
| 743 | * @brief Position the account selection view, if it is present, and the messages/text entry splitview appropriately |
| 744 | */ |
| 745 | - (void)updateFramesForAccountSelectionView |
| 746 | { |
| 747 | int accountSelectionHeight = (view_accountSelection ? [view_accountSelection frame].size.height : 0); |
| 748 | |
| 749 | if (view_accountSelection) { |
| 750 | [view_accountSelection setFrameOrigin:NSMakePoint(NSMinX([view_accountSelection frame]), NSHeight([[view_accountSelection superview] frame]) - accountSelectionHeight)]; |
| 751 | [view_accountSelection setNeedsDisplay:YES( BOOL ) 1]; |
| 752 | } |
| 753 | |
| 754 | NSRect splitView_textEntryHorizontalFrame = [splitView_textEntryHorizontal frame]; |
| 755 | splitView_textEntryHorizontalFrame.size.height = NSHeight([[splitView_textEntryHorizontal superview] frame]) - accountSelectionHeight - NSMinY(splitView_textEntryHorizontalFrame); |
| 756 | [splitView_textEntryHorizontal setFrame:splitView_textEntryHorizontalFrame]; |
| 757 | |
| 758 | [splitView_textEntryHorizontal setNeedsDisplay:YES( BOOL ) 1]; |
| 759 | } |
| 760 | |
| 761 | |
| 762 | //Text Entry ----------------------------------------------------------------------------------------------------------- |
| 763 | #pragma mark Text Entry |
| 764 | /*! |
| 765 | * @brief Preferences changed, update sending keys |
| 766 | */ |
| 767 | - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key object:(AIListObject *)object |
| 768 | preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime |
| 769 | { |
| 770 | [textView_outgoing setSendOnReturn:[[prefDict objectForKey:SEND_ON_RETURN@ "Send On Return"] boolValue]]; |
| 771 | [textView_outgoing setSendOnEnter:[[prefDict objectForKey:SEND_ON_ENTER@ "Send On Enter"] boolValue]]; |
| 772 | } |
| 773 | |
| 774 | /*! |
| 775 | * @brief Configure the text entry view |
| 776 | */ |
| 777 | - (void)_configureTextEntryView |
| 778 | { |
| 779 | //Configure the text entry view |
| 780 | [textView_outgoing setTarget:self action:@selector(sendMessage:)]; |
| 781 | |
| 782 | //This is necessary for tab completion. |
| 783 | [textView_outgoing setDelegate:self]; |
| 784 | |
| 785 | [textView_outgoing setTextContainerInset:NSMakeSize(0,2)]; |
| 786 | if ([textView_outgoing respondsToSelector:@selector(setUsesFindPanel:)]) { |
| 787 | [textView_outgoing setUsesFindPanel:YES( BOOL ) 1]; |
| 788 | } |
| 789 | [textView_outgoing setClearOnEscape:YES( BOOL ) 1]; |
| 790 | [textView_outgoing setTypingAttributes:[[adium contentController] defaultFormattingAttributes]]; |
| 791 | |
| 792 | //User's choice of mininum height for their text entry view |
| 793 | entryMinHeight = [[[adium preferenceController] preferenceForKey:KEY_ENTRY_TEXTVIEW_MIN_HEIGHT@ "Minimum Text Height" |
| 794 | group:PREF_GROUP_DUAL_WINDOW_INTERFACE@ "Dual Window Interface"] intValue]; |
| 795 | if (entryMinHeight <= 0) entryMinHeight = [self _textEntryViewProperHeightIgnoringUserMininum:YES( BOOL ) 1]; |
| 796 | |
| 797 | //Associate the view with our message view so it knows which view to scroll in response to page up/down |
| 798 | //and other special key-presses. |
| 799 | [textView_outgoing setAssociatedView:[messageDisplayController messageScrollView]]; |
| 800 | |
| 801 | //Associate the text entry view with our chat and inform Adium that it exists. |
| 802 | //This is necessary for text entry filters to work correctly. |
| 803 | [textView_outgoing setChat:chat]; |
| 804 | |
| 805 | //Observe text entry view size changes so we can dynamically resize as the user enters text |
| 806 | [[NSNotificationCenter defaultCenter] addObserver:self |
| 807 | selector:@selector(outgoingTextViewDesiredSizeDidChange:) |
| 808 | name:AIViewDesiredSizeDidChangeNotification@ "AIViewDesiredSizeDidChangeNotification" |
| 809 | object:textView_outgoing]; |
| 810 | |
| 811 | [self _updateTextEntryViewHeight]; |
| 812 | } |
| 813 | |
| 814 | /*! |
| 815 | * @brief Sets our text entry view as the first responder |
| 816 | */ |
| 817 | - (void)makeTextEntryViewFirstResponder |
| 818 | { |
| 819 | [[textView_outgoing window] makeFirstResponder:textView_outgoing]; |
| 820 | } |
| 821 | |
| 822 | /*! |
| 823 | * @brief Clear the message entry text view |
| 824 | */ |
| 825 | - (void)clearTextEntryView |
| 826 | { |
| 827 | NSWritingDirection writingDirection; |
| 828 | |
| 829 | writingDirection = [textView_outgoing baseWritingDirection]; |
| 830 | |
| 831 | [textView_outgoing setString:@""]; |
| 832 | [textView_outgoing setTypingAttributes:[[adium contentController] defaultFormattingAttributes]]; |
| 833 | |
| 834 | [textView_outgoing setBaseWritingDirection:writingDirection]; //Preserve the writing diraction |
| 835 | |
| 836 | [[NSNotificationCenter defaultCenter] postNotificationName:NSTextDidChangeNotification |
| 837 | object:textView_outgoing]; |
| 838 | } |
| 839 | |
| 840 | /*! |
| 841 | * @brief Add text to the message entry text view |
| 842 | * |
| 843 | * Adds the passed string to the entry text view at the insertion point. If there is selected text in the view, it |
| 844 | * will be replaced. |
| 845 | */ |
| 846 | - (void)addToTextEntryView:(NSAttributedString *)inString |
| 847 | { |
| 848 | [textView_outgoing insertText:inString]; |
| 849 | [[NSNotificationCenter defaultCenter] postNotificationName:NSTextDidChangeNotification object:textView_outgoing]; |
| 850 | } |
| 851 | |
| 852 | /*! |
| 853 | * @brief Add data to the message entry text view |
| 854 | * |
| 855 | * Adds the passed pasteboard data to the entry text view at the insertion point. If there is selected text in the |
| 856 | * view, it will be replaced. |
| 857 | */ |
| 858 | - (void)addDraggedDataToTextEntryView:(id <NSDraggingInfo>)draggingInfo |
| 859 | { |
| 860 | [textView_outgoing performDragOperation:draggingInfo]; |
| 861 | [[NSNotificationCenter defaultCenter] postNotificationName:NSTextDidChangeNotification object:textView_outgoing]; |
| 862 | } |
| 863 | |
| 864 | /*! |
| 865 | * @brief Update the text entry view's height when its desired size changes |
| 866 | */ |
| 867 | - (void)outgoingTextViewDesiredSizeDidChange:(NSNotification *)notification |
| 868 | { |
| 869 | [self _updateTextEntryViewHeight]; |
| 870 | } |
| 871 | |
| 872 | - (void)tabViewDidChangeVisibility |
| 873 | { |
| 874 | [self _updateTextEntryViewHeight]; |
| 875 | } |
| 876 | |
| 877 | /* |
| 878 | * @brief Update the height of our text entry view |
| 879 | * |
| 880 | * This method sets the height of the text entry view to the most ideal value, and adjusts the other views in our |
| 881 | * window to fill the remaining space. |
| 882 | */ |
| 883 | - (void)_updateTextEntryViewHeight |
| 884 | { |
| 885 | int height = [self _textEntryViewProperHeightIgnoringUserMininum:NO( BOOL ) 0]; |
| 886 | //Display the vertical scroller if our view is not tall enough to display all the entered text |
| 887 | [scrollView_outgoing setHasVerticalScroller:(height < [textView_outgoing desiredSize].height)]; |
| 888 | |
| 889 | //First, set the text entry subview to the exact height we want |
| 890 | [[splitView_textEntryHorizontal subviewAtPosition:1] setMinDimension:height andMaxDimension:height]; |
| 891 | [splitView_textEntryHorizontal adjustSubviews]; |
| 892 | |
| 893 | //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 |
| 894 | [[splitView_textEntryHorizontal subviewAtPosition:1] setMinDimension:[self _textEntryViewProperHeightIgnoringUserMininum:YES( BOOL ) 1] andMaxDimension:([splitView_textEntryHorizontal frame].size.height * MESSAGE_VIEW_MIN_HEIGHT_RATIO.50)]; |
| 895 | } |
| 896 | |
| 897 | /*! |
| 898 | * @brief Returns the height our text entry view should be |
| 899 | * |
| 900 | * This method takes into account user preference, the amount of entered text, and the current window size to return |
| 901 | * a height which is most ideal for the text entry view. |
| 902 | * |
| 903 | * @param ignoreUserMininum If YES, the user's preference for mininum height will be ignored |
| 904 | */ |
| 905 | - (int)_textEntryViewProperHeightIgnoringUserMininum:(BOOL)ignoreUserMininum |
| 906 | { |
| 907 | int dividerThickness = [splitView_textEntryHorizontal dividerThickness]; |
| 908 | int allowedHeight = ([splitView_textEntryHorizontal frame].size.height / 2.0) - dividerThickness; |
| 909 | int height; |
| 910 | |
| 911 | //Our primary goal is to display all the entered text |
| 912 | height = [textView_outgoing desiredSize].height; |
| 913 | |
| 914 | //But we must never fall below the user's prefered mininum or above the allowed height |
| 915 | if (!ignoreUserMininum && height < entryMinHeight) { |
| 916 | height = entryMinHeight; |
| 917 | } |
| 918 | if (height > allowedHeight) height = allowedHeight; |
| 919 | |
| 920 | return height; |
| 921 | } |
| 922 | |
| 923 | #pragma mark Autocompletion |
| 924 | /*! |
| 925 | * @brief Should the tab key cause an autocompletion if possible? |
| 926 | * |
| 927 | * We only tab to autocomplete for a group chat |
| 928 | */ |
| 929 | - (BOOL)textViewShouldTabComplete:(NSTextView *)inTextView |
| 930 | { |
| 931 | return [[self chat] isGroupChat]; |
| 932 | } |
| 933 | |
| 934 | - (NSArray *)textView:(NSTextView *)textView completions:(NSArray *)words forPartialWordRange:(NSRange)charRange indexOfSelectedItem:(int *)index |
| 935 | { |
| 936 | NSMutableArray *completions; |
| 937 | |
| 938 | if ([[self chat] isGroupChat]) { |
| 939 | NSString *partialWord = [[[textView textStorage] attributedSubstringFromRange:charRange] string]; |
| 940 | NSEnumerator *enumerator; |
| 941 | AIListContact *listContact; |
| 942 | |
| 943 | NSString *suffix; |
| 944 | if (charRange.location == 0) { |
| 945 | //At the start of a line, append ": " |
| 946 |