Bug Summary

File:Plugins/Dual Window Interface/AIMessageViewController.m
Location:line 528, column 4
Description:Memory Leak
Code is compiled without garbage collection.

Annotated Source Code

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 suffix = @": ";
947 } else