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