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.
2 * Adium is the legal property of its developers, whose names are listed in the copyright file included
3 * with this source distribution.
5 * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
6 * General Public License as published by the Free Software Foundation; either version 2 of the License,
7 * or (at your option) any later version.
9 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
10 * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
11 * Public License for more details.
13 * You should have received a copy of the GNU General Public License along with this program; if not,
14 * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
17 #import <Adium/AIChat.h>
18 #import <Adium/AIAccount.h>
19 #import <Adium/AIListObject.h>
20 #import <Adium/AIListContact.h>
21 #import <Adium/AIMessageEntryTextView.h>
22 #import <Adium/ESFileWrapperExtension.h>
23 #import <Adium/AITextAttachmentExtension.h>
25 #import <Adium/AIMenuControllerProtocol.h>
26 #import <Adium/AIContentControllerProtocol.h>
27 #import <Adium/AIInterfaceControllerProtocol.h>
28 #import <Adium/AIContentContext.h>
30 #import <AIUtilities/AIApplicationAdditions.h>
31 #import <AIUtilities/AIAttributedStringAdditions.h>
32 #import <AIUtilities/AIColorAdditions.h>
33 #import <AIUtilities/AITextAttributes.h>
34 #import <AIUtilities/AIImageAdditions.h>
35 #import <AIUtilities/AIFileManagerAdditions.h>
36 #import <AIUtilities/AIPasteboardAdditions.h>
37 #import <AIUtilities/AIBezierPathAdditions.h>
38 #import <Adium/AIContactControllerProtocol.h>
40 #import <FriBidi/NSString-FBAdditions.h>
42 #import <AIUtilities/AILeopardCompatibility.h>
44 #define MAX_HISTORY 25 //Number of messages to remember in history
45 #define ENTRY_TEXTVIEW_PADDING 6 //Padding for auto-sizing
47 #define KEY_DISABLE_TYPING_NOTIFICATIONS @"Disable Typing Notifications"
49 #define KEY_SPELL_CHECKING @"Spell Checking Enabled"
50 #define KEY_GRAMMAR_CHECKING @"Grammar Checking Enabled"
51 #define PREF_GROUP_DUAL_WINDOW_INTERFACE @"Dual Window Interface"
53 #define KEY_SUBSTITUTION_DASH @"Smart Dash Substitutions"
54 #define KEY_SUBSTITUTION_DATA_DETECTORS @"Smart Data Detectors Substitutions"
55 #define KEY_SUBSTITUTION_REPLACEMENT @"Text Replacement Substitutions"
56 #define KEY_SUBSTITUTION_SPELLING @"Spelling Substitutions"
57 #define KEY_SUBSTITUTION_COPY_PASTE @"Smart Copy Paste Substitutions"
58 #define KEY_SUBSTITUTION_QUOTE @"Smart Quote Substitutions"
59 #define KEY_SUBSTITUTION_LINK @"Smart Links Substitutions"
61 #define INDICATOR_RIGHT_PADDING 2 // Padding between right side of the message view and the rightmost indicator
63 #define PREF_GROUP_CHARACTER_COUNTER @"Character Counter"
64 #define KEY_CHARACTER_COUNTER_ENABLED @"Character Counter Enabled"
65 #define KEY_MAX_NUMBER_OF_CHARACTERS @"Maximum Number Of Characters"
67 #define FILES_AND_IMAGES_TYPES [NSArray arrayWithObjects: \
68 NSFilenamesPboardType, AIiTunesTrackPboardType, NSTIFFPboardType, NSPDFPboardType, NSPICTPboardType, nil]
70 #define PASS_TO_SUPERCLASS_DRAG_TYPE_ARRAY [NSArray arrayWithObjects: \
71 NSRTFPboardType, NSStringPboardType, nil]
74 * @class AISimpleTextView
75 * @brief Just draws an attributed string. That's it.
77 * No really, it's dead simple. It just draws an attributed string in its bounds (which you set). That's it.
78 * It's totally not even useful.
81 @implementation AISimpleTextView
90 - (void)drawRect:(NSRect)rect
92 [string drawInRect:self.bounds];
96 @interface AIMessageEntryTextView ()
97 - (void)_setPushIndicatorVisible:(BOOL)visible;
98 - (void)positionPushIndicator;
99 - (void)_resetCacheAndPostSizeChanged;
101 - (NSAttributedString *)attributedStringWithAITextAttachmentExtensionsFromRTFDData:(NSData *)data;
102 - (NSAttributedString *)attributedStringWithTextAttachmentExtension:(AITextAttachmentExtension *)attachment;
103 - (void)addAttachmentOfPath:(NSString *)inPath;
104 - (void)addAttachmentOfImage:(NSImage *)inImage;
105 - (void)addAttachmentsFromPasteboard:(NSPasteboard *)pasteboard;
107 - (void)setCharacterCounterVisible:(BOOL)visible;
108 - (void)setCharacterCounterMaximum:(int)inMaxCharacters;
109 - (void)setCharacterCounterPrefix:(NSString *)prefix;
110 - (void)updateCharacterCounter;
111 - (void)positionCharacterCounter;
113 - (void)positionIndicators:(NSNotification *)notification;
116 @interface NSMutableAttributedString (AIMessageEntryTextViewAdditions)
117 - (void)convertForPasteWithTraitsUsingAttributes:(NSDictionary *)inAttributes;
120 @implementation AIMessageEntryTextView
122 - (void)_initMessageEntryTextView
124 associatedView = nil;
127 pushPopEnabled = YES;
128 historyEnabled = YES;
130 homeToStartOfLine = YES;
132 enableTypingNotifications = NO;
133 historyArray = [[NSMutableArray alloc] initWithObjects:@"",nil];
134 pushArray = [[NSMutableArray alloc] init];
135 currentHistoryLocation = 0;
136 [self setDrawsBackground:YES];
137 _desiredSizeCached = NSMakeSize(0,0);
138 characterCounter = nil;
139 characterCounterPrefix = nil;
141 savedTextColor = nil;
143 if ([self respondsToSelector:@selector(setAllowsUndo:)]) {
144 [self setAllowsUndo:YES];
146 if ([self respondsToSelector:@selector(setAllowsDocumentBackgroundColorChange:)]) {
147 [self setAllowsDocumentBackgroundColorChange:YES];
150 [self setImportsGraphics:YES];
152 [[NSNotificationCenter defaultCenter] addObserver:self
153 selector:@selector(textDidChange:)
154 name:NSTextDidChangeNotification
156 [[NSNotificationCenter defaultCenter] addObserver:self
157 selector:@selector(frameDidChange:)
158 name:NSViewFrameDidChangeNotification
160 [[NSNotificationCenter defaultCenter] addObserver:self
161 selector:@selector(toggleMessageSending:)
162 name:@"AIChatDidChangeCanSendMessagesNotification"
164 [[NSNotificationCenter defaultCenter] addObserver:self
165 selector:@selector(contentObjectAdded:)
166 name:Content_ContentObjectAdded
169 [adium.preferenceController registerPreferenceObserver:self forGroup:PREF_GROUP_DUAL_WINDOW_INTERFACE];
171 [[AIContactObserverManager sharedManager] registerListObjectObserver:self];
175 - (id)initWithFrame:(NSRect)frameRect textContainer:(NSTextContainer *)aTextContainer
177 if ((self = [super initWithFrame:frameRect textContainer:aTextContainer])) {
178 [self _initMessageEntryTextView];
184 - (id)initWithCoder:(NSCoder *)coder
186 if ((self = [super initWithCoder:coder])) {
187 [self _initMessageEntryTextView];
195 if(chat.isGroupChat) {
196 [chat removeObserver:self forKeyPath:@"Character Counter Max"];
197 [chat removeObserver:self forKeyPath:@"Character Counter Prefix"];
200 [[NSNotificationCenter defaultCenter] removeObserver:self];
201 [adium.preferenceController unregisterPreferenceObserver:self];
202 [[AIContactObserverManager sharedManager] unregisterListObjectObserver:self];
204 [savedTextColor release];
205 [characterCounter release];
206 [characterCounterPrefix release];
208 [associatedView release];
209 [historyArray release]; historyArray = nil;
210 [pushArray release]; pushArray = nil;
215 - (void) setDelegate:(id<AIMessageEntryTextViewDelegate>)del
217 super.delegate = del;
220 - (id<AIMessageEntryTextViewDelegate>)delegate
222 return super.delegate;
225 - (void)keyDown:(NSEvent *)inEvent
227 NSString *charactersIgnoringModifiers = [inEvent charactersIgnoringModifiers];
229 if ([charactersIgnoringModifiers length]) {
230 unichar inChar = [charactersIgnoringModifiers characterAtIndex:0];
231 unsigned int flags = [inEvent modifierFlags];
233 //We have to test ctrl before option, because otherwise we'd miss ctrl-option-* events
234 if (pushPopEnabled &&
235 (flags & NSControlKeyMask) && !(flags & NSShiftKeyMask)) {
236 if (inChar == NSUpArrowFunctionKey) {
238 } else if (inChar == NSDownArrowFunctionKey) {
240 } else if (inChar == 's') {
243 [super keyDown:inEvent];
246 } else if (historyEnabled &&
247 (flags & NSAlternateKeyMask) && !(flags & NSShiftKeyMask)) {
248 if (inChar == NSUpArrowFunctionKey) {
250 } else if (inChar == NSDownArrowFunctionKey) {
253 [super keyDown:inEvent];
256 } else if (associatedView &&
257 (flags & NSCommandKeyMask) && !(flags & NSShiftKeyMask)) {
258 if ((inChar == NSUpArrowFunctionKey || inChar == NSDownArrowFunctionKey) ||
259 (inChar == NSHomeFunctionKey || inChar == NSEndFunctionKey) ||
260 (inChar == NSPageUpFunctionKey || inChar == NSPageDownFunctionKey)) {
261 //Pass the associatedView a keyDown event equivalent equal to inEvent except without the modifier flags
262 [associatedView keyDown:[NSEvent keyEventWithType:[inEvent type]
263 location:[inEvent locationInWindow]
265 timestamp:[inEvent timestamp]
266 windowNumber:[inEvent windowNumber]
267 context:[inEvent context]
268 characters:[inEvent characters]
269 charactersIgnoringModifiers:charactersIgnoringModifiers
270 isARepeat:[inEvent isARepeat]
271 keyCode:[inEvent keyCode]]];
273 [super keyDown:inEvent];
276 } else if (associatedView &&
277 (inChar == NSPageUpFunctionKey || inChar == NSPageDownFunctionKey)) {
278 [associatedView keyDown:inEvent];
280 } else if (inChar == NSHomeFunctionKey || inChar == NSEndFunctionKey) {
281 if (homeToStartOfLine) {
284 if (flags & NSShiftKeyMask) {
285 //With shift, select to the beginning/end of the line
286 NSRange selectedRange = [self selectedRange];
287 if (inChar == NSHomeFunctionKey) {
288 //Home: from 0 to the current location
289 newRange.location = 0;
290 newRange.length = selectedRange.location;
292 //End: from current location to the end
293 newRange.location = selectedRange.location;
294 newRange.length = [[self string] length] - newRange.location;
298 newRange.location = ((inChar == NSHomeFunctionKey) ? 0 : [[self string] length]);
302 [self setSelectedRange:newRange];
305 //If !homeToStartOfLine, pass the keypress to our associated view.
306 if (associatedView) {
307 [associatedView keyDown:inEvent];
309 [super keyDown:inEvent];
313 } else if (inChar == NSTabCharacter) {
314 if ([self.delegate respondsToSelector:@selector(textViewShouldTabComplete:)] &&
315 [self.delegate textViewShouldTabComplete:self]) {
318 [super keyDown:inEvent];
322 [super keyDown:inEvent];
325 [super keyDown:inEvent];
330 - (void)textDidChange:(NSNotification *)notification
332 //Update typing status
333 if (enableTypingNotifications) {
334 [adium.contentController userIsTypingContentForChat:chat hasEnteredText:[[self textStorage] length] > 0];
337 //Hide any existing contact list tooltip when we begin typing
338 [adium.interfaceController showTooltipForListObject:nil atScreenPoint:NSZeroPoint onWindow:nil];
340 //Reset cache and resize
341 [self _resetCacheAndPostSizeChanged];
343 //Update the character counter
344 if (characterCounter) {
345 [self updateCharacterCounter];
350 * @brief Clear any link attribute in the current typing attributes
352 * Any link attribute is removed. All other typing attributes are unchanged.
354 - (void)clearLinkAttribute
356 NSDictionary *typingAttributes = [self typingAttributes];
358 if ([typingAttributes objectForKey:NSLinkAttributeName]) {
359 NSMutableDictionary *newTypingAttributes = [typingAttributes mutableCopy];
361 [newTypingAttributes removeObjectForKey:NSLinkAttributeName];
362 [self setTypingAttributes:newTypingAttributes];
364 [newTypingAttributes release];
369 * @brief The user pressed escape: clear our text view in response
371 - (void)cancelOperation:(id)sender
374 NSUndoManager *undoManager = [self undoManager];
375 [undoManager registerUndoWithTarget:self
376 selector:@selector(setAttributedString:)
377 object:[[[self textStorage] copy] autorelease]];
378 [undoManager setActionName:AILocalizedString(@"Clear", nil)];
380 [self setString:@""];
381 [self clearLinkAttribute];
384 if ([self.delegate respondsToSelector:@selector(textViewDidCancel:)]) {
385 [self.delegate textViewDidCancel:self];
390 //Configure ------------------------------------------------------------------------------------------------------------
391 #pragma mark Configure
392 @synthesize clearOnEscape, homeToStartOfLine, associatedView;
394 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key
395 object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
397 if ((!object || (object == chat.account)) &&
398 [group isEqualToString:GROUP_ACCOUNT_STATUS] &&
399 (!key || [key isEqualToString:KEY_DISABLE_TYPING_NOTIFICATIONS])) {
400 enableTypingNotifications = ![[chat.account preferenceForKey:KEY_DISABLE_TYPING_NOTIFICATIONS
401 group:GROUP_ACCOUNT_STATUS] boolValue];
404 if (!object && [group isEqualToString:PREF_GROUP_DUAL_WINDOW_INTERFACE]) {
405 if (!key || [key isEqualToString:KEY_GRAMMAR_CHECKING]) {
406 [self setGrammarCheckingEnabled:[[prefDict objectForKey:KEY_GRAMMAR_CHECKING] boolValue]];
409 if (!key || [key isEqualToString:KEY_SPELL_CHECKING]) {
410 [self setContinuousSpellCheckingEnabled:[[prefDict objectForKey:KEY_SPELL_CHECKING] boolValue]];
413 if ([NSApp isOnSnowLeopardOrBetter]) {
414 if (!key || [key isEqualToString:KEY_SUBSTITUTION_DASH]) {
415 [self setAutomaticDashSubstitutionEnabled:[[prefDict objectForKey:KEY_SUBSTITUTION_DASH] boolValue]];
418 if (!key || [key isEqualToString:KEY_SUBSTITUTION_DATA_DETECTORS]) {
419 [self setAutomaticDataDetectionEnabled:[[prefDict objectForKey:KEY_SUBSTITUTION_DATA_DETECTORS] boolValue]];
422 if (!key || [key isEqualToString:KEY_SUBSTITUTION_REPLACEMENT]) {
423 [self setAutomaticTextReplacementEnabled:[[prefDict objectForKey:KEY_SUBSTITUTION_REPLACEMENT] boolValue]];
426 if (!key || [key isEqualToString:KEY_SUBSTITUTION_SPELLING]) {
427 [self setAutomaticSpellingCorrectionEnabled:[[prefDict objectForKey:KEY_SUBSTITUTION_SPELLING] boolValue]];
431 if (!key || [key isEqualToString:KEY_SUBSTITUTION_COPY_PASTE]) {
432 [self setSmartInsertDeleteEnabled:[[prefDict objectForKey:KEY_SUBSTITUTION_COPY_PASTE] boolValue]];
435 if (!key || [key isEqualToString:KEY_SUBSTITUTION_QUOTE]) {
436 [self setAutomaticQuoteSubstitutionEnabled:[[prefDict objectForKey:KEY_SUBSTITUTION_QUOTE] boolValue]];
439 if (!key || [key isEqualToString:KEY_SUBSTITUTION_LINK]) {
440 [self setAutomaticLinkDetectionEnabled:[[prefDict objectForKey:KEY_SUBSTITUTION_LINK] boolValue]];
445 //Adium Text Entry -----------------------------------------------------------------------------------------------------
446 #pragma mark Adium Text Entry
449 * @brief Toggle whether message sending is enabled based on a notification. The notification object is the AIChat of the appropriate message entry view
451 - (void)toggleMessageSending:(NSNotification *)not
453 //XXX - We really should query the AIChat about this, but AIChat's "can't send" is really designed for handling offline, not banned. Bringing up the offline messaging dialog when banned would make no sense.
454 [self setSendingEnabled:[[[not userInfo] objectForKey:@"TypingEnabled"] boolValue]];
458 * @brief Are we available for sending?
460 - (BOOL)availableForSending
462 return self.sendingEnabled;
465 //Set our string, preserving the selected range
466 - (void)setAttributedString:(NSAttributedString *)inAttributedString
468 int length = [inAttributedString length];
469 NSRange oldRange = [self selectedRange];
472 [[self textStorage] setAttributedString:inAttributedString];
474 //Restore the old selected range
475 if (oldRange.location < length) {
476 if (oldRange.location + oldRange.length <= length) {
477 [self setSelectedRange:oldRange];
479 [self setSelectedRange:NSMakeRange(oldRange.location, length - oldRange.location)];
483 //Notify everyone that our text changed
484 [[NSNotificationCenter defaultCenter] postNotificationName:NSTextDidChangeNotification object:self];
487 //Set our string (plain text)
488 - (void)setString:(NSString *)string
490 [super setString:string];
492 //Notify everyone that our text changed
493 [[NSNotificationCenter defaultCenter] postNotificationName:NSTextDidChangeNotification object:self];
496 //Set our typing format
497 - (void)setTypingAttributes:(NSDictionary *)attrs
499 [super setTypingAttributes:attrs];
501 [self setInsertionPointColor:[[attrs objectForKey:NSBackgroundColorAttributeName] contrastingColor]];
506 - (BOOL)handlePasteAsRichText
508 NSPasteboard *generalPasteboard = [NSPasteboard generalPasteboard];
509 BOOL handledPaste = NO;
511 //Types is ordered by the preference for handling of the data; enumerating it lets us allow the sending application's hints to be followed.
512 for (NSString *type in generalPasteboard.types) {
513 if ([type isEqualToString:NSRTFDPboardType]) {
514 NSData *data = [generalPasteboard dataForType:NSRTFDPboardType];
515 [self insertText:[self attributedStringWithAITextAttachmentExtensionsFromRTFDData:data]];
518 } else if ([PASS_TO_SUPERCLASS_DRAG_TYPE_ARRAY containsObject:type]) {
519 //When we hit a type we should let the superclass handle, break without doing anything
522 } else if ([FILES_AND_IMAGES_TYPES containsObject:type]) {
523 [self addAttachmentsFromPasteboard:generalPasteboard];
527 if (handledPaste) break;
534 //Paste as rich text without altering our typing attributes
535 - (void)pasteAsRichText:(id)sender
537 NSDictionary *attributes = [[self typingAttributes] copy];
539 if (![self handlePasteAsRichText]) {
544 [self setTypingAttributes:attributes];
547 [attributes release];
549 [self scrollRangeToVisible:[self selectedRange]];
552 - (void)pasteAsPlainTextWithTraits:(id)sender
554 NSDictionary *attributes = [[self typingAttributes] copy];
556 NSPasteboard *generalPasteboard = [NSPasteboard generalPasteboard];
559 NSArray *supportedTypes =
560 [NSArray arrayWithObjects:NSURLPboardType, NSRTFDPboardType, NSRTFPboardType, NSHTMLPboardType, NSStringPboardType,
561 NSFilenamesPboardType, NSTIFFPboardType, NSPDFPboardType, NSPICTPboardType, nil];
563 type = [[NSPasteboard generalPasteboard] availableTypeFromArray:supportedTypes];
565 if ([type isEqualToString:NSRTFPboardType] ||
566 [type isEqualToString:NSRTFDPboardType] ||
567 [type isEqualToString:NSHTMLPboardType] ||
568 [type isEqualToString:NSStringPboardType]) {
572 data = [generalPasteboard dataForType:type];
573 } @catch (NSException *localException) {
577 //Failed. Try again with the string type.
578 if (!data && ![type isEqualToString:NSStringPboardType]) {
579 if ([[[NSPasteboard generalPasteboard] types] containsObject:NSStringPboardType]) {
580 type = NSStringPboardType;
582 data = [generalPasteboard dataForType:type];
583 } @catch (NSException *localException) {
590 //We still didn't get valid data... maybe super can handle it
593 } @catch (NSException *localException) {
599 NSMutableAttributedString *attributedString;
601 if ([type isEqualToString:NSStringPboardType]) {
602 NSString *string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
603 attributedString = [[NSMutableAttributedString alloc] initWithString:string
604 attributes:[self typingAttributes]];
609 if ([type isEqualToString:NSRTFPboardType]) {
610 attributedString = [[NSMutableAttributedString alloc] initWithRTF:data
611 documentAttributes:NULL];
612 } else if ([type isEqualToString:NSRTFDPboardType]) {
613 attributedString = [[NSMutableAttributedString alloc] initWithRTFD:data
614 documentAttributes:NULL];
615 } else /* NSHTMLPboardType */ {
616 attributedString = [[NSMutableAttributedString alloc] initWithHTML:data
617 documentAttributes:NULL];
619 } @catch (NSException *localException) {
620 //Error while reading the RTF or HTML data, which can happen. Fall back on plain text
621 if ([[[NSPasteboard generalPasteboard] types] containsObject:NSStringPboardType]) {
622 data = [generalPasteboard dataForType:NSStringPboardType];
623 NSString *string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
624 attributedString = [[NSMutableAttributedString alloc] initWithString:string
625 attributes:[self typingAttributes]];
628 attributedString = nil;
632 if (!attributedString) {
637 [attributedString convertForPasteWithTraitsUsingAttributes:[self typingAttributes]];
640 NSRange selectedRange = [self selectedRange];
641 NSTextStorage *textStorage = [self textStorage];
643 //Prepare the undo operation
644 NSUndoManager *undoManager = [self undoManager];
645 [[undoManager prepareWithInvocationTarget:textStorage]
646 replaceCharactersInRange:NSMakeRange(selectedRange.location, [attributedString length])
647 withAttributedString:[textStorage attributedSubstringFromRange:selectedRange]];
648 [undoManager setActionName:AILocalizedString(@"Paste", nil)];
651 [textStorage replaceCharactersInRange:selectedRange
652 withAttributedString:attributedString];
653 // Align our text properly (only need to if the first character was changed)
654 if (selectedRange.location == 0)
655 [self setBaseWritingDirection:[[textStorage string] baseWritingDirection]];
656 //Notify that we changed our text
657 [[NSNotificationCenter defaultCenter] postNotificationName:NSTextDidChangeNotification
659 [attributedString release];
661 } else if ([FILES_AND_IMAGES_TYPES containsObject:type] ||
662 [type isEqualToString:NSURLPboardType]) {
663 if (![self handlePasteAsRichText]) {
668 //If we didn't handle it yet, let super try to deal with it
673 [self setTypingAttributes:attributes];
676 [attributes release];
678 [self scrollRangeToVisible:[self selectedRange]];
681 #pragma mark Deletion
683 - (void)deleteBackward:(id)sender
686 [super deleteBackward:sender];
688 //If we are now an empty string, and we still have a link active, clear the link
689 if ([[self textStorage] length] == 0) {
690 [self clearLinkAttribute];
694 //Contact menu ---------------------------------------------------------------------------------------------------------
695 #pragma mark Contact menu
696 //Set and return the selected chat (to auto-configure the contact menu)
697 - (void)setChat:(AIChat *)inChat
699 if (chat != inChat) {
700 if(chat.isGroupChat) {
701 [chat removeObserver:self forKeyPath:@"Character Counter Max"];
702 [chat removeObserver:self forKeyPath:@"Character Counter Prefix"];
706 chat = [inChat retain];
708 // We only need to update our observation state for group chats.
709 if(chat.isGroupChat) {
710 [chat addObserver:self
711 forKeyPath:@"Character Counter Max"
712 options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld | NSKeyValueObservingOptionInitial)
715 [chat addObserver:self
716 forKeyPath:@"Character Counter Prefix"
717 options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld | NSKeyValueObservingOptionInitial)
721 //Observe preferences changes for typing enable/disable
722 [adium.preferenceController registerPreferenceObserver:self forGroup:GROUP_ACCOUNT_STATUS];
725 //Set up the character counter for this chat's list object.
726 //This is done regardless of a chat changing because destination changes need to trigger this.
727 if(!chat.isGroupChat) {
728 [self setCharacterCounterMaximum:[chat.listObject integerValueForProperty:@"Character Counter Max"]];
729 [self setCharacterCounterVisible:([chat.listObject valueForProperty:@"Character Counter Max"] != nil)];
730 [self setCharacterCounterPrefix:[chat.listObject valueForProperty:@"Character Counter Prefix"]];
732 [self updateCharacterCounter];
739 //Return the selected list object (to auto-configure the contact menu)
740 - (AIListContact *)listObject
742 return chat.listObject;
745 - (AIListContact *)preferredListObject
747 return [chat preferredListObject];
750 //Auto Sizing ----------------------------------------------------------------------------------------------------------
751 #pragma mark Auto-sizing
752 //Returns our desired size
753 - (NSSize)desiredSize
755 if (_desiredSizeCached.width == 0) {
757 if ([[self textStorage] length] != 0) {
758 //If there is text in this view, let the container tell us its height
760 //Force glyph generation. We must do this or usedRectForTextContainer might only return a rect for a
761 //portion of our text.
762 [[self layoutManager] glyphRangeForTextContainer:[self textContainer]];
764 textHeight = [[self layoutManager] usedRectForTextContainer:[self textContainer]].size.height;
766 //Otherwise, we use the current typing attributes to guess what the height of a line should be
767 textHeight = [NSAttributedString stringHeightForAttributes:[self typingAttributes]];
770 /* When we called glyphRangeForTextContainer, we may have triggered re-entry via
771 * -[self setFrame:] --> -[self frameDidChange:] --> -[self _resetCacheAndPostSizeChanged]
772 * in which case the second entry through the loop (the future relative to our conversation in this comment) got the correct desired size.
773 * In the present, an *old* value is in textHeight. We don't want to use that. Jumping gigawatts!
775 if (_desiredSizeCached.width == 0) {
776 _desiredSizeCached = NSMakeSize([self frame].size.width, textHeight + ENTRY_TEXTVIEW_PADDING);
780 return _desiredSizeCached;
783 //Reset the desired size cache when our frame changes
784 - (void)frameDidChange:(NSNotification *)notification
786 //resetCacheAndPostSizeChanged can get us right back to here, resulting in an infinite loop if we're not careful
789 [self _resetCacheAndPostSizeChanged];
794 //Reset the desired size cache and post a size changed notification. Call after the text's dimensions change
795 - (void)_resetCacheAndPostSizeChanged
797 //Reset the size cache
798 _desiredSizeCached = NSMakeSize(0,0);
800 //Post notification if size changed
801 if (!NSEqualSizes([self desiredSize], lastPostedSize)) {
802 lastPostedSize = [self desiredSize];
803 [[NSNotificationCenter defaultCenter] postNotificationName:AIViewDesiredSizeDidChangeNotification object:self];
807 //Paging ---------------------------------------------------------------------------------------------------------------
809 //Page up or down in the message view
810 - (void)scrollPageUp:(id)sender
812 if (associatedView && [associatedView respondsToSelector:@selector(pageUp:)]) {
813 [associatedView pageUp:nil];
815 [super scrollPageUp:sender];
818 - (void)scrollPageDown:(id)sender
820 if (associatedView && [associatedView respondsToSelector:@selector(pageDown:)]) {
821 [associatedView pageDown:nil];
823 [super scrollPageDown:sender];
828 //History --------------------------------------------------------------------------------------------------------------
830 @synthesize historyEnabled;
832 //Move up through the history
835 if (currentHistoryLocation == 0) {
836 //Store current message
837 [historyArray replaceObjectAtIndex:0 withObject:[[[self textStorage] copy] autorelease]];
840 if (currentHistoryLocation < [historyArray count]-1) {
842 currentHistoryLocation++;
845 [self setAttributedString:[historyArray objectAtIndex:currentHistoryLocation]];
849 //Move down through history
852 if (currentHistoryLocation > 0) {
854 currentHistoryLocation--;
857 [self setAttributedString:[historyArray objectAtIndex:currentHistoryLocation]];
861 //Update history when content is sent
862 - (IBAction)sendContent:(id)sender
864 NSAttributedString *textStorage = [self textStorage];
866 //Add to history if there is text being sent
867 [historyArray insertObject:[[textStorage copy] autorelease] atIndex:1];
868 if ([historyArray count] > MAX_HISTORY) {
869 [historyArray removeLastObject];
872 currentHistoryLocation = 0; //Move back to bottom of history
875 [super sendContent:sender];
877 //Clear the undo/redo stack as it makes no sense to carry between sends (the history is for that)
878 [[self undoManager] removeAllActions];
881 //Populate the history with messages from the message history
882 - (void)contentObjectAdded:(NSNotification *)notification
884 AIContentObject *content = [notification.userInfo objectForKey:@"AIContentObject"];
886 if (self.chat == content.chat && ([content.type isEqualToString:CONTENT_CONTEXT_TYPE]) && content.isOutgoing) {
887 //Populate the history with messages from us
888 [historyArray insertObject:content.message atIndex:1];
889 if (historyArray.count > MAX_HISTORY) {
890 [historyArray removeLastObject];
895 //Push and Pop ---------------------------------------------------------------------------------------------------------
896 #pragma mark Push and Pop
897 //Enable/Disable push-pop
898 - (void)setPushPopEnabled:(BOOL)inBool
900 pushPopEnabled = inBool;
903 //Push out of the message entry field
906 if ([[self textStorage] length] != 0 && pushPopEnabled) {
907 [pushArray addObject:[[[self textStorage] copy] autorelease]];
908 [self setString:@""];
909 [self _setPushIndicatorVisible:YES];
913 //Pop into the message entry field
916 if ([pushArray count] && pushPopEnabled) {
917 [self setAttributedString:[pushArray lastObject]];
918 [self setSelectedRange:NSMakeRange([[self textStorage] length], 0)]; //selection to end
919 [pushArray removeLastObject];
920 if ([pushArray count] == 0) {
921 [self _setPushIndicatorVisible:NO];
926 //Swap current content
929 if (pushPopEnabled) {
930 NSAttributedString *tempMessage = [[[self textStorage] copy] autorelease];
932 if ([pushArray count]) {
935 [self setString:@""];
938 if (tempMessage && [tempMessage length] != 0) {
939 [pushArray addObject:tempMessage];
940 [self _setPushIndicatorVisible:YES];
946 - (void)_setPushIndicatorVisible:(BOOL)visible
948 static NSImage *pushIndicatorImage = nil;
951 if (!pushIndicatorImage) pushIndicatorImage = [[NSImage imageNamed:@"stackImage" forClass:[self class]] retain];
953 if (visible && !pushIndicatorVisible) {
954 pushIndicatorVisible = visible;
956 //Push text over to make room for indicator
957 NSSize size = [self frame].size;
958 size.width -= ([pushIndicatorImage size].width);
959 [self setFrameSize:size];
961 // Make the indicator and set its action. It is a button with no border.
962 pushIndicator = [[NSButton alloc] initWithFrame:
963 NSMakeRect(0, 0, [pushIndicatorImage size].width, [pushIndicatorImage size].height)];
964 [pushIndicator setButtonType:NSMomentaryPushButton];
965 [pushIndicator setAutoresizingMask:(NSViewMinXMargin)];
966 [pushIndicator setImage:pushIndicatorImage];
967 [pushIndicator setImagePosition:NSImageOnly];
968 [pushIndicator setBezelStyle:NSRegularSquareBezelStyle];
969 [pushIndicator setBordered:NO];
970 [[self superview] addSubview:pushIndicator];
971 [pushIndicator setTarget:self];
972 [pushIndicator setAction:@selector(popContent)];
974 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(positionIndicators:) name:NSViewBoundsDidChangeNotification object:[self superview]];
975 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(positionIndicators:) name:NSViewFrameDidChangeNotification object:[self superview]];
977 [self positionPushIndicator]; //Set the indicators initial position
979 } else if (!visible && pushIndicatorVisible) {
980 pushIndicatorVisible = visible;
983 NSSize size = [self frame].size;
984 size.width += [pushIndicatorImage size].width;
985 [self setFrameSize:size];
987 //Unsubcribe, if necessary.
988 if (!characterCounter) {
989 [[NSNotificationCenter defaultCenter] removeObserver:self name:NSViewBoundsDidChangeNotification object:[self superview]];
990 [[NSNotificationCenter defaultCenter] removeObserver:self name:NSViewFrameDidChangeNotification object:[self superview]];
993 [pushIndicator removeFromSuperview];
994 [pushIndicator release]; pushIndicator = nil;
996 [self positionPushIndicator];
1000 //Reposition the push indicator into lower right corner
1001 - (void)positionPushIndicator
1003 NSRect visRect = [[self superview] bounds];
1004 NSRect indFrame = [pushIndicator frame];
1005 float counterPadding = characterCounter ? NSWidth([characterCounter frame]) : 0;
1006 [pushIndicator setFrameOrigin:NSMakePoint(NSMaxX(visRect) - NSWidth(indFrame) - INDICATOR_RIGHT_PADDING - counterPadding,
1007 NSMidY([self frame]) - NSHeight(indFrame)/2)];
1008 [[self enclosingScrollView] setNeedsDisplay:YES];
1011 #pragma mark Indicators Positioning
1014 * @brief Dispatch for both indicators to observe bounds & frame changes of their superview
1016 * Stupid that this is necessary, but you can only remove an entire object from a notification center's observer list,
1017 * not on a per-method basis.
1019 - (void)positionIndicators:(NSNotification *)notification
1021 if (pushIndicatorVisible)
1022 [self positionPushIndicator];
1023 if (characterCounter)
1024 [self positionCharacterCounter];
1027 #pragma mark Character Counter
1030 * @brief Makes the character counter for this view visible.
1032 - (void)setCharacterCounterVisible:(BOOL)visible
1034 if (visible && !characterCounter) {
1035 characterCounter = [[AISimpleTextView alloc] initWithFrame:NSZeroRect];
1036 [characterCounter setAutoresizingMask:(NSViewMinXMargin|NSViewWidthSizable)];
1038 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(positionIndicators:) name:NSViewBoundsDidChangeNotification object:[self superview]];
1039 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(positionIndicators:) name:NSViewFrameDidChangeNotification object:[self superview]];
1041 [self updateCharacterCounter];
1042 [[self superview] addSubview:characterCounter];
1044 } else if (!visible && characterCounter) {
1045 [characterCounter removeFromSuperview];
1047 // Make sure to resize this view back to the right size.
1048 NSSize size = [self frame].size;
1049 size.width += NSWidth([characterCounter frame]);
1050 [self setFrameSize:size];
1052 //Unsubscribe, if necessary.
1053 if (!pushIndicatorVisible) {
1054 [[NSNotificationCenter defaultCenter] removeObserver:self name:NSViewBoundsDidChangeNotification object:[self superview]];
1055 [[NSNotificationCenter defaultCenter] removeObserver:self name:NSViewFrameDidChangeNotification object:[self superview]];
1058 [characterCounter release];
1059 characterCounter = nil;
1061 // Reposition the push indicator, if necessary.
1062 if (pushIndicatorVisible)
1063 [self positionPushIndicator];
1065 [[self enclosingScrollView] setNeedsDisplay:YES];
1070 * @brief Set the prefix for the character count.
1072 - (void)setCharacterCounterPrefix:(NSString *)prefix
1074 if(prefix != characterCounterPrefix) {
1075 [characterCounterPrefix release];
1076 characterCounterPrefix = [prefix retain];
1081 * @brief Set the number of characters the character counter should count down from.
1083 - (void)setCharacterCounterMaximum:(int)inMaxCharacters
1085 maxCharacters = inMaxCharacters;
1087 if (characterCounter)
1088 [self updateCharacterCounter];
1092 * @brief Update the character counter and resize this view to make space if the counter's bounds change.
1094 - (void)updateCharacterCounter
1096 NSRect visRect = [[self superview] bounds];
1098 NSString *inputString = [self.chat.account encodedAttributedString:[self textStorage] forListObject:self.chat.listObject];
1099 int currentCount = (maxCharacters - [inputString length]);
1101 if(maxCharacters && currentCount < 0) {
1102 savedTextColor = [[self textColor] retain];
1104 [self setBackgroundColor:[NSColor colorWithCalibratedHue:0.983
1109 [self.enclosingScrollView setBackgroundColor:[NSColor colorWithCalibratedHue:0.983
1114 if (savedTextColor) {
1115 [self setTextColor:savedTextColor];
1116 savedTextColor = nil;
1119 [self setBackgroundColor:[NSColor controlBackgroundColor]];
1120 [self.enclosingScrollView setBackgroundColor:[NSColor controlBackgroundColor]];
1123 NSString *counterText = [NSString stringWithFormat:@"%d", currentCount];
1125 if (characterCounterPrefix) {
1126 counterText = [NSString stringWithFormat:@"%@%@", characterCounterPrefix, counterText];
1129 NSAttributedString *label = [[NSAttributedString alloc] initWithString:counterText
1130 attributes:[adium.contentController defaultFormattingAttributes]];
1131 [characterCounter setString:label];
1132 [characterCounter setFrameSize:label.size];
1135 //Reposition the character counter.
1136 [self positionCharacterCounter];
1138 //Shift the text entry view over as necessary.
1140 if (pushIndicatorVisible || characterCounter) {
1141 float pushIndicatorX = pushIndicator ? NSMinX([pushIndicator frame]) : NSMaxX([self bounds]);
1142 float characterCounterX = characterCounter ? NSMinX([characterCounter frame]) : NSMaxX([self bounds]);
1143 indent = NSWidth(visRect) - fminf(pushIndicatorX, characterCounterX);
1145 [self setFrameSize:NSMakeSize(NSWidth(visRect) - indent, NSHeight([self frame]))];
1147 //Reposition the push indicator if necessary.
1148 if (pushIndicatorVisible)
1149 [self positionPushIndicator];
1151 [[self enclosingScrollView] setNeedsDisplay:YES];
1155 * @brief Keeps the character counter in the bottom right corner.
1157 - (void)positionCharacterCounter
1159 NSRect visRect = [[self superview] bounds];
1160 NSSize counterSize = characterCounter.string.size;
1162 //NSMaxY([self frame]) is necessary because visRect's height changes after you start typing. No idea why.
1163 [characterCounter setFrameOrigin:NSMakePoint(NSMaxX(visRect) - counterSize.width - INDICATOR_RIGHT_PADDING,
1164 NSMidY([self frame]) - (counterSize.height)/2)];
1165 [characterCounter setFrameSize:counterSize];
1166 [[self enclosingScrollView] setNeedsDisplay:YES];
1169 #pragma mark List Object Observer / Chat KVO
1171 - (NSSet *)updateListObject:(AIListObject *)inObject keys:(NSSet *)inModifiedKeys silent:(BOOL)silent
1173 if ((inObject == chat.listObject) &&
1174 (!inModifiedKeys || [inModifiedKeys containsObject:@"Character Counter Max"] || [inModifiedKeys containsObject:@"Character Counter Prefix"])) {
1175 [self setCharacterCounterMaximum:[inObject integerValueForProperty:@"Character Counter Max"]];
1176 [self setCharacterCounterVisible:([inObject valueForProperty:@"Character Counter Max"] != nil)];
1177 [self setCharacterCounterPrefix:[inObject valueForProperty:@"Character Counter Prefix"]];
1179 [self updateCharacterCounter];
1185 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
1187 if(object == chat && ([keyPath isEqualToString:@"Character Counter Max"] || [keyPath isEqualToString:@"Character Counter Prefix"])) {
1188 [self setCharacterCounterMaximum:[chat integerValueForProperty:@"Character Counter Max"]];
1189 [self setCharacterCounterVisible:([chat valueForProperty:@"Character Counter Max"] != nil)];
1190 [self setCharacterCounterPrefix:[chat valueForProperty:@"Character Counter Prefix"]];
1192 [self updateCharacterCounter];
1196 #pragma mark Contextual Menus
1198 - (NSMenu *)menuForEvent:(NSEvent *)theEvent
1200 NSMenu *contextualMenu = nil;
1202 NSArray *itemsArray = nil;
1203 BOOL addedOurLinkItems = NO;
1205 if ((contextualMenu = [super menuForEvent:theEvent])) {
1206 contextualMenu = [[contextualMenu copy] autorelease];
1208 NSMenuItem *editLinkItem = nil;
1209 for (NSMenuItem *menuItem in contextualMenu.itemArray) {
1210 if ([[menuItem title] rangeOfString:AILocalizedString(@"Edit Link", nil)].location != NSNotFound) {
1211 editLinkItem = menuItem;
1217 //There was an Edit Link item. Remove it, and add out own link editing items in its place.
1218 int editIndex = [contextualMenu indexOfItem:editLinkItem];
1219 [contextualMenu removeItem:editLinkItem];
1221 NSMenu *linkItemsMenu = [adium.menuController contextualMenuWithLocations:[NSArray arrayWithObject:
1222 [NSNumber numberWithInt:Context_TextView_LinkEditing]]];
1224 for (NSMenuItem *menuItem in linkItemsMenu.itemArray) {
1225 [contextualMenu insertItem:[[menuItem copy] autorelease] atIndex:editIndex++];
1228 addedOurLinkItems = YES;
1231 contextualMenu = [[[NSMenu alloc] init] autorelease];
1234 //Retrieve the items which should be added to the bottom of the default menu
1235 NSArray *locationArray = (addedOurLinkItems ?
1236 [NSArray arrayWithObject:[NSNumber numberWithInt:Context_TextView_Edit]] :
1237 [NSArray arrayWithObjects:[NSNumber numberWithInt:Context_TextView_LinkEditing],
1238 [NSNumber numberWithInt:Context_TextView_Edit], nil]);
1239 NSMenu *adiumMenu = [adium.menuController contextualMenuWithLocations:locationArray];
1240 itemsArray = [adiumMenu itemArray];
1242 if ([itemsArray count] > 0) {
1243 [contextualMenu addItem:[NSMenuItem separatorItem]];
1244 int i = [(NSMenu *)contextualMenu numberOfItems];
1245 for (NSMenuItem *menuItem in itemsArray) {
1246 //We're going to be copying; call menu needs update now since it won't be called later.
1247 NSMenu *submenu = [menuItem submenu];
1248 NSMenuItem *menuItemCopy = [[menuItem copy] autorelease];
1249 if (submenu && [submenu respondsToSelector:@selector(delegate)]) {
1250 [[menuItemCopy submenu] setDelegate:[submenu delegate]];
1253 [contextualMenu insertItem:menuItemCopy atIndex:i++];
1257 return contextualMenu;
1260 #pragma mark Drag and drop
1262 /*An NSTextView which has setImportsGraphics:YES as of 10.5 gets the following drag types by default:
1263 "NeXT RTFD pasteboard type",
1264 "NeXT Rich Text Format v1.0 pasteboard type",
1265 "Apple HTML pasteboard type",
1266 NSFilenamesPboardType,
1267 "CorePasteboardFlavorType 0x6D6F6F76",
1268 "Apple PDF pasteboard type",
1269 "NeXT TIFF v4.0 pasteboard type",
1270 "Apple PICT pasteboard type",
1271 "NeXT Encapsulated PostScript v1.2 pasteboard type",
1272 "Apple PNG pasteboard type",
1273 WebURLsWithTitlesPboardType,
1274 "CorePasteboardFlavorType 0x75726C20",
1275 "Apple URL pasteboard type",
1277 "NSColor pasteboard type",
1278 "NeXT font pasteboard type",
1279 "NeXT ruler pasteboard type",
1282 - (NSArray *)acceptableDragTypes;
1284 NSMutableArray *dragTypes;
1286 dragTypes = [NSMutableArray arrayWithArray:[super acceptableDragTypes]];
1287 [dragTypes addObject:AIiTunesTrackPboardType];
1292 - (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender
1294 NSPasteboard *pasteboard = [sender draggingPasteboard];
1296 if ([pasteboard availableTypeFromArray:FILES_AND_IMAGES_TYPES])
1297 return NSDragOperationCopy;
1299 return [super draggingEntered:sender];
1302 - (NSDragOperation)draggingUpdated:(id <NSDraggingInfo>)sender
1304 NSPasteboard *pasteboard = [sender draggingPasteboard];
1306 if ([pasteboard availableTypeFromArray:FILES_AND_IMAGES_TYPES])
1307 return NSDragOperationCopy;
1309 return [super draggingUpdated:sender];
1312 //We don't need to prepare for the types we are handling in performDragOperation: below
1313 - (BOOL)prepareForDragOperation:(id <NSDraggingInfo>)sender
1315 NSPasteboard *pasteboard = [sender draggingPasteboard];
1316 NSString *type = [pasteboard availableTypeFromArray:FILES_AND_IMAGES_TYPES];
1317 NSString *superclassType = [pasteboard availableTypeFromArray:PASS_TO_SUPERCLASS_DRAG_TYPE_ARRAY];
1318 BOOL allowDragOperation;
1320 if (type && !superclassType) {
1321 // XXX - This shouldn't let you insert into a view for which the delegate says NO to some sort of check.
1322 allowDragOperation = YES;
1324 allowDragOperation = [super prepareForDragOperation:sender];
1327 return (allowDragOperation);
1330 //No conclusion is needed for the types we are handling in performDragOperation: below
1331 - (void)concludeDragOperation:(id <NSDraggingInfo>)sender
1333 NSPasteboard *pasteboard = [sender draggingPasteboard];
1334 NSString *type = [pasteboard availableTypeFromArray:FILES_AND_IMAGES_TYPES];
1335 NSString *superclassType = [pasteboard availableTypeFromArray:PASS_TO_SUPERCLASS_DRAG_TYPE_ARRAY];
1339 if (!type || superclassType) {
1340 [super concludeDragOperation:sender];
1344 - (void)addAttachmentsFromPasteboard:(NSPasteboard *)pasteboard
1346 NSString *availableType;
1347 if ((availableType = [pasteboard availableTypeFromArray:[NSArray arrayWithObjects:NSFilenamesPboardType, AIiTunesTrackPboardType, nil]])) {
1348 //The pasteboard points to one or more files on disc. Use them directly.
1349 NSArray *files = nil;
1350 if ([availableType isEqualToString:NSFilenamesPboardType]) {
1351 files = [pasteboard propertyListForType:NSFilenamesPboardType];
1353 } else if ([availableType isEqualToString:AIiTunesTrackPboardType]) {
1354 files = [pasteboard filesFromITunesDragPasteboard];
1358 for (path in files) {
1359 [self addAttachmentOfPath:path];
1363 //The pasteboard contains image data with no corresponding file.
1364 NSImage *image = [[NSImage alloc] initWithPasteboard:pasteboard];
1365 [self addAttachmentOfImage:image];
1370 //The textView's method of inserting into the view is insufficient; we can do better.
1371 - (BOOL)performDragOperation:(id <NSDraggingInfo>)sender
1373 NSPasteboard *pasteboard = [sender draggingPasteboard];
1376 NSString *myType = [[pasteboard types] firstObjectCommonWithArray:FILES_AND_IMAGES_TYPES];
1377 NSString *superclassType = [[pasteboard types] firstObjectCommonWithArray:PASS_TO_SUPERCLASS_DRAG_TYPE_ARRAY];
1380 (!superclassType || ([[pasteboard types] indexOfObject:myType] < [[pasteboard types] indexOfObject:superclassType]))) {
1381 [self addAttachmentsFromPasteboard:pasteboard];
1385 success = [super performDragOperation:sender];
1392 #pragma mark Spell Checking
1395 * @brief Spell checking was toggled
1397 * Set our preference, as we toggle spell checking globally when it is changed locally
1399 - (void)toggleContinuousSpellChecking:(id)sender
1401 [super toggleContinuousSpellChecking:sender];
1403 [adium.preferenceController setPreference:[NSNumber numberWithBool:[self isContinuousSpellCheckingEnabled]]
1404 forKey:KEY_SPELL_CHECKING
1405 group:PREF_GROUP_DUAL_WINDOW_INTERFACE];
1409 * @brief Grammar checking was toggled
1411 * Set our preference, as we toggle grammar checking globally when it is changed locally
1413 - (void)toggleGrammarChecking:(id)sender
1415 [super toggleGrammarChecking:sender];
1417 [adium.preferenceController setPreference:[NSNumber numberWithBool:[self isGrammarCheckingEnabled]]
1418 forKey:KEY_GRAMMAR_CHECKING
1419 group:PREF_GROUP_DUAL_WINDOW_INTERFACE];
1422 #pragma mark Substitutions
1424 * @brief Dash substitution was toggled
1426 - (void)toggleAutomaticDashSubstitution:(id)sender
1428 [super toggleAutomaticDashSubstitution:sender];
1430 [adium.preferenceController setPreference:[NSNumber numberWithBool:[self isAutomaticDashSubstitutionEnabled]]
1431 forKey:KEY_SUBSTITUTION_DASH
1432 group:PREF_GROUP_DUAL_WINDOW_INTERFACE];
1436 * @brief Data Detector substitution was toggled
1438 - (void)toggleAutomaticDataDetection:(id)sender
1440 [super toggleAutomaticDataDetection:sender];
1442 [adium.preferenceController setPreference:[NSNumber numberWithBool:[self isAutomaticDataDetectionEnabled]]
1443 forKey:KEY_SUBSTITUTION_DATA_DETECTORS
1444 group:PREF_GROUP_DUAL_WINDOW_INTERFACE];
1448 * @brief Text Replacement substitution was toggled
1450 - (void)toggleAutomaticTextReplacement:(id)sender
1452 [super toggleAutomaticTextReplacement:sender];
1454 [adium.preferenceController setPreference:[NSNumber numberWithBool:[self isAutomaticTextReplacementEnabled]]
1455 forKey:KEY_SUBSTITUTION_REPLACEMENT
1456 group:PREF_GROUP_DUAL_WINDOW_INTERFACE];
1460 * @brief Spelling replacement substitution was toggled
1462 - (void)toggleAutomaticSpellingCorrection:(id)sender
1464 [super toggleAutomaticSpellingCorrection:sender];
1466 [adium.preferenceController setPreference:[NSNumber numberWithBool:[self isAutomaticSpellingCorrectionEnabled]]
1467 forKey:KEY_SUBSTITUTION_SPELLING
1468 group:PREF_GROUP_DUAL_WINDOW_INTERFACE];
1472 * @brief Smart insert delete was toggled
1474 - (void)toggleSmartInsertDelete:(id)sender
1476 [super toggleSmartInsertDelete:sender];
1478 [adium.preferenceController setPreference:[NSNumber numberWithBool:[self smartInsertDeleteEnabled]]
1479 forKey:KEY_SUBSTITUTION_COPY_PASTE
1480 group:PREF_GROUP_DUAL_WINDOW_INTERFACE];
1484 * @brief Smart quote substitution was toggled
1486 - (void)toggleAutomaticQuoteSubstitution:(id)sender
1488 [super toggleAutomaticQuoteSubstitution:sender];
1490 [adium.preferenceController setPreference:[NSNumber numberWithBool:[self isAutomaticQuoteSubstitutionEnabled]]
1491 forKey:KEY_SUBSTITUTION_QUOTE
1492 group:PREF_GROUP_DUAL_WINDOW_INTERFACE];
1496 * @brief Smart link substitution was toggled
1498 - (void)toggleAutomaticLinkDetection:(id)sender
1500 [super toggleAutomaticLinkDetection:sender];
1502 [adium.preferenceController setPreference:[NSNumber numberWithBool:[self isAutomaticLinkDetectionEnabled]]
1503 forKey:KEY_SUBSTITUTION_LINK
1504 group:PREF_GROUP_DUAL_WINDOW_INTERFACE];
1507 #pragma mark Autocompleting
1508 - (NSRange)rangeForUserCompletion
1510 NSRange completionRange = [super rangeForUserCompletion];
1512 if ([self.delegate respondsToSelector:@selector(textView:rangeForCompletion:)]) {
1513 completionRange = [self.delegate textView:self rangeForCompletion:completionRange];
1516 return completionRange;
1519 #pragma mark Writing Direction
1520 - (void)toggleBaseWritingDirection:(id)sender
1522 if ([self baseWritingDirection] == NSWritingDirectionRightToLeft) {
1523 [self setBaseWritingDirection:NSWritingDirectionLeftToRight];
1525 [self setBaseWritingDirection:NSWritingDirectionRightToLeft];
1528 //Apply it immediately
1529 [self setBaseWritingDirection:[self baseWritingDirection]
1530 range:NSMakeRange(0, [[self textStorage] length])];
1533 #pragma mark Attachments
1535 * @brief Add an attachment of the file at inPath at the current insertion point
1537 * @param inPath The full path, whose contents will not be loaded into memory at this time
1539 - (void)addAttachmentOfPath:(NSString *)inPath
1541 if ([[inPath pathExtension] caseInsensitiveCompare:@"textClipping"] == NSOrderedSame) {
1542 inPath = [inPath stringByAppendingString:@"/..namedfork/rsrc"];
1544 NSData *data = [NSData dataWithContentsOfFile:inPath];
1546 data = [data subdataWithRange:NSMakeRange(260, [data length] - 260)];
1548 NSAttributedString *clipping = [[[NSAttributedString alloc] initWithRTF:data documentAttributes:nil] autorelease];
1550 NSDictionary *attributes = [[self typingAttributes] copy];
1552 [self insertText:clipping];
1555 [self setTypingAttributes:attributes];
1558 [attributes release];
1563 AITextAttachmentExtension *attachment = [[AITextAttachmentExtension alloc] init];
1564 [attachment setPath:inPath];
1565 [attachment setString:[inPath lastPathComponent]];
1566 [attachment setShouldSaveImageForLogging:YES];
1568 //Insert an attributed string into the text at the current insertion point
1569 [self insertText:[self attributedStringWithTextAttachmentExtension:attachment]];
1571 [attachment release];
1576 * @brief Add an attachment of inImage at the current insertion point
1578 - (void)addAttachmentOfImage:(NSImage *)inImage
1580 AITextAttachmentExtension *attachment = [[AITextAttachmentExtension alloc] init];
1582 [attachment setImage:inImage];
1583 [attachment setShouldSaveImageForLogging:YES];
1585 //Insert an attributed string into the text at the current insertion point
1586 [self insertText:[self attributedStringWithTextAttachmentExtension:attachment]];
1588 [attachment release];
1592 * @brief Generate an NSAttributedString which contains attachment and displays it using attachment's iconImage
1594 - (NSAttributedString *)attributedStringWithTextAttachmentExtension:(AITextAttachmentExtension *)attachment
1596 NSTextAttachmentCell *cell = [[NSTextAttachmentCell alloc] initImageCell:[attachment iconImage]];
1598 [attachment setHasAlternate:NO];
1599 [attachment setAttachmentCell:cell];
1602 return [NSAttributedString attributedStringWithAttachment:attachment];
1606 * @brief Given RTFD data, return an NSAttributedString whose attachments are all AITextAttachmentExtension objects
1608 - (NSAttributedString *)attributedStringWithAITextAttachmentExtensionsFromRTFDData:(NSData *)data
1610 NSMutableAttributedString *attributedString = [[[NSMutableAttributedString alloc] initWithRTFD:data
1611 documentAttributes:NULL] autorelease];
1612 if ([attributedString length] && [attributedString containsAttachments]) {
1613 int currentLocation = 0;
1614 NSRange attachmentRange;
1616 NSString *attachmentCharacterString = [NSString stringWithFormat:@"%C",NSAttachmentCharacter];
1618 //Find each attachment
1619 attachmentRange = [[attributedString string] rangeOfString:attachmentCharacterString
1621 range:NSMakeRange(currentLocation,
1622 [attributedString length] - currentLocation)];
1623 while (attachmentRange.length != 0) {
1624 //Found an attachment in at attachmentRange.location
1625 NSTextAttachment *attachment = [attributedString attribute:NSAttachmentAttributeName
1626 atIndex:attachmentRange.location
1627 effectiveRange:nil];
1629 //If it's not already an AITextAttachmentExtension, make it into one
1630 if (![attachment isKindOfClass:[AITextAttachmentExtension class]]) {
1631 NSAttributedString *replacement;
1632 NSFileWrapper *fileWrapper = [attachment fileWrapper];
1633 NSString *destinationPath;
1634 NSString *preferredName = [fileWrapper preferredFilename];
1636 //Get a unique folder within our temporary directory
1637 destinationPath = [NSTemporaryDirectory() stringByAppendingPathComponent:[[NSProcessInfo processInfo] globallyUniqueString]];
1638 [[NSFileManager defaultManager] createDirectoryAtPath:destinationPath withIntermediateDirectories:YES attributes:nil error:NULL];
1639 destinationPath = [destinationPath stringByAppendingPathComponent:preferredName];
1641 //Write the file out to it
1642 [fileWrapper writeToFile:destinationPath
1644 updateFilenames:NO];
1646 //Now create an AITextAttachmentExtension pointing to it
1647 AITextAttachmentExtension *attachment = [[AITextAttachmentExtension alloc] init];
1648 [attachment setPath:destinationPath];
1649 [attachment setString:preferredName];
1650 [attachment setShouldSaveImageForLogging:YES];
1652 //Insert an attributed string into the text at the current insertion point
1653 replacement = [self attributedStringWithTextAttachmentExtension:attachment];
1654 [attachment release];
1656 //Remove the NSTextAttachment, replacing it the AITextAttachmentExtension
1657 [attributedString replaceCharactersInRange:attachmentRange
1658 withAttributedString:replacement];
1660 attachmentRange.length = [replacement length];
1663 currentLocation = attachmentRange.location + attachmentRange.length;
1666 //Find the next attachment
1667 attachmentRange = [[attributedString string] rangeOfString:attachmentCharacterString
1669 range:NSMakeRange(currentLocation,
1670 [attributedString length] - currentLocation)];
1674 return attributedString;
1677 - (void)changeDocumentBackgroundColor:(id)sender
1679 NSColor *backgroundColor = [sender color];
1680 NSRange selectedRange = [self selectedRange];
1682 [[self textStorage] addAttribute:NSBackgroundColorAttributeName
1683 value:backgroundColor
1684 range:selectedRange];
1685 [[self textStorage] addAttribute:AIBodyColorAttributeName
1686 value:backgroundColor
1687 range:selectedRange];
1689 NSMutableDictionary *typingAttributes = [[self typingAttributes] mutableCopy];
1690 [typingAttributes setObject:backgroundColor forKey:AIBodyColorAttributeName];
1691 [typingAttributes setObject:backgroundColor forKey:NSBackgroundColorAttributeName];
1692 [self setTypingAttributes:typingAttributes];
1693 [typingAttributes release];
1695 [[self textStorage] edited:NSTextStorageEditedAttributes
1700 - (void)insertText:(id)aString
1702 [super insertText:aString];
1703 // Auto set the writing direction based on our content
1704 [self setBaseWritingDirection:[[[self textStorage] string] baseWritingDirection]];
1709 @implementation NSMutableAttributedString (AIMessageEntryTextViewAdditions)
1710 - (void)convertForPasteWithTraitsUsingAttributes:(NSDictionary *)typingAttributes;
1712 NSRange fullRange = NSMakeRange(0, [self length]);
1714 //Remove non-trait attributes
1715 if ([typingAttributes objectForKey:NSBackgroundColorAttributeName]) {
1716 [self addAttribute:NSBackgroundColorAttributeName
1717 value:[typingAttributes objectForKey:NSBackgroundColorAttributeName]
1721 [self removeAttribute:NSBackgroundColorAttributeName range:fullRange];
1724 if ([typingAttributes objectForKey:NSForegroundColorAttributeName]) {
1725 [self addAttribute:NSForegroundColorAttributeName
1726 value:[typingAttributes objectForKey:NSForegroundColorAttributeName]
1730 [self removeAttribute:NSForegroundColorAttributeName range:fullRange];
1733 if ([typingAttributes objectForKey:NSParagraphStyleAttributeName]) {
1734 [self addAttribute:NSParagraphStyleAttributeName
1735 value:[typingAttributes objectForKey:NSParagraphStyleAttributeName]
1739 [self removeAttribute:NSParagraphStyleAttributeName range:fullRange];
1742 [self removeAttribute:NSBaselineOffsetAttributeName range:fullRange];
1743 [self removeAttribute:NSCursorAttributeName range:fullRange];
1744 [self removeAttribute:NSExpansionAttributeName range:fullRange];
1745 [self removeAttribute:NSKernAttributeName range:fullRange];
1746 [self removeAttribute:NSLigatureAttributeName range:fullRange];
1747 [self removeAttribute:NSObliquenessAttributeName range:fullRange];
1748 [self removeAttribute:NSShadowAttributeName range:fullRange];
1749 [self removeAttribute:NSStrokeWidthAttributeName range:fullRange];
1751 NSRange searchRange = NSMakeRange(0, fullRange.length);
1752 NSFontManager *fontManager = [NSFontManager sharedFontManager];
1753 NSFont *myFont = [typingAttributes objectForKey:NSFontAttributeName];
1755 while (searchRange.location < fullRange.length) {
1757 NSRange effectiveRange;
1758 font = [self attribute:NSFontAttributeName
1759 atIndex:searchRange.location
1760 longestEffectiveRange:&effectiveRange
1761 inRange:searchRange];
1764 NSFontTraitMask thisFontTraits = [fontManager traitsOfFont:font];
1765 NSFontTraitMask traits = 0;
1767 if (thisFontTraits & NSBoldFontMask) {
1768 traits |= NSBoldFontMask;
1770 traits |= NSUnboldFontMask;
1773 if (thisFontTraits & NSItalicFontMask) {
1774 traits |= NSItalicFontMask;
1776 traits |= NSUnitalicFontMask;
1779 font = [fontManager fontWithFamily:[myFont familyName]
1781 weight:[fontManager weightOfFont:myFont]
1782 size:[myFont pointSize]];
1785 [self addAttribute:NSFontAttributeName
1787 range:effectiveRange];
1791 searchRange.location = effectiveRange.location + effectiveRange.length;
1792 searchRange.length = fullRange.length - searchRange.location;
1795 //Replace attachments with nothing! Absolutely nothing!
1796 [self convertAttachmentsToStringsUsingPlaceholder:@""];