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