Frameworks/Adium Framework/Source/AIMessageEntryTextView.m
author Zachary West <zacw@adium.im>
Sun Oct 25 21:21:42 2009 -0400 (2009-10-25)
changeset 2776 862f4e3f05e1
parent 2644 e7b5093aa5c1
child 3066 d3613a0dcc6c
permissions -rw-r--r--
Control more directly the completion range for group chats. Fixes #13237.

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

This fixes autocompleting things like "[mynick]" or the channel name (such as "#adium") since they start with non-word characters, but are perfectly valid.
     1 /* 
     2  * Adium is the legal property of its developers, whose names are listed in the copyright file included
     3  * with this source distribution.
     4  * 
     5  * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
     6  * General Public License as published by the Free Software Foundation; either version 2 of the License,
     7  * or (at your option) any later version.
     8  * 
     9  * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
    10  * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
    11  * Public License for more details.
    12  * 
    13  * You should have received a copy of the GNU General Public License along with this program; if not,
    14  * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
    15  */
    16 
    17 #import <Adium/AIChat.h>
    18 #import <Adium/AIAccount.h>
    19 #import <Adium/AIListObject.h>
    20 #import <Adium/AIListContact.h>
    21 #import <Adium/AIMessageEntryTextView.h>
    22 #import <Adium/ESFileWrapperExtension.h>
    23 #import <Adium/AITextAttachmentExtension.h>
    24 
    25 #import <Adium/AIMenuControllerProtocol.h>
    26 #import <Adium/AIContentControllerProtocol.h>
    27 #import <Adium/AIInterfaceControllerProtocol.h>
    28 #import <Adium/AIContentContext.h>
    29 
    30 #import <AIUtilities/AIApplicationAdditions.h>
    31 #import <AIUtilities/AIAttributedStringAdditions.h>
    32 #import <AIUtilities/AIColorAdditions.h>
    33 #import <AIUtilities/AITextAttributes.h>
    34 #import <AIUtilities/AIImageAdditions.h>
    35 #import <AIUtilities/AIFileManagerAdditions.h>
    36 #import <AIUtilities/AIPasteboardAdditions.h>
    37 #import <AIUtilities/AIBezierPathAdditions.h>
    38 #import <Adium/AIContactControllerProtocol.h>
    39 
    40 #import <FriBidi/NSString-FBAdditions.h>
    41 
    42 #import <AIUtilities/AILeopardCompatibility.h>
    43 
    44 #define MAX_HISTORY					25		//Number of messages to remember in history
    45 #define ENTRY_TEXTVIEW_PADDING		6		//Padding for auto-sizing
    46 
    47 #define KEY_DISABLE_TYPING_NOTIFICATIONS		@"Disable Typing Notifications"
    48 
    49 #define KEY_SPELL_CHECKING						@"Spell Checking Enabled"
    50 #define KEY_GRAMMAR_CHECKING					@"Grammar Checking Enabled"
    51 #define	PREF_GROUP_DUAL_WINDOW_INTERFACE		@"Dual Window Interface"
    52 
    53 #define KEY_SUBSTITUTION_DASH					@"Smart Dash Substitutions"
    54 #define KEY_SUBSTITUTION_DATA_DETECTORS			@"Smart Data Detectors Substitutions"
    55 #define KEY_SUBSTITUTION_REPLACEMENT			@"Text Replacement Substitutions"
    56 #define KEY_SUBSTITUTION_SPELLING				@"Spelling Substitutions"
    57 #define KEY_SUBSTITUTION_COPY_PASTE				@"Smart Copy Paste Substitutions"
    58 #define KEY_SUBSTITUTION_QUOTE					@"Smart Quote Substitutions"
    59 #define KEY_SUBSTITUTION_LINK					@"Smart Links Substitutions"
    60 
    61 #define INDICATOR_RIGHT_PADDING					2		// Padding between right side of the message view and the rightmost indicator
    62 
    63 #define PREF_GROUP_CHARACTER_COUNTER			@"Character Counter"
    64 #define KEY_CHARACTER_COUNTER_ENABLED			@"Character Counter Enabled"
    65 #define KEY_MAX_NUMBER_OF_CHARACTERS			@"Maximum Number Of Characters"
    66 
    67 #define FILES_AND_IMAGES_TYPES [NSArray arrayWithObjects: \
    68 	NSFilenamesPboardType, AIiTunesTrackPboardType, NSTIFFPboardType, NSPDFPboardType, NSPICTPboardType, nil]
    69 
    70 #define PASS_TO_SUPERCLASS_DRAG_TYPE_ARRAY [NSArray arrayWithObjects: \
    71 	NSRTFPboardType, NSStringPboardType, nil]
    72 
    73 /**
    74  * @class AISimpleTextView
    75  * @brief Just draws an attributed string. That's it.
    76  * 
    77  * No really, it's dead simple. It just draws an attributed string in its bounds (which you set). That's it.
    78  * It's totally not even useful.
    79  */
    80 
    81 @implementation  AISimpleTextView
    82 
    83 @synthesize string;
    84 - (void)dealloc
    85 {
    86 	[string release];
    87 	[super dealloc];
    88 }
    89 
    90 - (void)drawRect:(NSRect)rect 
    91 {
    92 	[string drawInRect:self.bounds];
    93 }
    94 @end
    95 
    96 @interface AIMessageEntryTextView ()
    97 - (void)_setPushIndicatorVisible:(BOOL)visible;
    98 - (void)positionPushIndicator;
    99 - (void)_resetCacheAndPostSizeChanged;
   100 
   101 - (NSAttributedString *)attributedStringWithAITextAttachmentExtensionsFromRTFDData:(NSData *)data;
   102 - (NSAttributedString *)attributedStringWithTextAttachmentExtension:(AITextAttachmentExtension *)attachment;
   103 - (void)addAttachmentOfPath:(NSString *)inPath;
   104 - (void)addAttachmentOfImage:(NSImage *)inImage;
   105 - (void)addAttachmentsFromPasteboard:(NSPasteboard *)pasteboard;
   106 
   107 - (void)setCharacterCounterVisible:(BOOL)visible;
   108 - (void)setCharacterCounterMaximum:(int)inMaxCharacters;
   109 - (void)setCharacterCounterPrefix:(NSString *)prefix;
   110 - (void)updateCharacterCounter;
   111 - (void)positionCharacterCounter;
   112 
   113 - (void)positionIndicators:(NSNotification *)notification;
   114 @end
   115 
   116 @interface NSMutableAttributedString (AIMessageEntryTextViewAdditions)
   117 - (void)convertForPasteWithTraitsUsingAttributes:(NSDictionary *)inAttributes;
   118 @end
   119 
   120 @implementation AIMessageEntryTextView
   121 
   122 - (void)_initMessageEntryTextView
   123 {
   124 	associatedView = nil;
   125 	chat = nil;
   126 	pushIndicator = nil;
   127 	pushPopEnabled = YES;
   128 	historyEnabled = YES;
   129 	clearOnEscape = NO;
   130 	homeToStartOfLine = YES;
   131 	resizing = NO;
   132 	enableTypingNotifications = NO;
   133 	historyArray = [[NSMutableArray alloc] initWithObjects:@"",nil];
   134 	pushArray = [[NSMutableArray alloc] init];
   135 	currentHistoryLocation = 0;
   136 	[self setDrawsBackground:YES];
   137 	_desiredSizeCached = NSMakeSize(0,0);
   138 	characterCounter = nil;
   139 	characterCounterPrefix = nil;
   140 	maxCharacters = 0;
   141 	savedTextColor = nil;
   142 	
   143 	if ([self respondsToSelector:@selector(setAllowsUndo:)]) {
   144 		[self setAllowsUndo:YES];
   145 	}
   146 	if ([self respondsToSelector:@selector(setAllowsDocumentBackgroundColorChange:)]) {
   147 		[self setAllowsDocumentBackgroundColorChange:YES];
   148 	}
   149 	
   150 	[self setImportsGraphics:YES];
   151 	
   152 	[[NSNotificationCenter defaultCenter] addObserver:self 
   153 											 selector:@selector(textDidChange:)
   154 												 name:NSTextDidChangeNotification 
   155 											   object:self];
   156 	[[NSNotificationCenter defaultCenter] addObserver:self
   157 											 selector:@selector(frameDidChange:) 
   158 												 name:NSViewFrameDidChangeNotification 
   159 											   object:self];
   160 	[[NSNotificationCenter defaultCenter] addObserver:self
   161 															selector:@selector(toggleMessageSending:)
   162 																name:@"AIChatDidChangeCanSendMessagesNotification"
   163 															  object:chat];
   164 	[[NSNotificationCenter defaultCenter] addObserver:self 
   165 															selector:@selector(contentObjectAdded:) 
   166 																name:Content_ContentObjectAdded 
   167 															  object:nil];
   168 
   169 	[adium.preferenceController registerPreferenceObserver:self forGroup:PREF_GROUP_DUAL_WINDOW_INTERFACE];	
   170 	
   171 	[[AIContactObserverManager sharedManager] registerListObjectObserver:self];
   172 }
   173 
   174 //Init the text view
   175 - (id)initWithFrame:(NSRect)frameRect textContainer:(NSTextContainer *)aTextContainer
   176 {
   177 	if ((self = [super initWithFrame:frameRect textContainer:aTextContainer])) {
   178 		[self _initMessageEntryTextView];
   179 	}
   180 	
   181     return self;
   182 }
   183 
   184 - (id)initWithCoder:(NSCoder *)coder
   185 {
   186 	if ((self = [super initWithCoder:coder])) {
   187 		[self _initMessageEntryTextView];
   188 	}
   189 	
   190 	return self;
   191 }
   192 
   193 - (void)dealloc
   194 {
   195 	if(chat.isGroupChat) {
   196 		[chat removeObserver:self forKeyPath:@"Character Counter Max"];
   197 		[chat removeObserver:self forKeyPath:@"Character Counter Prefix"];
   198 	}
   199 	
   200 	[[NSNotificationCenter defaultCenter] removeObserver:self];
   201 	[adium.preferenceController unregisterPreferenceObserver:self];
   202 	[[AIContactObserverManager sharedManager] unregisterListObjectObserver:self];
   203 
   204 	[savedTextColor release];
   205 	[characterCounter release];
   206 	[characterCounterPrefix release];
   207     [chat release];
   208     [associatedView release];
   209     [historyArray release]; historyArray = nil;
   210     [pushArray release]; pushArray = nil;
   211 
   212     [super dealloc];
   213 }
   214 
   215 - (void) setDelegate:(id<AIMessageEntryTextViewDelegate>)del
   216 {
   217 	super.delegate = del;
   218 }
   219 
   220 - (id<AIMessageEntryTextViewDelegate>)delegate
   221 {
   222 	return super.delegate;
   223 }
   224 
   225 - (void)keyDown:(NSEvent *)inEvent
   226 {
   227 	NSString *charactersIgnoringModifiers = [inEvent charactersIgnoringModifiers];
   228 
   229 	if ([charactersIgnoringModifiers length]) {
   230 		unichar		 inChar = [charactersIgnoringModifiers characterAtIndex:0];
   231 		unsigned int flags = [inEvent modifierFlags];
   232 		
   233 		//We have to test ctrl before option, because otherwise we'd miss ctrl-option-* events
   234 		if (pushPopEnabled &&
   235 			(flags & NSControlKeyMask) && !(flags & NSShiftKeyMask)) {
   236 			if (inChar == NSUpArrowFunctionKey) {
   237 				[self popContent];
   238 			} else if (inChar == NSDownArrowFunctionKey) {
   239 				[self pushContent];
   240 			} else if (inChar == 's') {
   241 				[self swapContent];
   242 			} else {
   243 				[super keyDown:inEvent];
   244 			}
   245 			
   246 		} else if (historyEnabled && 
   247 				   (flags & NSAlternateKeyMask) && !(flags & NSShiftKeyMask)) {
   248 			if (inChar == NSUpArrowFunctionKey) {
   249 				[self historyUp];
   250 			} else if (inChar == NSDownArrowFunctionKey) {
   251 				[self historyDown];
   252 			} else {
   253 				[super keyDown:inEvent];
   254 			}
   255 			
   256 		} else if (associatedView &&
   257 				   (flags & NSCommandKeyMask) && !(flags & NSShiftKeyMask)) {
   258 			if ((inChar == NSUpArrowFunctionKey || inChar == NSDownArrowFunctionKey) ||
   259 			   (inChar == NSHomeFunctionKey || inChar == NSEndFunctionKey) ||
   260 			   (inChar == NSPageUpFunctionKey || inChar == NSPageDownFunctionKey)) {
   261 				//Pass the associatedView a keyDown event equivalent equal to inEvent except without the modifier flags
   262 				[associatedView keyDown:[NSEvent keyEventWithType:[inEvent type]
   263 														 location:[inEvent locationInWindow]
   264 													modifierFlags:0
   265 														timestamp:[inEvent timestamp]
   266 													 windowNumber:[inEvent windowNumber]
   267 														  context:[inEvent context]
   268 													   characters:[inEvent characters]
   269 									  charactersIgnoringModifiers:charactersIgnoringModifiers
   270 														isARepeat:[inEvent isARepeat]
   271 														  keyCode:[inEvent keyCode]]];
   272 			} else {
   273 				[super keyDown:inEvent];
   274 			}
   275 			
   276 		} else if (associatedView &&
   277 				   (inChar == NSPageUpFunctionKey || inChar == NSPageDownFunctionKey)) {
   278 			[associatedView keyDown:inEvent];
   279 			
   280 		} else if (inChar == NSHomeFunctionKey || inChar == NSEndFunctionKey) {
   281 			if (homeToStartOfLine) {
   282 				NSRange	newRange;
   283 				
   284 				if (flags & NSShiftKeyMask) {
   285 					//With shift, select to the beginning/end of the line
   286 					NSRange	selectedRange = [self selectedRange];
   287 					if (inChar == NSHomeFunctionKey) {
   288 						//Home: from 0 to the current location
   289 						newRange.location = 0;
   290 						newRange.length = selectedRange.location;
   291 					} else {
   292 						//End: from current location to the end
   293 						newRange.location = selectedRange.location;
   294 						newRange.length = [[self string] length] - newRange.location;
   295 					}
   296 					
   297 				} else {
   298 					newRange.location = ((inChar == NSHomeFunctionKey) ? 0 : [[self string] length]);
   299 					newRange.length = 0;
   300 				}
   301 
   302 				[self setSelectedRange:newRange];
   303 
   304 			} else {
   305 				//If !homeToStartOfLine, pass the keypress to our associated view.
   306 				if (associatedView) {
   307 					[associatedView keyDown:inEvent];
   308 				} else {
   309 					[super keyDown:inEvent];					
   310 				}
   311 			}
   312 
   313 		} else if (inChar == NSTabCharacter) {
   314 			if ([self.delegate respondsToSelector:@selector(textViewShouldTabComplete:)] &&
   315 				[self.delegate textViewShouldTabComplete:self]) {
   316 				[self complete:nil];
   317 			} else {
   318 				[super keyDown:inEvent];				
   319 			} 
   320 
   321 		} else {
   322 			[super keyDown:inEvent];
   323 		}
   324 	} else {
   325 		[super keyDown:inEvent];
   326 	}
   327 }
   328 
   329 //Text changed
   330 - (void)textDidChange:(NSNotification *)notification
   331 {
   332 	//Update typing status
   333 	if (enableTypingNotifications) {
   334 		[adium.contentController userIsTypingContentForChat:chat hasEnteredText:[[self textStorage] length] > 0];
   335 	}
   336 
   337 	//Hide any existing contact list tooltip when we begin typing
   338 	[adium.interfaceController showTooltipForListObject:nil atScreenPoint:NSZeroPoint onWindow:nil];
   339 
   340     //Reset cache and resize
   341 	[self _resetCacheAndPostSizeChanged]; 
   342 	
   343 	//Update the character counter
   344 	if (characterCounter) {
   345 		[self updateCharacterCounter];
   346 	}
   347 }
   348 
   349 /*!
   350  * @brief Clear any link attribute in the current typing attributes
   351  *
   352  * Any link attribute is removed. All other typing attributes are unchanged.
   353  */
   354 - (void)clearLinkAttribute
   355 {
   356 	NSDictionary *typingAttributes = [self typingAttributes];
   357 
   358 	if ([typingAttributes objectForKey:NSLinkAttributeName]) {
   359 		NSMutableDictionary *newTypingAttributes = [typingAttributes mutableCopy];
   360 
   361 		[newTypingAttributes removeObjectForKey:NSLinkAttributeName];
   362 		[self setTypingAttributes:newTypingAttributes];
   363 
   364 		[newTypingAttributes release];
   365 	}
   366 }
   367 
   368 /*!
   369  * @brief The user pressed escape: clear our text view in response
   370  */
   371 - (void)cancelOperation:(id)sender
   372 {
   373 	if (clearOnEscape) {
   374 		NSUndoManager	*undoManager = [self undoManager];
   375 		[undoManager registerUndoWithTarget:self
   376 								   selector:@selector(setAttributedString:)
   377 									 object:[[[self textStorage] copy] autorelease]];
   378 		[undoManager setActionName:AILocalizedString(@"Clear", nil)];
   379 
   380 		[self setString:@""];
   381 		[self clearLinkAttribute];		
   382 	}
   383 
   384 	if ([self.delegate respondsToSelector:@selector(textViewDidCancel:)]) {
   385 		[self.delegate textViewDidCancel:self];
   386 	}
   387 }
   388 
   389 
   390 //Configure ------------------------------------------------------------------------------------------------------------
   391 #pragma mark Configure
   392 @synthesize clearOnEscape, homeToStartOfLine, associatedView;
   393 
   394 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key
   395 							object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
   396 {
   397 	if ((!object || (object == chat.account)) &&
   398 		[group isEqualToString:GROUP_ACCOUNT_STATUS] &&
   399 		(!key || [key isEqualToString:KEY_DISABLE_TYPING_NOTIFICATIONS])) {
   400 		enableTypingNotifications = ![[chat.account preferenceForKey:KEY_DISABLE_TYPING_NOTIFICATIONS
   401 																 group:GROUP_ACCOUNT_STATUS] boolValue];
   402 	}
   403 	
   404 	if (!object && [group isEqualToString:PREF_GROUP_DUAL_WINDOW_INTERFACE]) {
   405 		if (!key || [key isEqualToString:KEY_GRAMMAR_CHECKING]) {
   406 			[self setGrammarCheckingEnabled:[[prefDict objectForKey:KEY_GRAMMAR_CHECKING] boolValue]];
   407 		}
   408 
   409 		if (!key || [key isEqualToString:KEY_SPELL_CHECKING]) {
   410 			[self setContinuousSpellCheckingEnabled:[[prefDict objectForKey:KEY_SPELL_CHECKING] boolValue]];
   411 		}
   412 		
   413 		if ([NSApp isOnSnowLeopardOrBetter]) {
   414 			if (!key || [key isEqualToString:KEY_SUBSTITUTION_DASH]) {
   415 				[self setAutomaticDashSubstitutionEnabled:[[prefDict objectForKey:KEY_SUBSTITUTION_DASH] boolValue]];
   416 			}
   417 		
   418 			if (!key || [key isEqualToString:KEY_SUBSTITUTION_DATA_DETECTORS]) {
   419 				[self setAutomaticDataDetectionEnabled:[[prefDict objectForKey:KEY_SUBSTITUTION_DATA_DETECTORS] boolValue]];
   420 			}
   421 		
   422 			if (!key || [key isEqualToString:KEY_SUBSTITUTION_REPLACEMENT]) {
   423 				[self setAutomaticTextReplacementEnabled:[[prefDict objectForKey:KEY_SUBSTITUTION_REPLACEMENT] boolValue]];
   424 			}
   425 		
   426 			if (!key || [key isEqualToString:KEY_SUBSTITUTION_SPELLING]) {
   427 				[self setAutomaticSpellingCorrectionEnabled:[[prefDict objectForKey:KEY_SUBSTITUTION_SPELLING] boolValue]];
   428 			}
   429 		}
   430 		
   431 		if (!key || [key isEqualToString:KEY_SUBSTITUTION_COPY_PASTE]) {
   432 			[self setSmartInsertDeleteEnabled:[[prefDict objectForKey:KEY_SUBSTITUTION_COPY_PASTE] boolValue]];
   433 		}
   434 		
   435 		if (!key || [key isEqualToString:KEY_SUBSTITUTION_QUOTE]) {
   436 			[self setAutomaticQuoteSubstitutionEnabled:[[prefDict objectForKey:KEY_SUBSTITUTION_QUOTE] boolValue]];
   437 		}
   438 		
   439 		if (!key || [key isEqualToString:KEY_SUBSTITUTION_LINK]) {
   440 			[self setAutomaticLinkDetectionEnabled:[[prefDict objectForKey:KEY_SUBSTITUTION_LINK] boolValue]];
   441 		}
   442 	}
   443 }
   444 
   445 //Adium Text Entry -----------------------------------------------------------------------------------------------------
   446 #pragma mark Adium Text Entry
   447 
   448 /*!
   449  * @brief Toggle whether message sending is enabled based on a notification. The notification object is the AIChat of the appropriate message entry view
   450  */
   451 - (void)toggleMessageSending:(NSNotification *)not
   452 {
   453 	//XXX - We really should query the AIChat about this, but AIChat's "can't send" is really designed for handling offline, not banned. Bringing up the offline messaging dialog when banned would make no sense.
   454 	[self setSendingEnabled:[[[not userInfo] objectForKey:@"TypingEnabled"] boolValue]];
   455 }
   456 
   457 /*!
   458  * @brief Are we available for sending?
   459  */
   460 - (BOOL)availableForSending
   461 {
   462 	return self.sendingEnabled;
   463 }
   464 
   465 //Set our string, preserving the selected range
   466 - (void)setAttributedString:(NSAttributedString *)inAttributedString
   467 {
   468     int			length = [inAttributedString length];
   469     NSRange 	oldRange = [self selectedRange];
   470 
   471     //Change our string
   472     [[self textStorage] setAttributedString:inAttributedString];
   473 
   474     //Restore the old selected range
   475     if (oldRange.location < length) {
   476         if (oldRange.location + oldRange.length <= length) {
   477             [self setSelectedRange:oldRange];
   478         } else {
   479             [self setSelectedRange:NSMakeRange(oldRange.location, length - oldRange.location)];       
   480         }
   481     }
   482 
   483     //Notify everyone that our text changed
   484     [[NSNotificationCenter defaultCenter] postNotificationName:NSTextDidChangeNotification object:self];
   485 }
   486 
   487 //Set our string (plain text)
   488 - (void)setString:(NSString *)string
   489 {
   490     [super setString:string];
   491 
   492     //Notify everyone that our text changed
   493     [[NSNotificationCenter defaultCenter] postNotificationName:NSTextDidChangeNotification object:self];
   494 }
   495 
   496 //Set our typing format
   497 - (void)setTypingAttributes:(NSDictionary *)attrs
   498 {
   499 	[super setTypingAttributes:attrs];
   500 
   501 	[self setInsertionPointColor:[[attrs objectForKey:NSBackgroundColorAttributeName] contrastingColor]];
   502 }
   503 
   504 #pragma mark Pasting
   505 
   506 - (BOOL)handlePasteAsRichText
   507 {
   508 	NSPasteboard *generalPasteboard = [NSPasteboard generalPasteboard];
   509 	BOOL		 handledPaste = NO;
   510 	
   511 	//Types is ordered by the preference for handling of the data; enumerating it lets us allow the sending application's hints to be followed.
   512 	for (NSString *type in generalPasteboard.types) {
   513 		if ([type isEqualToString:NSRTFDPboardType]) {
   514 			NSData *data = [generalPasteboard dataForType:NSRTFDPboardType];
   515 			[self insertText:[self attributedStringWithAITextAttachmentExtensionsFromRTFDData:data]];
   516 			handledPaste = YES;
   517 			
   518 		} else if ([PASS_TO_SUPERCLASS_DRAG_TYPE_ARRAY containsObject:type]) {
   519 			//When we hit a type we should let the superclass handle, break without doing anything
   520 			break;
   521 			
   522 		} else if ([FILES_AND_IMAGES_TYPES containsObject:type]) {
   523 			[self addAttachmentsFromPasteboard:generalPasteboard];
   524 			handledPaste = YES;
   525 		}
   526 		
   527 		if (handledPaste) break;
   528 		
   529 	}
   530 	
   531 	return handledPaste;
   532 }
   533 
   534 //Paste as rich text without altering our typing attributes
   535 - (void)pasteAsRichText:(id)sender
   536 {
   537 	NSDictionary	*attributes = [[self typingAttributes] copy];
   538 
   539 	if (![self handlePasteAsRichText]) {
   540 		[self paste:sender];
   541 	}
   542 
   543 	if (attributes) {
   544 		[self setTypingAttributes:attributes];
   545 	}
   546 
   547 	[attributes release];
   548 	
   549 	[self scrollRangeToVisible:[self selectedRange]];
   550 }
   551 
   552 - (void)pasteAsPlainTextWithTraits:(id)sender
   553 {
   554 	NSDictionary	*attributes = [[self typingAttributes] copy];
   555 	
   556 	NSPasteboard	*generalPasteboard = [NSPasteboard generalPasteboard];
   557 	NSString		*type;
   558 
   559 	NSArray *supportedTypes =
   560 		[NSArray arrayWithObjects:NSURLPboardType, NSRTFDPboardType, NSRTFPboardType, NSHTMLPboardType, NSStringPboardType, 
   561 			NSFilenamesPboardType, NSTIFFPboardType, NSPDFPboardType, NSPICTPboardType, nil];
   562 
   563 	type = [[NSPasteboard generalPasteboard] availableTypeFromArray:supportedTypes];
   564 	
   565 	if ([type isEqualToString:NSRTFPboardType] ||
   566 		[type isEqualToString:NSRTFDPboardType] ||
   567 		[type isEqualToString:NSHTMLPboardType] ||
   568 		[type isEqualToString:NSStringPboardType]) {
   569 		NSData *data;
   570 		
   571 		@try {
   572 			data = [generalPasteboard dataForType:type];
   573 		} @catch (NSException *localException) {
   574 			data = nil;
   575 		}
   576 		
   577 		//Failed. Try again with the string type.
   578 		if (!data && ![type isEqualToString:NSStringPboardType]) {
   579 			if ([[[NSPasteboard generalPasteboard] types] containsObject:NSStringPboardType]) {
   580 				type = NSStringPboardType;
   581 				@try {
   582 					data = [generalPasteboard dataForType:type];
   583 				} @catch (NSException *localException) {
   584 					data = nil;
   585 				}
   586 			}
   587 		}
   588 		
   589 		if (!data) {
   590 			//We still didn't get valid data... maybe super can handle it
   591 			@try {
   592 				[self paste:sender];
   593 			} @catch (NSException *localException) {
   594 				NSBeep();
   595 				return;
   596 			}
   597 		}
   598 		
   599 		NSMutableAttributedString *attributedString;
   600 		
   601 		if ([type isEqualToString:NSStringPboardType]) {
   602 			NSString *string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
   603 			attributedString = [[NSMutableAttributedString alloc] initWithString:string
   604 																	  attributes:[self typingAttributes]];
   605 			[string release];
   606 			
   607 		} else {
   608 			@try {
   609 				if ([type isEqualToString:NSRTFPboardType]) {
   610 					attributedString = [[NSMutableAttributedString alloc] initWithRTF:data
   611 																   documentAttributes:NULL];
   612 				} else if ([type isEqualToString:NSRTFDPboardType]) {
   613 					attributedString = [[NSMutableAttributedString alloc] initWithRTFD:data
   614 																	documentAttributes:NULL];
   615 				} else /* NSHTMLPboardType */ {
   616 					attributedString = [[NSMutableAttributedString alloc] initWithHTML:data
   617 																	documentAttributes:NULL];
   618 				}
   619 			} @catch (NSException *localException) {
   620 				//Error while reading the RTF or HTML data, which can happen. Fall back on plain text
   621 				if ([[[NSPasteboard generalPasteboard] types] containsObject:NSStringPboardType]) {
   622 					data = [generalPasteboard dataForType:NSStringPboardType];
   623 					NSString *string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
   624 					attributedString = [[NSMutableAttributedString alloc] initWithString:string
   625 																			  attributes:[self typingAttributes]];
   626 					[string release];
   627 				} else {
   628 					attributedString = nil;
   629 				}
   630 			}
   631 
   632 			if (!attributedString) {
   633 				NSBeep();
   634 				return;
   635 			}
   636 
   637 			[attributedString convertForPasteWithTraitsUsingAttributes:[self typingAttributes]];
   638 		}
   639 		
   640 		NSRange			selectedRange = [self selectedRange];
   641 		NSTextStorage	*textStorage = [self textStorage];
   642 		
   643 		//Prepare the undo operation
   644 		NSUndoManager	*undoManager = [self undoManager];
   645 		[[undoManager prepareWithInvocationTarget:textStorage]
   646 				replaceCharactersInRange:NSMakeRange(selectedRange.location, [attributedString length])
   647 					withAttributedString:[textStorage attributedSubstringFromRange:selectedRange]];
   648 		[undoManager setActionName:AILocalizedString(@"Paste", nil)];
   649 		
   650 		//Perform the paste
   651 		[textStorage replaceCharactersInRange:selectedRange
   652 						 withAttributedString:attributedString];
   653 		// Align our text properly (only need to if the first character was changed)
   654 		if (selectedRange.location == 0)
   655 			[self setBaseWritingDirection:[[textStorage string] baseWritingDirection]];
   656 		//Notify that we changed our text
   657 		[[NSNotificationCenter defaultCenter] postNotificationName:NSTextDidChangeNotification
   658 															object:self];
   659 		[attributedString release];
   660 
   661 	} else if ([FILES_AND_IMAGES_TYPES containsObject:type] ||
   662 			   [type isEqualToString:NSURLPboardType]) {
   663 		if (![self handlePasteAsRichText]) {
   664 			[self paste:sender];
   665 		}
   666 
   667 	} else {		
   668 		//If we didn't handle it yet, let super try to deal with it
   669 		[self paste:sender];
   670 	}
   671 
   672 	if (attributes) {
   673 		[self setTypingAttributes:attributes];
   674 	}
   675 
   676 	[attributes release];	
   677 	
   678 	[self scrollRangeToVisible:[self selectedRange]];
   679 }
   680 
   681 #pragma mark Deletion
   682 
   683 - (void)deleteBackward:(id)sender
   684 {
   685 	//Perform the delete
   686 	[super deleteBackward:sender];
   687 	
   688 	//If we are now an empty string, and we still have a link active, clear the link
   689 	if ([[self textStorage] length] == 0) {
   690 		[self clearLinkAttribute];
   691 	}
   692 }
   693 
   694 //Contact menu ---------------------------------------------------------------------------------------------------------
   695 #pragma mark Contact menu
   696 //Set and return the selected chat (to auto-configure the contact menu)
   697 - (void)setChat:(AIChat *)inChat
   698 {
   699     if (chat != inChat) {
   700 		if(chat.isGroupChat) {
   701 			[chat removeObserver:self forKeyPath:@"Character Counter Max"];
   702 			[chat removeObserver:self forKeyPath:@"Character Counter Prefix"];
   703 		}
   704 		
   705         [chat release];
   706         chat = [inChat retain];	
   707 		
   708 		// We only need to update our observation state for group chats.
   709 		if(chat.isGroupChat) {
   710 			[chat addObserver:self
   711 				   forKeyPath:@"Character Counter Max"
   712 					  options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld | NSKeyValueObservingOptionInitial)
   713 					  context:NULL];
   714 			
   715 			[chat addObserver:self
   716 				   forKeyPath:@"Character Counter Prefix"
   717 					  options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld | NSKeyValueObservingOptionInitial)
   718 					  context:NULL];
   719 		}
   720 		
   721 		//Observe preferences changes for typing enable/disable
   722 		[adium.preferenceController registerPreferenceObserver:self forGroup:GROUP_ACCOUNT_STATUS];
   723     }
   724 	
   725 	//Set up the character counter for this chat's list object.
   726 	//This is done regardless of a chat changing because destination changes need to trigger this.
   727 	if(!chat.isGroupChat) {
   728 		[self setCharacterCounterMaximum:[chat.listObject integerValueForProperty:@"Character Counter Max"]];
   729 		[self setCharacterCounterVisible:([chat.listObject valueForProperty:@"Character Counter Max"] != nil)];
   730 		[self setCharacterCounterPrefix:[chat.listObject valueForProperty:@"Character Counter Prefix"]];
   731 		
   732 		[self updateCharacterCounter];
   733 	}
   734 }
   735 - (AIChat *)chat{
   736     return chat;
   737 }
   738 
   739 //Return the selected list object (to auto-configure the contact menu)
   740 - (AIListContact *)listObject
   741 {
   742 	return chat.listObject;
   743 }
   744 
   745 - (AIListContact *)preferredListObject
   746 {
   747 	return [chat preferredListObject];
   748 }
   749 
   750 //Auto Sizing ----------------------------------------------------------------------------------------------------------
   751 #pragma mark Auto-sizing
   752 //Returns our desired size
   753 - (NSSize)desiredSize
   754 {
   755     if (_desiredSizeCached.width == 0) {
   756         float 		textHeight;
   757         if ([[self textStorage] length] != 0) {
   758             //If there is text in this view, let the container tell us its height
   759 
   760 			//Force glyph generation.  We must do this or usedRectForTextContainer might only return a rect for a
   761 			//portion of our text.
   762             [[self layoutManager] glyphRangeForTextContainer:[self textContainer]];            
   763 
   764             textHeight = [[self layoutManager] usedRectForTextContainer:[self textContainer]].size.height;
   765         } else {
   766             //Otherwise, we use the current typing attributes to guess what the height of a line should be
   767 			textHeight = [NSAttributedString stringHeightForAttributes:[self typingAttributes]];
   768         }
   769 
   770 		/* When we called glyphRangeForTextContainer, we may have triggered re-entry via
   771 		 *		-[self setFrame:] --> -[self frameDidChange:] --> -[self _resetCacheAndPostSizeChanged]
   772 		 * in which case the second entry through the loop (the future relative to our conversation in this comment) got the correct desired size.
   773 		 * In the present, an *old* value is in textHeight.  We don't want to use that. Jumping gigawatts!
   774 		 */
   775 		if (_desiredSizeCached.width == 0) {
   776 			_desiredSizeCached = NSMakeSize([self frame].size.width, textHeight + ENTRY_TEXTVIEW_PADDING);
   777 		}
   778     }
   779 
   780     return _desiredSizeCached;
   781 }
   782 
   783 //Reset the desired size cache when our frame changes
   784 - (void)frameDidChange:(NSNotification *)notification
   785 {
   786 	//resetCacheAndPostSizeChanged can get us right back to here, resulting in an infinite loop if we're not careful
   787 	if (!resizing) {
   788 		resizing = YES;
   789 		[self _resetCacheAndPostSizeChanged];
   790 		resizing = NO;
   791 	}
   792 }
   793 
   794 //Reset the desired size cache and post a size changed notification.  Call after the text's dimensions change
   795 - (void)_resetCacheAndPostSizeChanged
   796 {
   797 	//Reset the size cache
   798 	_desiredSizeCached = NSMakeSize(0,0);
   799 
   800 	//Post notification if size changed
   801 	if (!NSEqualSizes([self desiredSize], lastPostedSize)) {
   802 		lastPostedSize = [self desiredSize];
   803 		[[NSNotificationCenter defaultCenter] postNotificationName:AIViewDesiredSizeDidChangeNotification object:self];
   804 	}
   805 }
   806 
   807 //Paging ---------------------------------------------------------------------------------------------------------------
   808 #pragma mark Paging
   809 //Page up or down in the message view
   810 - (void)scrollPageUp:(id)sender
   811 {
   812     if (associatedView && [associatedView respondsToSelector:@selector(pageUp:)]) {
   813 		[associatedView pageUp:nil];
   814     } else {
   815 		[super scrollPageUp:sender];
   816 	}
   817 }
   818 - (void)scrollPageDown:(id)sender
   819 {
   820     if (associatedView && [associatedView respondsToSelector:@selector(pageDown:)]) {
   821 		[associatedView pageDown:nil];
   822     } else {
   823 		[super scrollPageDown:sender];
   824 	}
   825 }
   826 
   827 
   828 //History --------------------------------------------------------------------------------------------------------------
   829 #pragma mark History
   830 @synthesize historyEnabled;
   831 
   832 //Move up through the history
   833 - (void)historyUp
   834 {
   835     if (currentHistoryLocation == 0) {
   836 		//Store current message
   837         [historyArray replaceObjectAtIndex:0 withObject:[[[self textStorage] copy] autorelease]];
   838     }
   839 	
   840     if (currentHistoryLocation < [historyArray count]-1) {
   841         //Move up
   842         currentHistoryLocation++;
   843 		
   844         //Display history
   845         [self setAttributedString:[historyArray objectAtIndex:currentHistoryLocation]];
   846     }
   847 }
   848 
   849 //Move down through history
   850 - (void)historyDown
   851 {
   852     if (currentHistoryLocation > 0) {
   853         //Move down
   854         currentHistoryLocation--;
   855 		
   856         //Display history
   857         [self setAttributedString:[historyArray objectAtIndex:currentHistoryLocation]];
   858 	}
   859 }
   860 
   861 //Update history when content is sent
   862 - (IBAction)sendContent:(id)sender
   863 {
   864 	NSAttributedString	*textStorage = [self textStorage];
   865 	
   866 	//Add to history if there is text being sent
   867 	[historyArray insertObject:[[textStorage copy] autorelease] atIndex:1];
   868 	if ([historyArray count] > MAX_HISTORY) {
   869 		[historyArray removeLastObject];
   870 	}
   871 
   872 	currentHistoryLocation = 0; //Move back to bottom of history
   873 
   874 	//Send the content
   875 	[super sendContent:sender];
   876 	
   877 	//Clear the undo/redo stack as it makes no sense to carry between sends (the history is for that)
   878 	[[self undoManager] removeAllActions];
   879 }
   880 
   881 //Populate the history with messages from the message history
   882 - (void)contentObjectAdded:(NSNotification *)notification
   883 {
   884 	AIContentObject *content = [notification.userInfo objectForKey:@"AIContentObject"];
   885 
   886 	if (self.chat == content.chat && ([content.type isEqualToString:CONTENT_CONTEXT_TYPE]) && content.isOutgoing) {
   887 		//Populate the history with messages from us
   888 		[historyArray insertObject:content.message atIndex:1];
   889 		if (historyArray.count > MAX_HISTORY) {
   890 			[historyArray removeLastObject];
   891 		}
   892 	}
   893 }
   894 
   895 //Push and Pop ---------------------------------------------------------------------------------------------------------
   896 #pragma mark Push and Pop
   897 //Enable/Disable push-pop
   898 - (void)setPushPopEnabled:(BOOL)inBool
   899 {
   900 	pushPopEnabled = inBool;
   901 }
   902 
   903 //Push out of the message entry field
   904 - (void)pushContent
   905 {
   906 	if ([[self textStorage] length] != 0 && pushPopEnabled) {
   907 		[pushArray addObject:[[[self textStorage] copy] autorelease]];
   908 		[self setString:@""];
   909 		[self _setPushIndicatorVisible:YES];
   910 	}
   911 }
   912 
   913 //Pop into the message entry field
   914 - (void)popContent
   915 {
   916     if ([pushArray count] && pushPopEnabled) {
   917         [self setAttributedString:[pushArray lastObject]];
   918         [self setSelectedRange:NSMakeRange([[self textStorage] length], 0)]; //selection to end
   919         [pushArray removeLastObject];
   920         if ([pushArray count] == 0) {
   921             [self _setPushIndicatorVisible:NO];
   922         }
   923     }
   924 }
   925 
   926 //Swap current content
   927 - (void)swapContent
   928 {
   929 	if (pushPopEnabled) {
   930 		NSAttributedString *tempMessage = [[[self textStorage] copy] autorelease];
   931 				
   932 		if ([pushArray count]) {
   933 			[self popContent];
   934 		} else {
   935 			[self setString:@""];
   936 		}
   937 		
   938 		if (tempMessage && [tempMessage length] != 0) {
   939 			[pushArray addObject:tempMessage];
   940 			[self _setPushIndicatorVisible:YES];
   941 		}
   942 	}
   943 }
   944 
   945 //Push indicator
   946 - (void)_setPushIndicatorVisible:(BOOL)visible
   947 {
   948 	static NSImage	*pushIndicatorImage = nil;
   949 	
   950 	//
   951 	if (!pushIndicatorImage) pushIndicatorImage = [[NSImage imageNamed:@"stackImage" forClass:[self class]] retain];
   952 
   953     if (visible && !pushIndicatorVisible) {
   954         pushIndicatorVisible = visible;
   955 		
   956         //Push text over to make room for indicator
   957         NSSize size = [self frame].size;
   958         size.width -= ([pushIndicatorImage size].width);
   959         [self setFrameSize:size];
   960 				
   961 		// Make the indicator and set its action. It is a button with no border.
   962 		pushIndicator = [[NSButton alloc] initWithFrame:
   963             NSMakeRect(0, 0, [pushIndicatorImage size].width, [pushIndicatorImage size].height)]; 
   964 		[pushIndicator setButtonType:NSMomentaryPushButton];
   965         [pushIndicator setAutoresizingMask:(NSViewMinXMargin)];
   966         [pushIndicator setImage:pushIndicatorImage];
   967         [pushIndicator setImagePosition:NSImageOnly];
   968 		[pushIndicator setBezelStyle:NSRegularSquareBezelStyle];
   969 		[pushIndicator setBordered:NO];
   970         [[self superview] addSubview:pushIndicator];
   971 		[pushIndicator setTarget:self];
   972 		[pushIndicator setAction:@selector(popContent)];
   973 		
   974 		[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(positionIndicators:) name:NSViewBoundsDidChangeNotification object:[self superview]];
   975         [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(positionIndicators:) name:NSViewFrameDidChangeNotification object:[self superview]];
   976 		
   977         [self positionPushIndicator]; //Set the indicators initial position
   978 		
   979     } else if (!visible && pushIndicatorVisible) {
   980         pushIndicatorVisible = visible;
   981 
   982         //Push text back
   983         NSSize size = [self frame].size;
   984         size.width += [pushIndicatorImage size].width;
   985         [self setFrameSize:size];
   986 
   987 		//Unsubcribe, if necessary.
   988 		if (!characterCounter) {
   989 			[[NSNotificationCenter defaultCenter] removeObserver:self name:NSViewBoundsDidChangeNotification object:[self superview]];
   990 			[[NSNotificationCenter defaultCenter] removeObserver:self name:NSViewFrameDidChangeNotification object:[self superview]];
   991 		}
   992 		//Remove indicator
   993         [pushIndicator removeFromSuperview];
   994         [pushIndicator release]; pushIndicator = nil;
   995 		
   996 		[self positionPushIndicator];
   997     }
   998 }
   999 
  1000 //Reposition the push indicator into lower right corner
  1001 - (void)positionPushIndicator
  1002 {
  1003     NSRect visRect = [[self superview] bounds];
  1004     NSRect indFrame = [pushIndicator frame];
  1005 	float counterPadding = characterCounter ? NSWidth([characterCounter frame]) : 0;
  1006 	[pushIndicator setFrameOrigin:NSMakePoint(NSMaxX(visRect) - NSWidth(indFrame) - INDICATOR_RIGHT_PADDING - counterPadding, 
  1007 											  NSMidY([self frame]) - NSHeight(indFrame)/2)];
  1008     [[self enclosingScrollView] setNeedsDisplay:YES];
  1009 }
  1010 
  1011 #pragma mark Indicators Positioning
  1012 
  1013 /**
  1014  * @brief Dispatch for both indicators to observe bounds & frame changes of their superview
  1015  *
  1016  * Stupid that this is necessary, but you can only remove an entire object from a notification center's observer list,
  1017  * not on a per-method basis.
  1018  */
  1019 - (void)positionIndicators:(NSNotification *)notification
  1020 {
  1021 	if (pushIndicatorVisible)
  1022 		[self positionPushIndicator];
  1023 	if (characterCounter)
  1024 		[self positionCharacterCounter];
  1025 }
  1026 
  1027 #pragma mark Character Counter
  1028 
  1029 /**
  1030  * @brief Makes the character counter for this view visible.
  1031  */
  1032 - (void)setCharacterCounterVisible:(BOOL)visible
  1033 {
  1034 	if (visible && !characterCounter) {
  1035 		characterCounter = [[AISimpleTextView alloc] initWithFrame:NSZeroRect];
  1036 		[characterCounter setAutoresizingMask:(NSViewMinXMargin|NSViewWidthSizable)];
  1037 
  1038 		[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(positionIndicators:) name:NSViewBoundsDidChangeNotification object:[self superview]];
  1039         [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(positionIndicators:) name:NSViewFrameDidChangeNotification object:[self superview]];		
  1040 
  1041 		[self updateCharacterCounter];
  1042 		[[self superview] addSubview:characterCounter];
  1043 		
  1044 	} else if (!visible && characterCounter) {	
  1045 		[characterCounter removeFromSuperview];
  1046 		
  1047 		// Make sure to resize this view back to the right size.
  1048 		NSSize size = [self frame].size;
  1049         size.width += NSWidth([characterCounter frame]);
  1050         [self setFrameSize:size];
  1051 
  1052 		//Unsubscribe, if necessary.
  1053 		if (!pushIndicatorVisible) {
  1054 			[[NSNotificationCenter defaultCenter] removeObserver:self name:NSViewBoundsDidChangeNotification object:[self superview]];
  1055 			[[NSNotificationCenter defaultCenter] removeObserver:self name:NSViewFrameDidChangeNotification object:[self superview]];
  1056 		}
  1057 
  1058 		[characterCounter release];
  1059 		characterCounter = nil;
  1060 		
  1061 		// Reposition the push indicator, if necessary.
  1062 		if (pushIndicatorVisible)
  1063 			[self positionPushIndicator];
  1064 		
  1065 		[[self enclosingScrollView] setNeedsDisplay:YES];
  1066 	}
  1067 }
  1068 
  1069 /*!
  1070  * @brief Set the prefix for the character count.
  1071  */
  1072 - (void)setCharacterCounterPrefix:(NSString *)prefix
  1073 {
  1074 	if(prefix != characterCounterPrefix) {
  1075 		[characterCounterPrefix release];
  1076 		characterCounterPrefix = [prefix retain];
  1077 	}
  1078 }
  1079 
  1080 /**
  1081  * @brief Set the number of characters the character counter should count down from.
  1082  */
  1083 - (void)setCharacterCounterMaximum:(int)inMaxCharacters
  1084 {
  1085 	maxCharacters = inMaxCharacters;
  1086 	
  1087 	if (characterCounter)
  1088 		[self updateCharacterCounter];
  1089 }
  1090 
  1091 /**
  1092  * @brief Update the character counter and resize this view to make space if the counter's bounds change.
  1093  */
  1094 - (void)updateCharacterCounter
  1095 {
  1096 	NSRect visRect = [[self superview] bounds];
  1097 	
  1098 	NSString *inputString = [self.chat.account encodedAttributedString:[self textStorage] forListObject:self.chat.listObject];
  1099 	int currentCount = (maxCharacters - [inputString length]);
  1100 
  1101 	if(maxCharacters && currentCount < 0) {
  1102 		savedTextColor = [[self textColor] retain];
  1103 		
  1104 		[self setBackgroundColor:[NSColor colorWithCalibratedHue:0.983
  1105 													  saturation:0.43
  1106 													  brightness:0.99
  1107 														   alpha:1.0]];
  1108 		
  1109 		[self.enclosingScrollView setBackgroundColor:[NSColor colorWithCalibratedHue:0.983
  1110 																		  saturation:0.43
  1111 																		  brightness:0.99
  1112 																			   alpha:1.0]];
  1113 	} else {
  1114 		if (savedTextColor) {
  1115 			[self setTextColor:savedTextColor];
  1116 			savedTextColor = nil;
  1117 		}
  1118 		
  1119 		[self setBackgroundColor:[NSColor controlBackgroundColor]];
  1120 		[self.enclosingScrollView setBackgroundColor:[NSColor controlBackgroundColor]];
  1121 	}
  1122 	
  1123 	NSString *counterText = [NSString stringWithFormat:@"%d", currentCount];
  1124 	
  1125 	if (characterCounterPrefix) {
  1126 		counterText = [NSString stringWithFormat:@"%@%@", characterCounterPrefix, counterText];
  1127 	}
  1128 	
  1129 	NSAttributedString *label = [[NSAttributedString alloc] initWithString:counterText
  1130 																attributes:[adium.contentController defaultFormattingAttributes]];
  1131 	[characterCounter setString:label];
  1132 	[characterCounter setFrameSize:label.size];
  1133 	[label release];
  1134 
  1135 	//Reposition the character counter.
  1136 	[self positionCharacterCounter];
  1137 	
  1138 	//Shift the text entry view over as necessary.
  1139 	float indent = 0;
  1140 	if (pushIndicatorVisible || characterCounter) {
  1141 		float pushIndicatorX = pushIndicator ? NSMinX([pushIndicator frame]) : NSMaxX([self bounds]);
  1142 		float characterCounterX = characterCounter ? NSMinX([characterCounter frame]) : NSMaxX([self bounds]);
  1143 		indent = NSWidth(visRect) - fminf(pushIndicatorX, characterCounterX);
  1144 	}
  1145 	[self setFrameSize:NSMakeSize(NSWidth(visRect) - indent, NSHeight([self frame]))];
  1146 	
  1147 	//Reposition the push indicator if necessary.
  1148 	if (pushIndicatorVisible)
  1149 		[self positionPushIndicator];
  1150 		
  1151 	[[self enclosingScrollView] setNeedsDisplay:YES];
  1152 }
  1153 
  1154 /**
  1155  * @brief Keeps the character counter in the bottom right corner.
  1156  */
  1157 - (void)positionCharacterCounter
  1158 {
  1159 	NSRect visRect = [[self superview] bounds];
  1160 	NSSize counterSize = characterCounter.string.size;
  1161 	
  1162 	//NSMaxY([self frame]) is necessary because visRect's height changes after you start typing. No idea why.
  1163 	[characterCounter setFrameOrigin:NSMakePoint(NSMaxX(visRect) - counterSize.width - INDICATOR_RIGHT_PADDING,
  1164 												 NSMidY([self frame]) - (counterSize.height)/2)];
  1165 	[characterCounter setFrameSize:counterSize];
  1166 	[[self enclosingScrollView] setNeedsDisplay:YES];
  1167 }
  1168 
  1169 #pragma mark List Object Observer / Chat KVO
  1170 
  1171 - (NSSet *)updateListObject:(AIListObject *)inObject keys:(NSSet *)inModifiedKeys silent:(BOOL)silent
  1172 {
  1173 	if ((inObject == chat.listObject) &&
  1174 		(!inModifiedKeys || [inModifiedKeys containsObject:@"Character Counter Max"] || [inModifiedKeys containsObject:@"Character Counter Prefix"])) {
  1175 		[self setCharacterCounterMaximum:[inObject integerValueForProperty:@"Character Counter Max"]];
  1176 		[self setCharacterCounterVisible:([inObject valueForProperty:@"Character Counter Max"] != nil)];
  1177 		[self setCharacterCounterPrefix:[inObject valueForProperty:@"Character Counter Prefix"]];
  1178 		
  1179 		[self updateCharacterCounter];
  1180 	}
  1181 
  1182 	return nil;
  1183 }
  1184 
  1185 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
  1186 {
  1187 	if(object == chat && ([keyPath isEqualToString:@"Character Counter Max"] || [keyPath isEqualToString:@"Character Counter Prefix"])) {
  1188 		[self setCharacterCounterMaximum:[chat integerValueForProperty:@"Character Counter Max"]];
  1189 		[self setCharacterCounterVisible:([chat valueForProperty:@"Character Counter Max"] != nil)];
  1190 		[self setCharacterCounterPrefix:[chat valueForProperty:@"Character Counter Prefix"]];
  1191 		
  1192 		[self updateCharacterCounter];
  1193 	}
  1194 }
  1195 
  1196 #pragma mark Contextual Menus
  1197 
  1198 - (NSMenu *)menuForEvent:(NSEvent *)theEvent
  1199 {
  1200 	NSMenu			*contextualMenu = nil;
  1201 	
  1202 	NSArray			*itemsArray = nil;
  1203 	BOOL			addedOurLinkItems = NO;
  1204 
  1205 	if ((contextualMenu = [super menuForEvent:theEvent])) {
  1206 		contextualMenu = [[contextualMenu copy] autorelease];
  1207 
  1208 		NSMenuItem	*editLinkItem = nil;
  1209 		for (NSMenuItem *menuItem in contextualMenu.itemArray) {
  1210 			if ([[menuItem title] rangeOfString:AILocalizedString(@"Edit Link", nil)].location != NSNotFound) {
  1211 				editLinkItem = menuItem;
  1212 				break;
  1213 			}
  1214 		}
  1215 
  1216 		if (editLinkItem) {
  1217 			//There was an Edit Link item.  Remove it, and add out own link editing items in its place.
  1218 			int editIndex = [contextualMenu indexOfItem:editLinkItem];
  1219 			[contextualMenu removeItem:editLinkItem];
  1220 			
  1221 			NSMenu  *linkItemsMenu = [adium.menuController contextualMenuWithLocations:[NSArray arrayWithObject:
  1222 				[NSNumber numberWithInt:Context_TextView_LinkEditing]]];
  1223 			
  1224 			for (NSMenuItem *menuItem in linkItemsMenu.itemArray) {
  1225 				[contextualMenu insertItem:[[menuItem copy] autorelease] atIndex:editIndex++];
  1226 			}
  1227 			
  1228 			addedOurLinkItems = YES;
  1229 		}
  1230 	} else {
  1231 		contextualMenu = [[[NSMenu alloc] init] autorelease];
  1232 	}
  1233 
  1234 	//Retrieve the items which should be added to the bottom of the default menu
  1235 	NSArray	*locationArray = (addedOurLinkItems ?
  1236 							  [NSArray arrayWithObject:[NSNumber numberWithInt:Context_TextView_Edit]] :
  1237 							  [NSArray arrayWithObjects:[NSNumber numberWithInt:Context_TextView_LinkEditing], 
  1238 								  [NSNumber numberWithInt:Context_TextView_Edit], nil]);
  1239 	NSMenu  *adiumMenu = [adium.menuController contextualMenuWithLocations:locationArray];
  1240 	itemsArray = [adiumMenu itemArray];
  1241 	
  1242 	if ([itemsArray count] > 0) {
  1243 		[contextualMenu addItem:[NSMenuItem separatorItem]];
  1244 		int i = [(NSMenu *)contextualMenu numberOfItems];
  1245 		for (NSMenuItem *menuItem in itemsArray) {
  1246 			//We're going to be copying; call menu needs update now since it won't be called later.
  1247 			NSMenu	*submenu = [menuItem submenu];
  1248 			NSMenuItem	*menuItemCopy = [[menuItem copy] autorelease];
  1249 			if (submenu && [submenu respondsToSelector:@selector(delegate)]) {
  1250 				[[menuItemCopy submenu] setDelegate:[submenu delegate]];
  1251 			}
  1252 
  1253 			[contextualMenu insertItem:menuItemCopy atIndex:i++];
  1254 		}
  1255 	}
  1256 	
  1257     return contextualMenu;
  1258 }
  1259 
  1260 #pragma mark Drag and drop
  1261 
  1262 /*An NSTextView which has setImportsGraphics:YES as of 10.5 gets the following drag types by default:
  1263  "NeXT RTFD pasteboard type",
  1264  "NeXT Rich Text Format v1.0 pasteboard type",
  1265  "Apple HTML pasteboard type",
  1266  NSFilenamesPboardType,
  1267  "CorePasteboardFlavorType 0x6D6F6F76",
  1268  "Apple PDF pasteboard type",
  1269  "NeXT TIFF v4.0 pasteboard type",
  1270  "Apple PICT pasteboard type",
  1271  "NeXT Encapsulated PostScript v1.2 pasteboard type",
  1272  "Apple PNG pasteboard type",
  1273  WebURLsWithTitlesPboardType,
  1274  "CorePasteboardFlavorType 0x75726C20",
  1275  "Apple URL pasteboard type",
  1276  NSStringPboardType,
  1277  "NSColor pasteboard type",
  1278  "NeXT font pasteboard type",
  1279  "NeXT ruler pasteboard type",
  1280 */
  1281 
  1282 - (NSArray *)acceptableDragTypes;
  1283 {
  1284     NSMutableArray *dragTypes;
  1285     
  1286     dragTypes = [NSMutableArray arrayWithArray:[super acceptableDragTypes]];
  1287 	[dragTypes addObject:AIiTunesTrackPboardType];
  1288 
  1289     return dragTypes;
  1290 }
  1291 
  1292 - (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender
  1293 {
  1294 	NSPasteboard	*pasteboard = [sender draggingPasteboard];
  1295 
  1296 	if ([pasteboard availableTypeFromArray:FILES_AND_IMAGES_TYPES])
  1297 		return NSDragOperationCopy;
  1298 	else 
  1299 		return [super draggingEntered:sender];
  1300 }
  1301 
  1302 - (NSDragOperation)draggingUpdated:(id <NSDraggingInfo>)sender
  1303 {
  1304 	NSPasteboard	*pasteboard = [sender draggingPasteboard];
  1305 	
  1306 	if ([pasteboard availableTypeFromArray:FILES_AND_IMAGES_TYPES])
  1307 		return NSDragOperationCopy;
  1308 	else 
  1309 		return [super draggingUpdated:sender];
  1310 }
  1311 
  1312 //We don't need to prepare for the types we are handling in performDragOperation: below
  1313 - (BOOL)prepareForDragOperation:(id <NSDraggingInfo>)sender
  1314 {
  1315 	NSPasteboard	*pasteboard = [sender draggingPasteboard];
  1316 	NSString 		*type = [pasteboard availableTypeFromArray:FILES_AND_IMAGES_TYPES];
  1317 	NSString		*superclassType = [pasteboard availableTypeFromArray:PASS_TO_SUPERCLASS_DRAG_TYPE_ARRAY];
  1318 	BOOL			allowDragOperation;
  1319 
  1320 	if (type && !superclassType) {		
  1321 		// XXX - This shouldn't let you insert into a view for which the delegate says NO to some sort of check.
  1322 		allowDragOperation = YES;
  1323 	} else {
  1324 		allowDragOperation = [super prepareForDragOperation:sender];
  1325 	}
  1326 	
  1327 	return (allowDragOperation);
  1328 }
  1329 
  1330 //No conclusion is needed for the types we are handling in performDragOperation: below
  1331 - (void)concludeDragOperation:(id <NSDraggingInfo>)sender
  1332 {
  1333 	NSPasteboard	*pasteboard = [sender draggingPasteboard];
  1334 	NSString 		*type = [pasteboard availableTypeFromArray:FILES_AND_IMAGES_TYPES];
  1335 	NSString		*superclassType = [pasteboard availableTypeFromArray:PASS_TO_SUPERCLASS_DRAG_TYPE_ARRAY];
  1336 	
  1337 	
  1338 	
  1339 	if (!type || superclassType) {
  1340 		[super concludeDragOperation:sender];
  1341 	}
  1342 }
  1343 
  1344 - (void)addAttachmentsFromPasteboard:(NSPasteboard *)pasteboard
  1345 {
  1346 	NSString *availableType;
  1347 	if ((availableType = [pasteboard availableTypeFromArray:[NSArray arrayWithObjects:NSFilenamesPboardType, AIiTunesTrackPboardType, nil]])) {
  1348 		//The pasteboard points to one or more files on disc.  Use them directly.
  1349 		NSArray			*files = nil;
  1350 		if ([availableType isEqualToString:NSFilenamesPboardType]) {
  1351 			files = [pasteboard propertyListForType:NSFilenamesPboardType];
  1352 			
  1353 		} else if ([availableType isEqualToString:AIiTunesTrackPboardType]) {
  1354 			files = [pasteboard filesFromITunesDragPasteboard];
  1355 		}
  1356 		
  1357 		NSString		*path;
  1358 		for (path in files) {
  1359 			[self addAttachmentOfPath:path];
  1360 		}
  1361 		
  1362 	} else {
  1363 		//The pasteboard contains image data with no corresponding file.
  1364 		NSImage	*image = [[NSImage alloc] initWithPasteboard:pasteboard];
  1365 		[self addAttachmentOfImage:image];
  1366 		[image release];			
  1367 	}	
  1368 }
  1369 
  1370 //The textView's method of inserting into the view is insufficient; we can do better.
  1371 - (BOOL)performDragOperation:(id <NSDraggingInfo>)sender
  1372 {
  1373 	NSPasteboard	*pasteboard = [sender draggingPasteboard];
  1374 	BOOL			success = NO;
  1375 
  1376 	NSString *myType = [[pasteboard types] firstObjectCommonWithArray:FILES_AND_IMAGES_TYPES];
  1377 	NSString *superclassType = [[pasteboard types] firstObjectCommonWithArray:PASS_TO_SUPERCLASS_DRAG_TYPE_ARRAY];
  1378 	
  1379 	if (myType &&
  1380 		(!superclassType || ([[pasteboard types] indexOfObject:myType] < [[pasteboard types] indexOfObject:superclassType]))) {
  1381 		[self addAttachmentsFromPasteboard:pasteboard];
  1382 		
  1383 		success = YES;		
  1384 	} else {
  1385 		success = [super performDragOperation:sender];
  1386 		
  1387 	}
  1388 
  1389 	return success;
  1390 }
  1391 
  1392 #pragma mark Spell Checking
  1393 
  1394 /*!
  1395  * @brief Spell checking was toggled
  1396  *
  1397  * Set our preference, as we toggle spell checking globally when it is changed locally
  1398  */
  1399 - (void)toggleContinuousSpellChecking:(id)sender
  1400 {
  1401 	[super toggleContinuousSpellChecking:sender];
  1402 
  1403 	[adium.preferenceController setPreference:[NSNumber numberWithBool:[self isContinuousSpellCheckingEnabled]]
  1404 										 forKey:KEY_SPELL_CHECKING
  1405 										  group:PREF_GROUP_DUAL_WINDOW_INTERFACE];
  1406 }
  1407 
  1408 /*!
  1409  * @brief Grammar checking was toggled
  1410  *
  1411  * Set our preference, as we toggle grammar checking globally when it is changed locally
  1412  */
  1413 - (void)toggleGrammarChecking:(id)sender
  1414 {
  1415 	[super toggleGrammarChecking:sender];
  1416 	
  1417 	[adium.preferenceController setPreference:[NSNumber numberWithBool:[self isGrammarCheckingEnabled]]
  1418 										 forKey:KEY_GRAMMAR_CHECKING
  1419 										  group:PREF_GROUP_DUAL_WINDOW_INTERFACE];
  1420 }
  1421 
  1422 #pragma mark Substitutions
  1423 /*!
  1424  * @brief Dash substitution was toggled
  1425  */
  1426 - (void)toggleAutomaticDashSubstitution:(id)sender
  1427 {
  1428 	[super toggleAutomaticDashSubstitution:sender];
  1429 	
  1430 	[adium.preferenceController setPreference:[NSNumber numberWithBool:[self isAutomaticDashSubstitutionEnabled]]
  1431 									   forKey:KEY_SUBSTITUTION_DASH
  1432 										group:PREF_GROUP_DUAL_WINDOW_INTERFACE];
  1433 }
  1434 
  1435 /*!
  1436  * @brief Data Detector substitution was toggled
  1437  */
  1438 - (void)toggleAutomaticDataDetection:(id)sender
  1439 {
  1440 	[super toggleAutomaticDataDetection:sender];
  1441 	
  1442 	[adium.preferenceController setPreference:[NSNumber numberWithBool:[self isAutomaticDataDetectionEnabled]]
  1443 									   forKey:KEY_SUBSTITUTION_DATA_DETECTORS
  1444 										group:PREF_GROUP_DUAL_WINDOW_INTERFACE];
  1445 }
  1446 
  1447 /*!
  1448  * @brief Text Replacement substitution was toggled
  1449  */
  1450 - (void)toggleAutomaticTextReplacement:(id)sender
  1451 {
  1452 	[super toggleAutomaticTextReplacement:sender];
  1453 	
  1454 	[adium.preferenceController setPreference:[NSNumber numberWithBool:[self isAutomaticTextReplacementEnabled]]
  1455 									   forKey:KEY_SUBSTITUTION_REPLACEMENT
  1456 										group:PREF_GROUP_DUAL_WINDOW_INTERFACE];
  1457 }
  1458 
  1459 /*!
  1460  * @brief Spelling replacement substitution was toggled
  1461  */
  1462 - (void)toggleAutomaticSpellingCorrection:(id)sender
  1463 {
  1464 	[super toggleAutomaticSpellingCorrection:sender];
  1465 	
  1466 	[adium.preferenceController setPreference:[NSNumber numberWithBool:[self isAutomaticSpellingCorrectionEnabled]]
  1467 									   forKey:KEY_SUBSTITUTION_SPELLING
  1468 										group:PREF_GROUP_DUAL_WINDOW_INTERFACE];
  1469 }
  1470 
  1471 /*!
  1472  * @brief Smart insert delete was toggled
  1473  */
  1474 - (void)toggleSmartInsertDelete:(id)sender
  1475 {
  1476 	[super toggleSmartInsertDelete:sender];
  1477 	
  1478 	[adium.preferenceController setPreference:[NSNumber numberWithBool:[self smartInsertDeleteEnabled]]
  1479 									   forKey:KEY_SUBSTITUTION_COPY_PASTE
  1480 										group:PREF_GROUP_DUAL_WINDOW_INTERFACE];
  1481 }
  1482 
  1483 /*!
  1484  * @brief Smart quote substitution was toggled
  1485  */
  1486 - (void)toggleAutomaticQuoteSubstitution:(id)sender
  1487 {
  1488 	[super toggleAutomaticQuoteSubstitution:sender];
  1489 	
  1490 	[adium.preferenceController setPreference:[NSNumber numberWithBool:[self isAutomaticQuoteSubstitutionEnabled]]
  1491 									   forKey:KEY_SUBSTITUTION_QUOTE
  1492 										group:PREF_GROUP_DUAL_WINDOW_INTERFACE];
  1493 }
  1494 
  1495 /*!
  1496  * @brief Smart link substitution was toggled
  1497  */
  1498 - (void)toggleAutomaticLinkDetection:(id)sender
  1499 {
  1500 	[super toggleAutomaticLinkDetection:sender];
  1501 	
  1502 	[adium.preferenceController setPreference:[NSNumber numberWithBool:[self isAutomaticLinkDetectionEnabled]]
  1503 									   forKey:KEY_SUBSTITUTION_LINK
  1504 										group:PREF_GROUP_DUAL_WINDOW_INTERFACE];
  1505 }
  1506 
  1507 #pragma mark Autocompleting
  1508 - (NSRange)rangeForUserCompletion
  1509 {
  1510 	NSRange completionRange = [super rangeForUserCompletion];
  1511 	
  1512 	if ([self.delegate respondsToSelector:@selector(textView:rangeForCompletion:)]) {
  1513 		completionRange = [self.delegate textView:self rangeForCompletion:completionRange];
  1514 	}
  1515 	
  1516 	return completionRange;
  1517 }
  1518 
  1519 #pragma mark Writing Direction
  1520 - (void)toggleBaseWritingDirection:(id)sender
  1521 {
  1522 	if ([self baseWritingDirection] == NSWritingDirectionRightToLeft) {
  1523 		[self setBaseWritingDirection:NSWritingDirectionLeftToRight];
  1524 	} else {
  1525 		[self setBaseWritingDirection:NSWritingDirectionRightToLeft];			
  1526 	}
  1527 	
  1528 	//Apply it immediately
  1529 	[self setBaseWritingDirection:[self baseWritingDirection]
  1530 							range:NSMakeRange(0, [[self textStorage] length])];
  1531 }
  1532 
  1533 #pragma mark Attachments
  1534 /*!
  1535  * @brief Add an attachment of the file at inPath at the current insertion point
  1536  *
  1537  * @param inPath The full path, whose contents will not be loaded into memory at this time
  1538  */
  1539 - (void)addAttachmentOfPath:(NSString *)inPath
  1540 {
  1541 	if ([[inPath pathExtension] caseInsensitiveCompare:@"textClipping"] == NSOrderedSame) {
  1542 		inPath = [inPath stringByAppendingString:@"/..namedfork/rsrc"];
  1543 
  1544 		NSData *data = [NSData dataWithContentsOfFile:inPath];
  1545 		if (data) {
  1546 			data = [data subdataWithRange:NSMakeRange(260, [data length] - 260)];
  1547 			
  1548 			NSAttributedString *clipping = [[[NSAttributedString alloc] initWithRTF:data documentAttributes:nil] autorelease];
  1549 			if (clipping) {
  1550 				NSDictionary	*attributes = [[self typingAttributes] copy];
  1551 				
  1552 				[self insertText:clipping];
  1553 
  1554 				if (attributes) {
  1555 					[self setTypingAttributes:attributes];
  1556 				}
  1557 				
  1558 				[attributes release];
  1559 			}
  1560 		}
  1561 
  1562 	} else {
  1563 		AITextAttachmentExtension   *attachment = [[AITextAttachmentExtension alloc] init];
  1564 		[attachment setPath:inPath];
  1565 		[attachment setString:[inPath lastPathComponent]];
  1566 		[attachment setShouldSaveImageForLogging:YES];
  1567 		
  1568 		//Insert an attributed string into the text at the current insertion point
  1569 		[self insertText:[self attributedStringWithTextAttachmentExtension:attachment]];
  1570 		
  1571 		[attachment release];
  1572 	}
  1573 }
  1574 
  1575 /*!
  1576  * @brief Add an attachment of inImage at the current insertion point
  1577  */
  1578 - (void)addAttachmentOfImage:(NSImage *)inImage
  1579 {
  1580 	AITextAttachmentExtension   *attachment = [[AITextAttachmentExtension alloc] init];
  1581 	
  1582 	[attachment setImage:inImage];
  1583 	[attachment setShouldSaveImageForLogging:YES];
  1584 	
  1585 	//Insert an attributed string into the text at the current insertion point
  1586 	[self insertText:[self attributedStringWithTextAttachmentExtension:attachment]];
  1587 	
  1588 	[attachment release];
  1589 }
  1590 
  1591 /*!
  1592  * @brief Generate an NSAttributedString which contains attachment and displays it using attachment's iconImage
  1593  */
  1594 - (NSAttributedString *)attributedStringWithTextAttachmentExtension:(AITextAttachmentExtension *)attachment
  1595 {
  1596 	NSTextAttachmentCell		*cell = [[NSTextAttachmentCell alloc] initImageCell:[attachment iconImage]];
  1597 	
  1598 	[attachment setHasAlternate:NO];
  1599 	[attachment setAttachmentCell:cell];
  1600 	[cell release];
  1601 	
  1602 	return [NSAttributedString attributedStringWithAttachment:attachment];
  1603 }
  1604 
  1605 /*!
  1606  * @brief Given RTFD data, return an NSAttributedString whose attachments are all AITextAttachmentExtension objects
  1607  */
  1608 - (NSAttributedString *)attributedStringWithAITextAttachmentExtensionsFromRTFDData:(NSData *)data
  1609 {
  1610 	NSMutableAttributedString *attributedString = [[[NSMutableAttributedString alloc] initWithRTFD:data
  1611 																				documentAttributes:NULL] autorelease];
  1612 	if ([attributedString length] && [attributedString containsAttachments]) {
  1613 		int							currentLocation = 0;
  1614 		NSRange						attachmentRange;
  1615 		
  1616 		NSString					*attachmentCharacterString = [NSString stringWithFormat:@"%C",NSAttachmentCharacter];
  1617 		
  1618 		//Find each attachment
  1619 		attachmentRange = [[attributedString string] rangeOfString:attachmentCharacterString
  1620 														   options:0 
  1621 															 range:NSMakeRange(currentLocation,
  1622 																			   [attributedString length] - currentLocation)];
  1623 		while (attachmentRange.length != 0) {
  1624 			//Found an attachment in at attachmentRange.location
  1625 			NSTextAttachment	*attachment = [attributedString attribute:NSAttachmentAttributeName
  1626 																  atIndex:attachmentRange.location
  1627 														   effectiveRange:nil];
  1628 
  1629 			//If it's not already an AITextAttachmentExtension, make it into one
  1630 			if (![attachment isKindOfClass:[AITextAttachmentExtension class]]) {
  1631 				NSAttributedString	*replacement;
  1632 				NSFileWrapper		*fileWrapper = [attachment fileWrapper];
  1633 				NSString			*destinationPath;
  1634 				NSString			*preferredName = [fileWrapper preferredFilename];
  1635 				
  1636 				//Get a unique folder within our temporary directory
  1637 				destinationPath = [NSTemporaryDirectory() stringByAppendingPathComponent:[[NSProcessInfo processInfo] globallyUniqueString]];
  1638 				[[NSFileManager defaultManager] createDirectoryAtPath:destinationPath withIntermediateDirectories:YES attributes:nil error:NULL];
  1639 				destinationPath = [destinationPath stringByAppendingPathComponent:preferredName];
  1640 				
  1641 				//Write the file out to it
  1642 				[fileWrapper writeToFile:destinationPath
  1643 							  atomically:NO
  1644 						 updateFilenames:NO];
  1645 				
  1646 				//Now create an AITextAttachmentExtension pointing to it
  1647 				AITextAttachmentExtension   *attachment = [[AITextAttachmentExtension alloc] init];
  1648 				[attachment setPath:destinationPath];
  1649 				[attachment setString:preferredName];
  1650 				[attachment setShouldSaveImageForLogging:YES];
  1651 
  1652 				//Insert an attributed string into the text at the current insertion point
  1653 				replacement = [self attributedStringWithTextAttachmentExtension:attachment];
  1654 				[attachment release];
  1655 				
  1656 				//Remove the NSTextAttachment, replacing it the AITextAttachmentExtension
  1657 				[attributedString replaceCharactersInRange:attachmentRange
  1658 									  withAttributedString:replacement];
  1659 				
  1660 				attachmentRange.length = [replacement length];					
  1661 			} 
  1662 			
  1663 			currentLocation = attachmentRange.location + attachmentRange.length;
  1664 			
  1665 			
  1666 			//Find the next attachment
  1667 			attachmentRange = [[attributedString string] rangeOfString:attachmentCharacterString
  1668 															   options:0
  1669 																 range:NSMakeRange(currentLocation,
  1670 																				   [attributedString length] - currentLocation)];
  1671 		}
  1672 	}
  1673 
  1674 	return attributedString;
  1675 }
  1676 
  1677 - (void)changeDocumentBackgroundColor:(id)sender
  1678 {
  1679 	NSColor	*backgroundColor = [sender color];
  1680 	NSRange	selectedRange = [self selectedRange];
  1681 
  1682 	[[self textStorage] addAttribute:NSBackgroundColorAttributeName
  1683 							   value:backgroundColor
  1684 							   range:selectedRange];
  1685 	[[self textStorage] addAttribute:AIBodyColorAttributeName
  1686 							   value:backgroundColor
  1687 							   range:selectedRange];
  1688 
  1689 	NSMutableDictionary *typingAttributes = [[self typingAttributes] mutableCopy];
  1690 	[typingAttributes setObject:backgroundColor forKey:AIBodyColorAttributeName];
  1691 	[typingAttributes setObject:backgroundColor forKey:NSBackgroundColorAttributeName];
  1692 	[self setTypingAttributes:typingAttributes];
  1693 	[typingAttributes release];	
  1694 
  1695 	[[self textStorage] edited:NSTextStorageEditedAttributes
  1696 						 range:selectedRange
  1697 				changeInLength:0];
  1698 }
  1699 
  1700 - (void)insertText:(id)aString
  1701 {
  1702 	[super insertText:aString];
  1703 	// Auto set the writing direction based on our content
  1704 	[self setBaseWritingDirection:[[[self textStorage] string] baseWritingDirection]];
  1705 }
  1706 
  1707 @end
  1708 
  1709 @implementation NSMutableAttributedString (AIMessageEntryTextViewAdditions)
  1710 - (void)convertForPasteWithTraitsUsingAttributes:(NSDictionary *)typingAttributes;
  1711 {
  1712 	NSRange fullRange = NSMakeRange(0, [self length]);
  1713 
  1714 	//Remove non-trait attributes
  1715 	if ([typingAttributes objectForKey:NSBackgroundColorAttributeName]) {
  1716 		[self addAttribute:NSBackgroundColorAttributeName
  1717 					 value:[typingAttributes objectForKey:NSBackgroundColorAttributeName]
  1718 					 range:fullRange];
  1719 
  1720 	} else {
  1721 		[self removeAttribute:NSBackgroundColorAttributeName range:fullRange];
  1722 	}
  1723 
  1724 	if ([typingAttributes objectForKey:NSForegroundColorAttributeName]) {
  1725 		[self addAttribute:NSForegroundColorAttributeName
  1726 					 value:[typingAttributes objectForKey:NSForegroundColorAttributeName]
  1727 					 range:fullRange];
  1728 		
  1729 	} else {
  1730 		[self removeAttribute:NSForegroundColorAttributeName range:fullRange];
  1731 	}
  1732 
  1733 	if ([typingAttributes objectForKey:NSParagraphStyleAttributeName]) {
  1734 		[self addAttribute:NSParagraphStyleAttributeName
  1735 					 value:[typingAttributes objectForKey:NSParagraphStyleAttributeName]
  1736 					 range:fullRange];
  1737 		
  1738 	} else {
  1739 		[self removeAttribute:NSParagraphStyleAttributeName range:fullRange];
  1740 	}
  1741 
  1742 	[self removeAttribute:NSBaselineOffsetAttributeName range:fullRange];
  1743 	[self removeAttribute:NSCursorAttributeName range:fullRange];
  1744 	[self removeAttribute:NSExpansionAttributeName range:fullRange];
  1745 	[self removeAttribute:NSKernAttributeName range:fullRange];
  1746 	[self removeAttribute:NSLigatureAttributeName range:fullRange];
  1747 	[self removeAttribute:NSObliquenessAttributeName range:fullRange];
  1748 	[self removeAttribute:NSShadowAttributeName range:fullRange];
  1749 	[self removeAttribute:NSStrokeWidthAttributeName range:fullRange];
  1750 	
  1751 	NSRange			searchRange = NSMakeRange(0, fullRange.length);
  1752 	NSFontManager	*fontManager = [NSFontManager sharedFontManager];
  1753 	NSFont			*myFont = [typingAttributes objectForKey:NSFontAttributeName];
  1754 
  1755 	while (searchRange.location < fullRange.length) {
  1756 		NSFont *font;
  1757 		NSRange effectiveRange;
  1758 		font = [self attribute:NSFontAttributeName 
  1759 					   atIndex:searchRange.location
  1760 		 longestEffectiveRange:&effectiveRange
  1761 					   inRange:searchRange];
  1762 
  1763 		if (font) {
  1764 			NSFontTraitMask thisFontTraits = [fontManager traitsOfFont:font];
  1765 			NSFontTraitMask	traits = 0;
  1766 			
  1767 			if (thisFontTraits & NSBoldFontMask) {
  1768 				traits |= NSBoldFontMask;
  1769 			} else {
  1770 				traits |= NSUnboldFontMask;				
  1771 			}
  1772 
  1773 			if (thisFontTraits & NSItalicFontMask) {
  1774 				traits |= NSItalicFontMask;
  1775 			} else {
  1776 				traits |= NSUnitalicFontMask;
  1777 			}
  1778 			
  1779 			font = [fontManager fontWithFamily:[myFont familyName]
  1780 										traits:traits
  1781 										weight:[fontManager weightOfFont:myFont]
  1782 										  size:[myFont pointSize]];
  1783 			 
  1784 			if (font) {
  1785 				[self addAttribute:NSFontAttributeName
  1786 							 value:font
  1787 							 range:effectiveRange];
  1788 			}
  1789 		}
  1790 
  1791 		searchRange.location = effectiveRange.location + effectiveRange.length;
  1792 		searchRange.length = fullRange.length - searchRange.location;
  1793 	}
  1794 
  1795 	//Replace attachments with nothing! Absolutely nothing!
  1796 	[self convertAttachmentsToStringsUsingPlaceholder:@""];
  1797 }
  1798 
  1799 
  1800 
  1801 
  1802 @end