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