Correct the tendancy of the character counter to shift positions/shrink when the message window is resized. Fixes #11898.
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>
41 #import <FriBidi/NSString-FBAdditions.h>
43 #define MAX_HISTORY 25 //Number of messages to remember in history
44 #define ENTRY_TEXTVIEW_PADDING 6 //Padding for auto-sizing
46 #define KEY_DISABLE_TYPING_NOTIFICATIONS @"Disable Typing Notifications"
48 #define KEY_SPELL_CHECKING @"Spell Checking Enabled"
49 #define KEY_GRAMMAR_CHECKING @"Grammar Checking Enabled"
50 #define PREF_GROUP_DUAL_WINDOW_INTERFACE @"Dual Window Interface"
52 #define INDICATOR_RIGHT_PADDING 2 // Padding between right side of the message view and the rightmost indicator
54 #define PREF_GROUP_CHARACTER_COUNTER @"Character Counter"
55 #define KEY_CHARACTER_COUNTER_ENABLED @"Character Counter Enabled"
56 #define KEY_MAX_NUMBER_OF_CHARACTERS @"Maximum Number Of Characters"
58 #define FILES_AND_IMAGES_TYPES [NSArray arrayWithObjects: \
59 NSFilenamesPboardType, AIiTunesTrackPboardType, NSTIFFPboardType, NSPDFPboardType, NSPICTPboardType, nil]
61 #define PASS_TO_SUPERCLASS_DRAG_TYPE_ARRAY [NSArray arrayWithObjects: \
62 NSRTFPboardType, NSStringPboardType, nil]
65 * @class AISimpleTextView
66 * @brief Just draws an attributed string. That's it.
68 * No really, it's dead simple. It just draws an attributed string in its bounds (which you set). That's it.
69 * It's totally not even useful.
72 @implementation AISimpleTextView
81 - (void)drawRect:(NSRect)rect
83 [string drawInRect:self.bounds];
87 @interface AIMessageEntryTextView ()
88 - (void)_setPushIndicatorVisible:(BOOL)visible;
89 - (void)positionPushIndicator;
90 - (void)_resetCacheAndPostSizeChanged;
92 - (NSAttributedString *)attributedStringWithAITextAttachmentExtensionsFromRTFDData:(NSData *)data;
93 - (NSAttributedString *)attributedStringWithTextAttachmentExtension:(AITextAttachmentExtension *)attachment;
94 - (void)addAttachmentOfPath:(NSString *)inPath;
95 - (void)addAttachmentOfImage:(NSImage *)inImage;
96 - (void)addAttachmentsFromPasteboard:(NSPasteboard *)pasteboard;
98 - (void)setCharacterCounterVisible:(BOOL)visible;
99 - (void)setCharacterCounterMaximum:(int)inMaxCharacters;
100 - (void)setCharacterCounterPrefix:(NSString *)prefix;
101 - (void)updateCharacterCounter;
102 - (void)positionCharacterCounter;
104 - (void)positionIndicators:(NSNotification *)notification;
107 @interface NSMutableAttributedString (AIMessageEntryTextViewAdditions)
108 - (void)convertForPasteWithTraitsUsingAttributes:(NSDictionary *)inAttributes;
111 @implementation AIMessageEntryTextView
113 - (void)_initMessageEntryTextView
115 associatedView = nil;
118 pushPopEnabled = YES;
119 historyEnabled = YES;
121 homeToStartOfLine = YES;
123 enableTypingNotifications = NO;
124 historyArray = [[NSMutableArray alloc] initWithObjects:@"",nil];
125 pushArray = [[NSMutableArray alloc] init];
126 currentHistoryLocation = 0;
127 [self setDrawsBackground:YES];
128 _desiredSizeCached = NSMakeSize(0,0);
129 characterCounter = nil;
130 characterCounterPrefix = nil;
132 savedTextColor = nil;
134 if ([self respondsToSelector:@selector(setAllowsUndo:)]) {
135 [self setAllowsUndo:YES];
137 if ([self respondsToSelector:@selector(setAllowsDocumentBackgroundColorChange:)]) {
138 [self setAllowsDocumentBackgroundColorChange:YES];
141 [self setImportsGraphics:YES];
143 [[NSNotificationCenter defaultCenter] addObserver:self
144 selector:@selector(textDidChange:)
145 name:NSTextDidChangeNotification
147 [[NSNotificationCenter defaultCenter] addObserver:self
148 selector:@selector(frameDidChange:)
149 name:NSViewFrameDidChangeNotification
151 [[NSNotificationCenter defaultCenter] addObserver:self
152 selector:@selector(toggleMessageSending:)
153 name:@"AIChatDidChangeCanSendMessagesNotification"
155 [[NSNotificationCenter defaultCenter] addObserver:self
156 selector:@selector(contentObjectAdded:)
157 name:Content_ContentObjectAdded
160 [adium.preferenceController registerPreferenceObserver:self forGroup:PREF_GROUP_DUAL_WINDOW_INTERFACE];
162 [[AIContactObserverManager sharedManager] registerListObjectObserver:self];
166 - (id)initWithFrame:(NSRect)frameRect textContainer:(NSTextContainer *)aTextContainer
168 if ((self = [super initWithFrame:frameRect textContainer:aTextContainer])) {
169 [self _initMessageEntryTextView];
175 - (id)initWithCoder:(NSCoder *)coder
177 if ((self = [super initWithCoder:coder])) {
178 [self _initMessageEntryTextView];
186 if(chat.isGroupChat) {
187 [chat removeObserver:self forKeyPath:@"Character Counter Max"];
188 [chat removeObserver:self forKeyPath:@"Character Counter Prefix"];
191 [[NSNotificationCenter defaultCenter] removeObserver:self];
192 [adium.preferenceController unregisterPreferenceObserver:self];
193 [[AIContactObserverManager sharedManager] unregisterListObjectObserver:self];
195 [savedTextColor release];
196 [characterCounter release];
197 [characterCounterPrefix release];
199 [associatedView release];
200 [historyArray release]; historyArray = nil;
201 [pushArray release]; pushArray = nil;
206 - (void) setDelegate:(id<AIMessageEntryTextViewDelegate>)del
208 super.delegate = del;
211 - (id<AIMessageEntryTextViewDelegate>)delegate
213 return super.delegate;
216 - (void)keyDown:(NSEvent *)inEvent
218 NSString *charactersIgnoringModifiers = [inEvent charactersIgnoringModifiers];
220 if ([charactersIgnoringModifiers length]) {
221 unichar inChar = [charactersIgnoringModifiers characterAtIndex:0];
222 unsigned int flags = [inEvent modifierFlags];
224 //We have to test ctrl before option, because otherwise we'd miss ctrl-option-* events
225 if (pushPopEnabled &&
226 (flags & NSControlKeyMask) && !(flags & NSShiftKeyMask)) {
227 if (inChar == NSUpArrowFunctionKey) {
229 } else if (inChar == NSDownArrowFunctionKey) {
231 } else if (inChar == 's') {
234 [super keyDown:inEvent];
237 } else if (historyEnabled &&
238 (flags & NSAlternateKeyMask) && !(flags & NSShiftKeyMask)) {
239 if (inChar == NSUpArrowFunctionKey) {
241 } else if (inChar == NSDownArrowFunctionKey) {
244 [super keyDown:inEvent];
247 } else if (associatedView &&
248 (flags & NSCommandKeyMask) && !(flags & NSShiftKeyMask)) {
249 if ((inChar == NSUpArrowFunctionKey || inChar == NSDownArrowFunctionKey) ||
250 (inChar == NSHomeFunctionKey || inChar == NSEndFunctionKey) ||
251 (inChar == NSPageUpFunctionKey || inChar == NSPageDownFunctionKey)) {
252 //Pass the associatedView a keyDown event equivalent equal to inEvent except without the modifier flags
253 [associatedView keyDown:[NSEvent keyEventWithType:[inEvent type]
254 location:[inEvent locationInWindow]
256 timestamp:[inEvent timestamp]
257 windowNumber:[inEvent windowNumber]
258 context:[inEvent context]
259 characters:[inEvent characters]
260 charactersIgnoringModifiers:charactersIgnoringModifiers
261 isARepeat:[inEvent isARepeat]
262 keyCode:[inEvent keyCode]]];
264 [super keyDown:inEvent];
267 } else if (associatedView &&
268 (inChar == NSPageUpFunctionKey || inChar == NSPageDownFunctionKey)) {
269 [associatedView keyDown:inEvent];
271 } else if (inChar == NSHomeFunctionKey || inChar == NSEndFunctionKey) {
272 if (homeToStartOfLine) {
275 if (flags & NSShiftKeyMask) {
276 //With shift, select to the beginning/end of the line
277 NSRange selectedRange = [self selectedRange];
278 if (inChar == NSHomeFunctionKey) {
279 //Home: from 0 to the current location
280 newRange.location = 0;
281 newRange.length = selectedRange.location;
283 //End: from current location to the end
284 newRange.location = selectedRange.location;
285 newRange.length = [[self string] length] - newRange.location;
289 newRange.location = ((inChar == NSHomeFunctionKey) ? 0 : [[self string] length]);
293 [self setSelectedRange:newRange];
296 //If !homeToStartOfLine, pass the keypress to our associated view.
297 if (associatedView) {
298 [associatedView keyDown:inEvent];
300 [super keyDown:inEvent];
304 } else if (inChar == NSTabCharacter) {
305 if ([self.delegate respondsToSelector:@selector(textViewShouldTabComplete:)] &&
306 [self.delegate textViewShouldTabComplete:self]) {
309 [super keyDown:inEvent];
313 [super keyDown:inEvent];
316 [super keyDown:inEvent];
321 - (void)textDidChange:(NSNotification *)notification
323 //Update typing status
324 if (enableTypingNotifications) {
325 [adium.contentController userIsTypingContentForChat:chat hasEnteredText:[[self textStorage] length] > 0];
328 //Hide any existing contact list tooltip when we begin typing
329 [adium.interfaceController showTooltipForListObject:nil atScreenPoint:NSZeroPoint onWindow:nil];
331 //Reset cache and resize
332 [self _resetCacheAndPostSizeChanged];
334 //Update the character counter
335 if (characterCounter) {
336 [self updateCharacterCounter];
341 * @brief Clear any link attribute in the current typing attributes
343 * Any link attribute is removed. All other typing attributes are unchanged.
345 - (void)clearLinkAttribute
347 NSDictionary *typingAttributes = [self typingAttributes];
349 if ([typingAttributes objectForKey:NSLinkAttributeName]) {
350 NSMutableDictionary *newTypingAttributes = [typingAttributes mutableCopy];
352 [newTypingAttributes removeObjectForKey:NSLinkAttributeName];
353 [self setTypingAttributes:newTypingAttributes];
355 [newTypingAttributes release];
360 * @brief The user pressed escape: clear our text view in response
362 - (void)cancelOperation:(id)sender
365 NSUndoManager *undoManager = [self undoManager];
366 [undoManager registerUndoWithTarget:self
367 selector:@selector(setAttributedString:)
368 object:[[[self textStorage] copy] autorelease]];
369 [undoManager setActionName:AILocalizedString(@"Clear", nil)];
371 [self setString:@""];
372 [self clearLinkAttribute];
375 if ([self.delegate respondsToSelector:@selector(textViewDidCancel:)]) {
376 [self.delegate textViewDidCancel:self];
381 //Configure ------------------------------------------------------------------------------------------------------------
382 #pragma mark Configure
383 @synthesize clearOnEscape, homeToStartOfLine, associatedView;
385 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key
386 object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
388 if ((!object || (object == chat.account)) &&
389 [group isEqualToString:GROUP_ACCOUNT_STATUS] &&
390 (!key || [key isEqualToString:KEY_DISABLE_TYPING_NOTIFICATIONS])) {
391 enableTypingNotifications = ![[chat.account preferenceForKey:KEY_DISABLE_TYPING_NOTIFICATIONS
392 group:GROUP_ACCOUNT_STATUS] boolValue];
396 [group isEqualToString:PREF_GROUP_DUAL_WINDOW_INTERFACE] &&
397 (!key || [key isEqualToString:KEY_SPELL_CHECKING])) {
398 [self setContinuousSpellCheckingEnabled:[[prefDict objectForKey:KEY_SPELL_CHECKING] boolValue]];
402 [group isEqualToString:PREF_GROUP_DUAL_WINDOW_INTERFACE] &&
403 (!key || [key isEqualToString:KEY_GRAMMAR_CHECKING])) {
404 [self setGrammarCheckingEnabled:[[prefDict objectForKey:KEY_GRAMMAR_CHECKING] boolValue]];
408 //Adium Text Entry -----------------------------------------------------------------------------------------------------
409 #pragma mark Adium Text Entry
412 * @brief Toggle whether message sending is enabled based on a notification. The notification object is the AIChat of the appropriate message entry view
414 - (void)toggleMessageSending:(NSNotification *)not
416 //XXX - We really should query the AIChat about this, but AIChat's "can't send" is really designed for handling offline, not banned. Bringing up the offline messaging dialog when banned would make no sense.
417 [self setSendingEnabled:[[[not userInfo] objectForKey:@"TypingEnabled"] boolValue]];
421 * @brief Are we available for sending?
423 - (BOOL)availableForSending
425 return self.sendingEnabled;
428 //Set our string, preserving the selected range
429 - (void)setAttributedString:(NSAttributedString *)inAttributedString
431 int length = [inAttributedString length];
432 NSRange oldRange = [self selectedRange];
435 [[self textStorage] setAttributedString:inAttributedString];
437 //Restore the old selected range
438 if (oldRange.location < length) {
439 if (oldRange.location + oldRange.length <= length) {
440 [self setSelectedRange:oldRange];
442 [self setSelectedRange:NSMakeRange(oldRange.location, length - oldRange.location)];
446 //Notify everyone that our text changed
447 [[NSNotificationCenter defaultCenter] postNotificationName:NSTextDidChangeNotification object:self];
450 //Set our string (plain text)
451 - (void)setString:(NSString *)string
453 [super setString:string];
455 //Notify everyone that our text changed
456 [[NSNotificationCenter defaultCenter] postNotificationName:NSTextDidChangeNotification object:self];
459 //Set our typing format
460 - (void)setTypingAttributes:(NSDictionary *)attrs
462 [super setTypingAttributes:attrs];
464 [self setInsertionPointColor:[[attrs objectForKey:NSBackgroundColorAttributeName] contrastingColor]];
469 - (BOOL)handlePasteAsRichText
471 NSPasteboard *generalPasteboard = [NSPasteboard generalPasteboard];
472 BOOL handledPaste = NO;
474 //Types is ordered by the preference for handling of the data; enumerating it lets us allow the sending application's hints to be followed.
475 for (NSString *type in generalPasteboard.types) {
476 if ([type isEqualToString:NSRTFDPboardType]) {
477 NSData *data = [generalPasteboard dataForType:NSRTFDPboardType];
478 [self insertText:[self attributedStringWithAITextAttachmentExtensionsFromRTFDData:data]];
481 } else if ([PASS_TO_SUPERCLASS_DRAG_TYPE_ARRAY containsObject:type]) {
482 //When we hit a type we should let the superclass handle, break without doing anything
485 } else if ([FILES_AND_IMAGES_TYPES containsObject:type]) {
486 [self addAttachmentsFromPasteboard:generalPasteboard];
490 if (handledPaste) break;
497 //Paste as rich text without altering our typing attributes
498 - (void)pasteAsRichText:(id)sender
500 NSDictionary *attributes = [[self typingAttributes] copy];
502 if (![self handlePasteAsRichText]) {
507 [self setTypingAttributes:attributes];
510 [attributes release];
512 [self scrollRangeToVisible:[self selectedRange]];
515 - (void)pasteAsPlainTextWithTraits:(id)sender
517 NSDictionary *attributes = [[self typingAttributes] copy];
519 NSPasteboard *generalPasteboard = [NSPasteboard generalPasteboard];
522 NSArray *supportedTypes =
523 [NSArray arrayWithObjects:NSURLPboardType, NSRTFDPboardType, NSRTFPboardType, NSHTMLPboardType, NSStringPboardType,
524 NSFilenamesPboardType, NSTIFFPboardType, NSPDFPboardType, NSPICTPboardType, nil];
526 type = [[NSPasteboard generalPasteboard] availableTypeFromArray:supportedTypes];
528 if ([type isEqualToString:NSRTFPboardType] ||
529 [type isEqualToString:NSRTFDPboardType] ||
530 [type isEqualToString:NSHTMLPboardType] ||
531 [type isEqualToString:NSStringPboardType]) {
535 data = [generalPasteboard dataForType:type];
536 } @catch (NSException *localException) {
540 //Failed. Try again with the string type.
541 if (!data && ![type isEqualToString:NSStringPboardType]) {
542 if ([[[NSPasteboard generalPasteboard] types] containsObject:NSStringPboardType]) {
543 type = NSStringPboardType;
545 data = [generalPasteboard dataForType:type];
546 } @catch (NSException *localException) {
553 //We still didn't get valid data... maybe super can handle it
556 } @catch (NSException *localException) {
562 NSMutableAttributedString *attributedString;
564 if ([type isEqualToString:NSStringPboardType]) {
565 NSString *string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
566 attributedString = [[NSMutableAttributedString alloc] initWithString:string
567 attributes:[self typingAttributes]];
572 if ([type isEqualToString:NSRTFPboardType]) {
573 attributedString = [[NSMutableAttributedString alloc] initWithRTF:data
574 documentAttributes:NULL];
575 } else if ([type isEqualToString:NSRTFDPboardType]) {
576 attributedString = [[NSMutableAttributedString alloc] initWithRTFD:data
577 documentAttributes:NULL];
578 } else /* NSHTMLPboardType */ {
579 attributedString = [[NSMutableAttributedString alloc] initWithHTML:data
580 documentAttributes:NULL];
582 } @catch (NSException *localException) {
583 //Error while reading the RTF or HTML data, which can happen. Fall back on plain text
584 if ([[[NSPasteboard generalPasteboard] types] containsObject:NSStringPboardType]) {
585 data = [generalPasteboard dataForType:NSStringPboardType];
586 NSString *string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
587 attributedString = [[NSMutableAttributedString alloc] initWithString:string
588 attributes:[self typingAttributes]];
591 attributedString = nil;
595 if (!attributedString) {
600 [attributedString convertForPasteWithTraitsUsingAttributes:[self typingAttributes]];
603 NSRange selectedRange = [self selectedRange];
604 NSTextStorage *textStorage = [self textStorage];
606 //Prepare the undo operation
607 NSUndoManager *undoManager = [self undoManager];
608 [[undoManager prepareWithInvocationTarget:textStorage]
609 replaceCharactersInRange:NSMakeRange(selectedRange.location, [attributedString length])
610 withAttributedString:[textStorage attributedSubstringFromRange:selectedRange]];
611 [undoManager setActionName:AILocalizedString(@"Paste", nil)];
614 [textStorage replaceCharactersInRange:selectedRange
615 withAttributedString:attributedString];
616 // Align our text properly (only need to if the first character was changed)
617 if (selectedRange.location == 0)
618 [self setBaseWritingDirection:[[textStorage string] baseWritingDirection]];
619 //Notify that we changed our text
620 [[NSNotificationCenter defaultCenter] postNotificationName:NSTextDidChangeNotification
622 [attributedString release];
624 } else if ([FILES_AND_IMAGES_TYPES containsObject:type] ||
625 [type isEqualToString:NSURLPboardType]) {
626 if (![self handlePasteAsRichText]) {
631 //If we didn't handle it yet, let super try to deal with it
636 [self setTypingAttributes:attributes];
639 [attributes release];
641 [self scrollRangeToVisible:[self selectedRange]];
644 #pragma mark Deletion
646 - (void)deleteBackward:(id)sender
649 [super deleteBackward:sender];
651 //If we are now an empty string, and we still have a link active, clear the link
652 if ([[self textStorage] length] == 0) {
653 [self clearLinkAttribute];
657 //Contact menu ---------------------------------------------------------------------------------------------------------
658 #pragma mark Contact menu
659 //Set and return the selected chat (to auto-configure the contact menu)
660 - (void)setChat:(AIChat *)inChat
662 if (chat != inChat) {
663 if(chat.isGroupChat) {
664 [chat removeObserver:self forKeyPath:@"Character Counter Max"];
665 [chat removeObserver:self forKeyPath:@"Character Counter Prefix"];
669 chat = [inChat retain];
671 // We only need to update our observation state for group chats.
672 if(chat.isGroupChat) {
673 [chat addObserver:self
674 forKeyPath:@"Character Counter Max"
675 options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld | NSKeyValueObservingOptionInitial)
678 [chat addObserver:self
679 forKeyPath:@"Character Counter Prefix"
680 options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld | NSKeyValueObservingOptionInitial)
684 //Observe preferences changes for typing enable/disable
685 [adium.preferenceController registerPreferenceObserver:self forGroup:GROUP_ACCOUNT_STATUS];
688 //Set up the character counter for this chat's list object.
689 //This is done regardless of a chat changing because destination changes need to trigger this.
690 if(!chat.isGroupChat) {
691 [self setCharacterCounterMaximum:[chat.listObject integerValueForProperty:@"Character Counter Max"]];
692 [self setCharacterCounterVisible:([chat.listObject valueForProperty:@"Character Counter Max"] != nil)];
693 [self setCharacterCounterPrefix:[chat.listObject valueForProperty:@"Character Counter Prefix"]];
695 [self updateCharacterCounter];
702 //Return the selected list object (to auto-configure the contact menu)
703 - (AIListContact *)listObject
705 return chat.listObject;
708 - (AIListContact *)preferredListObject
710 return [chat preferredListObject];
713 //Auto Sizing ----------------------------------------------------------------------------------------------------------
714 #pragma mark Auto-sizing
715 //Returns our desired size
716 - (NSSize)desiredSize
718 if (_desiredSizeCached.width == 0) {
720 if ([[self textStorage] length] != 0) {
721 //If there is text in this view, let the container tell us its height
723 //Force glyph generation. We must do this or usedRectForTextContainer might only return a rect for a
724 //portion of our text.
725 [[self layoutManager] glyphRangeForTextContainer:[self textContainer]];
727 textHeight = [[self layoutManager] usedRectForTextContainer:[self textContainer]].size.height;
729 //Otherwise, we use the current typing attributes to guess what the height of a line should be
730 textHeight = [NSAttributedString stringHeightForAttributes:[self typingAttributes]];
733 /* When we called glyphRangeForTextContainer, we may have triggered re-entry via
734 * -[self setFrame:] --> -[self frameDidChange:] --> -[self _resetCacheAndPostSizeChanged]
735 * in which case the second entry through the loop (the future relative to our conversation in this comment) got the correct desired size.
736 * In the present, an *old* value is in textHeight. We don't want to use that. Jumping gigawatts!
738 if (_desiredSizeCached.width == 0) {
739 _desiredSizeCached = NSMakeSize([self frame].size.width, textHeight + ENTRY_TEXTVIEW_PADDING);
743 return _desiredSizeCached;
746 //Reset the desired size cache when our frame changes
747 - (void)frameDidChange:(NSNotification *)notification
749 //resetCacheAndPostSizeChanged can get us right back to here, resulting in an infinite loop if we're not careful
752 [self _resetCacheAndPostSizeChanged];
757 //Reset the desired size cache and post a size changed notification. Call after the text's dimensions change
758 - (void)_resetCacheAndPostSizeChanged
760 //Reset the size cache
761 _desiredSizeCached = NSMakeSize(0,0);
763 //Post notification if size changed
764 if (!NSEqualSizes([self desiredSize], lastPostedSize)) {
765 lastPostedSize = [self desiredSize];
766 [[NSNotificationCenter defaultCenter] postNotificationName:AIViewDesiredSizeDidChangeNotification object:self];
770 //Paging ---------------------------------------------------------------------------------------------------------------
772 //Page up or down in the message view
773 - (void)scrollPageUp:(id)sender
775 if (associatedView && [associatedView respondsToSelector:@selector(pageUp:)]) {
776 [associatedView pageUp:nil];
778 [super scrollPageUp:sender];
781 - (void)scrollPageDown:(id)sender
783 if (associatedView && [associatedView respondsToSelector:@selector(pageDown:)]) {
784 [associatedView pageDown:nil];
786 [super scrollPageDown:sender];
791 //History --------------------------------------------------------------------------------------------------------------
793 @synthesize historyEnabled;
795 //Move up through the history
798 if (currentHistoryLocation == 0) {
799 //Store current message
800 [historyArray replaceObjectAtIndex:0 withObject:[[[self textStorage] copy] autorelease]];
803 if (currentHistoryLocation < [historyArray count]-1) {
805 currentHistoryLocation++;
808 [self setAttributedString:[historyArray objectAtIndex:currentHistoryLocation]];
812 //Move down through history
815 if (currentHistoryLocation > 0) {
817 currentHistoryLocation--;
820 [self setAttributedString:[historyArray objectAtIndex:currentHistoryLocation]];
824 //Update history when content is sent
825 - (IBAction)sendContent:(id)sender
827 NSAttributedString *textStorage = [self textStorage];
829 //Add to history if there is text being sent
830 [historyArray insertObject:[[textStorage copy] autorelease] atIndex:1];
831 if ([historyArray count] > MAX_HISTORY) {
832 [historyArray removeLastObject];
835 currentHistoryLocation = 0; //Move back to bottom of history
838 [super sendContent:sender];
840 //Clear the undo/redo stack as it makes no sense to carry between sends (the history is for that)
841 [[self undoManager] removeAllActions];
844 //Populate the history with messages from the message history
845 - (void)contentObjectAdded:(NSNotification *)notification
847 AIContentObject *content = [notification.userInfo objectForKey:@"AIContentObject"];
849 if (self.chat == content.chat && ([content.type isEqualToString:CONTENT_CONTEXT_TYPE]) && content.isOutgoing) {
850 //Populate the history with messages from us
851 [historyArray insertObject:content.message atIndex:1];
852 if (historyArray.count > MAX_HISTORY) {
853 [historyArray removeLastObject];
858 //Push and Pop ---------------------------------------------------------------------------------------------------------
859 #pragma mark Push and Pop
860 //Enable/Disable push-pop
861 - (void)setPushPopEnabled:(BOOL)inBool
863 pushPopEnabled = inBool;
866 //Push out of the message entry field
869 if ([[self textStorage] length] != 0 && pushPopEnabled) {
870 [pushArray addObject:[[[self textStorage] copy] autorelease]];
871 [self setString:@""];
872 [self _setPushIndicatorVisible:YES];
876 //Pop into the message entry field
879 if ([pushArray count] && pushPopEnabled) {
880 [self setAttributedString:[pushArray lastObject]];
881 [self setSelectedRange:NSMakeRange([[self textStorage] length], 0)]; //selection to end
882 [pushArray removeLastObject];
883 if ([pushArray count] == 0) {
884 [self _setPushIndicatorVisible:NO];
889 //Swap current content
892 if (pushPopEnabled) {
893 NSAttributedString *tempMessage = [[[self textStorage] copy] autorelease];
895 if ([pushArray count]) {
898 [self setString:@""];
901 if (tempMessage && [tempMessage length] != 0) {
902 [pushArray addObject:tempMessage];
903 [self _setPushIndicatorVisible:YES];
909 - (void)_setPushIndicatorVisible:(BOOL)visible
911 static NSImage *pushIndicatorImage = nil;
914 if (!pushIndicatorImage) pushIndicatorImage = [[NSImage imageNamed:@"stackImage" forClass:[self class]] retain];
916 if (visible && !pushIndicatorVisible) {
917 pushIndicatorVisible = visible;
919 //Push text over to make room for indicator
920 NSSize size = [self frame].size;
921 size.width -= ([pushIndicatorImage size].width);
922 [self setFrameSize:size];
924 // Make the indicator and set its action. It is a button with no border.
925 pushIndicator = [[NSButton alloc] initWithFrame:
926 NSMakeRect(0, 0, [pushIndicatorImage size].width, [pushIndicatorImage size].height)];
927 [pushIndicator setButtonType:NSMomentaryPushButton];
928 [pushIndicator setAutoresizingMask:(NSViewMinXMargin)];
929 [pushIndicator setImage:pushIndicatorImage];
930 [pushIndicator setImagePosition:NSImageOnly];
931 [pushIndicator setBezelStyle:NSRegularSquareBezelStyle];
932 [pushIndicator setBordered:NO];
933 [[self superview] addSubview:pushIndicator];
934 [pushIndicator setTarget:self];
935 [pushIndicator setAction:@selector(popContent)];
937 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(positionIndicators:) name:NSViewBoundsDidChangeNotification object:[self superview]];
938 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(positionIndicators:) name:NSViewFrameDidChangeNotification object:[self superview]];
940 [self positionPushIndicator]; //Set the indicators initial position
942 } else if (!visible && pushIndicatorVisible) {
943 pushIndicatorVisible = visible;
946 NSSize size = [self frame].size;
947 size.width += [pushIndicatorImage size].width;
948 [self setFrameSize:size];
950 //Unsubcribe, if necessary.
951 if (!characterCounter) {
952 [[NSNotificationCenter defaultCenter] removeObserver:self name:NSViewBoundsDidChangeNotification object:[self superview]];
953 [[NSNotificationCenter defaultCenter] removeObserver:self name:NSViewFrameDidChangeNotification object:[self superview]];
956 [pushIndicator removeFromSuperview];
957 [pushIndicator release]; pushIndicator = nil;
959 [self positionPushIndicator];
963 //Reposition the push indicator into lower right corner
964 - (void)positionPushIndicator
966 NSRect visRect = [[self superview] bounds];
967 NSRect indFrame = [pushIndicator frame];
968 float counterPadding = characterCounter ? NSWidth([characterCounter frame]) : 0;
969 [pushIndicator setFrameOrigin:NSMakePoint(NSMaxX(visRect) - NSWidth(indFrame) - INDICATOR_RIGHT_PADDING - counterPadding,
970 NSMidY([self frame]) - NSHeight(indFrame)/2)];
971 [[self enclosingScrollView] setNeedsDisplay:YES];
974 #pragma mark Indicators Positioning
977 * @brief Dispatch for both indicators to observe bounds & frame changes of their superview
979 * Stupid that this is necessary, but you can only remove an entire object from a notification center's observer list,
980 * not on a per-method basis.
982 - (void)positionIndicators:(NSNotification *)notification
984 if (pushIndicatorVisible)
985 [self positionPushIndicator];
986 if (characterCounter)
987 [self positionCharacterCounter];
990 #pragma mark Character Counter
993 * @brief Makes the character counter for this view visible.
995 - (void)setCharacterCounterVisible:(BOOL)visible
997 if (visible && !characterCounter) {
998 characterCounter = [[AISimpleTextView alloc] initWithFrame:NSZeroRect];
999 [characterCounter setAutoresizingMask:(NSViewMinXMargin|NSViewWidthSizable)];
1001 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(positionIndicators:) name:NSViewBoundsDidChangeNotification object:[self superview]];
1002 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(positionIndicators:) name:NSViewFrameDidChangeNotification object:[self superview]];
1004 [self updateCharacterCounter];
1005 [[self superview] addSubview:characterCounter];
1007 } else if (!visible && characterCounter) {
1008 [characterCounter removeFromSuperview];
1010 // Make sure to resize this view back to the right size.
1011 NSSize size = [self frame].size;
1012 size.width += NSWidth([characterCounter frame]);
1013 [self setFrameSize:size];
1015 //Unsubscribe, if necessary.
1016 if (!pushIndicatorVisible) {
1017 [[NSNotificationCenter defaultCenter] removeObserver:self name:NSViewBoundsDidChangeNotification object:[self superview]];
1018 [[NSNotificationCenter defaultCenter] removeObserver:self name:NSViewFrameDidChangeNotification object:[self superview]];
1021 [characterCounter release];
1022 characterCounter = nil;
1024 // Reposition the push indicator, if necessary.
1025 if (pushIndicatorVisible)
1026 [self positionPushIndicator];
1028 [[self enclosingScrollView] setNeedsDisplay:YES];
1033 * @brief Set the prefix for the character count.
1035 - (void)setCharacterCounterPrefix:(NSString *)prefix
1037 if(prefix != characterCounterPrefix) {
1038 [characterCounterPrefix release];
1039 characterCounterPrefix = [prefix retain];
1044 * @brief Set the number of characters the character counter should count down from.
1046 - (void)setCharacterCounterMaximum:(int)inMaxCharacters
1048 maxCharacters = inMaxCharacters;
1050 if (characterCounter)
1051 [self updateCharacterCounter];
1055 * @brief Update the character counter and resize this view to make space if the counter's bounds change.
1057 - (void)updateCharacterCounter
1059 NSRect visRect = [[self superview] bounds];
1061 NSString *inputString = [self.chat.account encodedAttributedString:[self textStorage] forListObject:self.chat.listObject];
1062 int currentCount = (maxCharacters - [inputString length]);
1064 if(maxCharacters && currentCount < 0) {
1065 savedTextColor = [[self textColor] retain];
1067 [self setBackgroundColor:[NSColor colorWithCalibratedHue:0.983
1072 [self.enclosingScrollView setBackgroundColor:[NSColor colorWithCalibratedHue:0.983
1077 if (savedTextColor) {
1078 [self setTextColor:savedTextColor];
1079 savedTextColor = nil;
1082 [self setBackgroundColor:[NSColor controlBackgroundColor]];
1083 [self.enclosingScrollView setBackgroundColor:[NSColor controlBackgroundColor]];
1086 NSString *counterText = [NSString stringWithFormat:@"%d", currentCount];
1088 if (characterCounterPrefix) {
1089 counterText = [NSString stringWithFormat:@"%@%@", characterCounterPrefix, counterText];
1092 NSAttributedString *label = [[NSAttributedString alloc] initWithString:counterText
1093 attributes:[adium.contentController defaultFormattingAttributes]];
1094 [characterCounter setString:label];
1095 [characterCounter setFrameSize:label.size];
1098 //Reposition the character counter.
1099 [self positionCharacterCounter];
1101 //Shift the text entry view over as necessary.
1103 if (pushIndicatorVisible || characterCounter) {
1104 float pushIndicatorX = pushIndicator ? NSMinX([pushIndicator frame]) : NSMaxX([self bounds]);
1105 float characterCounterX = characterCounter ? NSMinX([characterCounter frame]) : NSMaxX([self bounds]);
1106 indent = NSWidth(visRect) - fminf(pushIndicatorX, characterCounterX);
1108 [self setFrameSize:NSMakeSize(NSWidth(visRect) - indent, NSHeight([self frame]))];
1110 //Reposition the push indicator if necessary.
1111 if (pushIndicatorVisible)
1112 [self positionPushIndicator];
1114 [[self enclosingScrollView] setNeedsDisplay:YES];
1118 * @brief Keeps the character counter in the bottom right corner.
1120 - (void)positionCharacterCounter
1122 NSRect visRect = [[self superview] bounds];
1123 NSSize counterSize = characterCounter.string.size;
1125 //NSMaxY([self frame]) is necessary because visRect's height changes after you start typing. No idea why.
1126 [characterCounter setFrameOrigin:NSMakePoint(NSMaxX(visRect) - counterSize.width - INDICATOR_RIGHT_PADDING,
1127 NSMidY([self frame]) - (counterSize.height)/2)];
1128 [characterCounter setFrameSize:counterSize];
1129 [[self enclosingScrollView] setNeedsDisplay:YES];
1132 #pragma mark List Object Observer / Chat KVO
1134 - (NSSet *)updateListObject:(AIListObject *)inObject keys:(NSSet *)inModifiedKeys silent:(BOOL)silent
1136 if ((inObject == chat.listObject) &&
1137 (!inModifiedKeys || [inModifiedKeys containsObject:@"Character Counter Max"] || [inModifiedKeys containsObject:@"Character Counter Prefix"])) {
1138 [self setCharacterCounterMaximum:[inObject integerValueForProperty:@"Character Counter Max"]];
1139 [self setCharacterCounterVisible:([inObject valueForProperty:@"Character Counter Max"] != nil)];
1140 [self setCharacterCounterPrefix:[inObject valueForProperty:@"Character Counter Prefix"]];
1142 [self updateCharacterCounter];
1148 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
1150 if(object == chat && ([keyPath isEqualToString:@"Character Counter Max"] || [keyPath isEqualToString:@"Character Counter Prefix"])) {
1151 [self setCharacterCounterMaximum:[chat integerValueForProperty:@"Character Counter Max"]];
1152 [self setCharacterCounterVisible:([chat valueForProperty:@"Character Counter Max"] != nil)];
1153 [self setCharacterCounterPrefix:[chat valueForProperty:@"Character Counter Prefix"]];
1155 [self updateCharacterCounter];
1159 #pragma mark Contextual Menus
1161 - (NSMenu *)menuForEvent:(NSEvent *)theEvent
1163 NSMenu *contextualMenu = nil;
1165 NSArray *itemsArray = nil;
1166 BOOL addedOurLinkItems = NO;
1168 if ((contextualMenu = [super menuForEvent:theEvent])) {
1169 contextualMenu = [[contextualMenu copy] autorelease];
1171 NSMenuItem *editLinkItem = nil;
1172 for (NSMenuItem *menuItem in contextualMenu.itemArray) {
1173 if ([[menuItem title] rangeOfString:AILocalizedString(@"Edit Link", nil)].location != NSNotFound) {
1174 editLinkItem = menuItem;
1180 //There was an Edit Link item. Remove it, and add out own link editing items in its place.
1181 int editIndex = [contextualMenu indexOfItem:editLinkItem];
1182 [contextualMenu removeItem:editLinkItem];
1184 NSMenu *linkItemsMenu = [adium.menuController contextualMenuWithLocations:[NSArray arrayWithObject:
1185 [NSNumber numberWithInt:Context_TextView_LinkEditing]]];
1187 for (NSMenuItem *menuItem in linkItemsMenu.itemArray) {
1188 [contextualMenu insertItem:[[menuItem copy] autorelease] atIndex:editIndex++];
1191 addedOurLinkItems = YES;
1194 contextualMenu = [[[NSMenu alloc] init] autorelease];
1197 //Retrieve the items which should be added to the bottom of the default menu
1198 NSArray *locationArray = (addedOurLinkItems ?
1199 [NSArray arrayWithObject:[NSNumber numberWithInt:Context_TextView_Edit]] :
1200 [NSArray arrayWithObjects:[NSNumber numberWithInt:Context_TextView_LinkEditing],
1201 [NSNumber numberWithInt:Context_TextView_Edit], nil]);
1202 NSMenu *adiumMenu = [adium.menuController contextualMenuWithLocations:locationArray];
1203 itemsArray = [adiumMenu itemArray];
1205 if ([itemsArray count] > 0) {
1206 [contextualMenu addItem:[NSMenuItem separatorItem]];
1207 int i = [(NSMenu *)contextualMenu numberOfItems];
1208 for (NSMenuItem *menuItem in itemsArray) {
1209 //We're going to be copying; call menu needs update now since it won't be called later.
1210 NSMenu *submenu = [menuItem submenu];
1211 NSMenuItem *menuItemCopy = [[menuItem copy] autorelease];
1212 if (submenu && [submenu respondsToSelector:@selector(delegate)]) {
1213 [[menuItemCopy submenu] setDelegate:[submenu delegate]];
1216 [contextualMenu insertItem:menuItemCopy atIndex:i++];
1220 return contextualMenu;
1223 #pragma mark Drag and drop
1225 /*An NSTextView which has setImportsGraphics:YES as of 10.5 gets the following drag types by default:
1226 "NeXT RTFD pasteboard type",
1227 "NeXT Rich Text Format v1.0 pasteboard type",
1228 "Apple HTML pasteboard type",
1229 NSFilenamesPboardType,
1230 "CorePasteboardFlavorType 0x6D6F6F76",
1231 "Apple PDF pasteboard type",
1232 "NeXT TIFF v4.0 pasteboard type",
1233 "Apple PICT pasteboard type",
1234 "NeXT Encapsulated PostScript v1.2 pasteboard type",
1235 "Apple PNG pasteboard type",
1236 WebURLsWithTitlesPboardType,
1237 "CorePasteboardFlavorType 0x75726C20",
1238 "Apple URL pasteboard type",
1240 "NSColor pasteboard type",
1241 "NeXT font pasteboard type",
1242 "NeXT ruler pasteboard type",
1245 - (NSArray *)acceptableDragTypes;
1247 NSMutableArray *dragTypes;
1249 dragTypes = [NSMutableArray arrayWithArray:[super acceptableDragTypes]];
1250 [dragTypes addObject:AIiTunesTrackPboardType];
1255 - (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender
1257 NSPasteboard *pasteboard = [sender draggingPasteboard];
1259 if ([pasteboard availableTypeFromArray:FILES_AND_IMAGES_TYPES])
1260 return NSDragOperationCopy;
1262 return [super draggingEntered:sender];
1265 - (NSDragOperation)draggingUpdated:(id <NSDraggingInfo>)sender
1267 NSPasteboard *pasteboard = [sender draggingPasteboard];
1269 if ([pasteboard availableTypeFromArray:FILES_AND_IMAGES_TYPES])
1270 return NSDragOperationCopy;
1272 return [super draggingUpdated:sender];
1275 //We don't need to prepare for the types we are handling in performDragOperation: below
1276 - (BOOL)prepareForDragOperation:(id <NSDraggingInfo>)sender
1278 NSPasteboard *pasteboard = [sender draggingPasteboard];
1279 NSString *type = [pasteboard availableTypeFromArray:FILES_AND_IMAGES_TYPES];
1280 NSString *superclassType = [pasteboard availableTypeFromArray:PASS_TO_SUPERCLASS_DRAG_TYPE_ARRAY];
1281 BOOL allowDragOperation;
1283 if (type && !superclassType) {
1284 // XXX - This shouldn't let you insert into a view for which the delegate says NO to some sort of check.
1285 allowDragOperation = YES;
1287 allowDragOperation = [super prepareForDragOperation:sender];
1290 return (allowDragOperation);
1293 //No conclusion is needed for the types we are handling in performDragOperation: below
1294 - (void)concludeDragOperation:(id <NSDraggingInfo>)sender
1296 NSPasteboard *pasteboard = [sender draggingPasteboard];
1297 NSString *type = [pasteboard availableTypeFromArray:FILES_AND_IMAGES_TYPES];
1298 NSString *superclassType = [pasteboard availableTypeFromArray:PASS_TO_SUPERCLASS_DRAG_TYPE_ARRAY];
1302 if (!type || superclassType) {
1303 [super concludeDragOperation:sender];
1307 - (void)addAttachmentsFromPasteboard:(NSPasteboard *)pasteboard
1309 NSString *availableType;
1310 if ((availableType = [pasteboard availableTypeFromArray:[NSArray arrayWithObjects:NSFilenamesPboardType, AIiTunesTrackPboardType, nil]])) {
1311 //The pasteboard points to one or more files on disc. Use them directly.
1312 NSArray *files = nil;
1313 if ([availableType isEqualToString:NSFilenamesPboardType]) {
1314 files = [pasteboard propertyListForType:NSFilenamesPboardType];
1316 } else if ([availableType isEqualToString:AIiTunesTrackPboardType]) {
1317 files = [pasteboard filesFromITunesDragPasteboard];
1321 for (path in files) {
1322 [self addAttachmentOfPath:path];
1326 //The pasteboard contains image data with no corresponding file.
1327 NSImage *image = [[NSImage alloc] initWithPasteboard:pasteboard];
1328 [self addAttachmentOfImage:image];
1333 //The textView's method of inserting into the view is insufficient; we can do better.
1334 - (BOOL)performDragOperation:(id <NSDraggingInfo>)sender
1336 NSPasteboard *pasteboard = [sender draggingPasteboard];
1339 NSString *myType = [[pasteboard types] firstObjectCommonWithArray:FILES_AND_IMAGES_TYPES];
1340 NSString *superclassType = [[pasteboard types] firstObjectCommonWithArray:PASS_TO_SUPERCLASS_DRAG_TYPE_ARRAY];
1343 (!superclassType || ([[pasteboard types] indexOfObject:myType] < [[pasteboard types] indexOfObject:superclassType]))) {
1344 [self addAttachmentsFromPasteboard:pasteboard];
1348 success = [super performDragOperation:sender];
1355 #pragma mark Spell Checking
1358 * @brief Spell checking was toggled
1360 * Set our preference, as we toggle spell checking globally when it is changed locally
1362 - (void)toggleContinuousSpellChecking:(id)sender
1364 [super toggleContinuousSpellChecking:sender];
1366 [adium.preferenceController setPreference:[NSNumber numberWithBool:[self isContinuousSpellCheckingEnabled]]
1367 forKey:KEY_SPELL_CHECKING
1368 group:PREF_GROUP_DUAL_WINDOW_INTERFACE];
1372 * @brief Grammar checking was toggled
1374 * Set our preference, as we toggle grammar checking globally when it is changed locally
1376 - (void)toggleGrammarChecking:(id)sender
1378 [super toggleGrammarChecking:sender];
1380 [adium.preferenceController setPreference:[NSNumber numberWithBool:[self isGrammarCheckingEnabled]]
1381 forKey:KEY_GRAMMAR_CHECKING
1382 group:PREF_GROUP_DUAL_WINDOW_INTERFACE];
1386 #pragma mark Writing Direction
1387 - (void)toggleBaseWritingDirection:(id)sender
1389 if ([self baseWritingDirection] == NSWritingDirectionRightToLeft) {
1390 [self setBaseWritingDirection:NSWritingDirectionLeftToRight];
1392 [self setBaseWritingDirection:NSWritingDirectionRightToLeft];
1395 //Apply it immediately
1396 [self setBaseWritingDirection:[self baseWritingDirection]
1397 range:NSMakeRange(0, [[self textStorage] length])];
1400 #pragma mark Attachments
1402 * @brief Add an attachment of the file at inPath at the current insertion point
1404 * @param inPath The full path, whose contents will not be loaded into memory at this time
1406 - (void)addAttachmentOfPath:(NSString *)inPath
1408 if ([[inPath pathExtension] caseInsensitiveCompare:@"textClipping"] == NSOrderedSame) {
1409 inPath = [inPath stringByAppendingString:@"/..namedfork/rsrc"];
1411 NSData *data = [NSData dataWithContentsOfFile:inPath];
1413 data = [data subdataWithRange:NSMakeRange(260, [data length] - 260)];
1415 NSAttributedString *clipping = [[[NSAttributedString alloc] initWithRTF:data documentAttributes:nil] autorelease];
1417 NSDictionary *attributes = [[self typingAttributes] copy];
1419 [self insertText:clipping];
1422 [self setTypingAttributes:attributes];
1425 [attributes release];
1430 AITextAttachmentExtension *attachment = [[AITextAttachmentExtension alloc] init];
1431 [attachment setPath:inPath];
1432 [attachment setString:[inPath lastPathComponent]];
1433 [attachment setShouldSaveImageForLogging:YES];
1435 //Insert an attributed string into the text at the current insertion point
1436 [self insertText:[self attributedStringWithTextAttachmentExtension:attachment]];
1438 [attachment release];
1443 * @brief Add an attachment of inImage at the current insertion point
1445 - (void)addAttachmentOfImage:(NSImage *)inImage
1447 AITextAttachmentExtension *attachment = [[AITextAttachmentExtension alloc] init];
1449 [attachment setImage:inImage];
1450 [attachment setShouldSaveImageForLogging:YES];
1452 //Insert an attributed string into the text at the current insertion point
1453 [self insertText:[self attributedStringWithTextAttachmentExtension:attachment]];
1455 [attachment release];
1459 * @brief Generate an NSAttributedString which contains attachment and displays it using attachment's iconImage
1461 - (NSAttributedString *)attributedStringWithTextAttachmentExtension:(AITextAttachmentExtension *)attachment
1463 NSTextAttachmentCell *cell = [[NSTextAttachmentCell alloc] initImageCell:[attachment iconImage]];
1465 [attachment setHasAlternate:NO];
1466 [attachment setAttachmentCell:cell];
1469 return [NSAttributedString attributedStringWithAttachment:attachment];
1473 * @brief Given RTFD data, return an NSAttributedString whose attachments are all AITextAttachmentExtension objects
1475 - (NSAttributedString *)attributedStringWithAITextAttachmentExtensionsFromRTFDData:(NSData *)data
1477 NSMutableAttributedString *attributedString = [[[NSMutableAttributedString alloc] initWithRTFD:data
1478 documentAttributes:NULL] autorelease];
1479 if ([attributedString length] && [attributedString containsAttachments]) {
1480 int currentLocation = 0;
1481 NSRange attachmentRange;
1483 NSString *attachmentCharacterString = [NSString stringWithFormat:@"%C",NSAttachmentCharacter];
1485 //Find each attachment
1486 attachmentRange = [[attributedString string] rangeOfString:attachmentCharacterString
1488 range:NSMakeRange(currentLocation,
1489 [attributedString length] - currentLocation)];
1490 while (attachmentRange.length != 0) {
1491 //Found an attachment in at attachmentRange.location
1492 NSTextAttachment *attachment = [attributedString attribute:NSAttachmentAttributeName
1493 atIndex:attachmentRange.location
1494 effectiveRange:nil];
1496 //If it's not already an AITextAttachmentExtension, make it into one
1497 if (![attachment isKindOfClass:[AITextAttachmentExtension class]]) {
1498 NSAttributedString *replacement;
1499 NSFileWrapper *fileWrapper = [attachment fileWrapper];
1500 NSString *destinationPath;
1501 NSString *preferredName = [fileWrapper preferredFilename];
1503 //Get a unique folder within our temporary directory
1504 destinationPath = [NSTemporaryDirectory() stringByAppendingPathComponent:[[NSProcessInfo processInfo] globallyUniqueString]];
1505 [[NSFileManager defaultManager] createDirectoryAtPath:destinationPath withIntermediateDirectories:YES attributes:nil error:NULL];
1506 destinationPath = [destinationPath stringByAppendingPathComponent:preferredName];
1508 //Write the file out to it
1509 [fileWrapper writeToFile:destinationPath
1511 updateFilenames:NO];
1513 //Now create an AITextAttachmentExtension pointing to it
1514 AITextAttachmentExtension *attachment = [[AITextAttachmentExtension alloc] init];
1515 [attachment setPath:destinationPath];
1516 [attachment setString:preferredName];
1517 [attachment setShouldSaveImageForLogging:YES];
1519 //Insert an attributed string into the text at the current insertion point
1520 replacement = [self attributedStringWithTextAttachmentExtension:attachment];
1521 [attachment release];
1523 //Remove the NSTextAttachment, replacing it the AITextAttachmentExtension
1524 [attributedString replaceCharactersInRange:attachmentRange
1525 withAttributedString:replacement];
1527 attachmentRange.length = [replacement length];
1530 currentLocation = attachmentRange.location + attachmentRange.length;
1533 //Find the next attachment
1534 attachmentRange = [[attributedString string] rangeOfString:attachmentCharacterString
1536 range:NSMakeRange(currentLocation,
1537 [attributedString length] - currentLocation)];
1541 return attributedString;
1544 - (void)changeDocumentBackgroundColor:(id)sender
1546 NSColor *backgroundColor = [sender color];
1547 NSRange selectedRange = [self selectedRange];
1549 [[self textStorage] addAttribute:NSBackgroundColorAttributeName
1550 value:backgroundColor
1551 range:selectedRange];
1552 [[self textStorage] addAttribute:AIBodyColorAttributeName
1553 value:backgroundColor
1554 range:selectedRange];
1556 NSMutableDictionary *typingAttributes = [[self typingAttributes] mutableCopy];
1557 [typingAttributes setObject:backgroundColor forKey:AIBodyColorAttributeName];
1558 [typingAttributes setObject:backgroundColor forKey:NSBackgroundColorAttributeName];
1559 [self setTypingAttributes:typingAttributes];
1560 [typingAttributes release];
1562 [[self textStorage] edited:NSTextStorageEditedAttributes
1567 - (void)insertText:(id)aString
1569 [super insertText:aString];
1570 // Auto set the writing direction based on our content
1571 [self setBaseWritingDirection:[[[self textStorage] string] baseWritingDirection]];
1576 @implementation NSMutableAttributedString (AIMessageEntryTextViewAdditions)
1577 - (void)convertForPasteWithTraitsUsingAttributes:(NSDictionary *)typingAttributes;
1579 NSRange fullRange = NSMakeRange(0, [self length]);
1581 //Remove non-trait attributes
1582 if ([typingAttributes objectForKey:NSBackgroundColorAttributeName]) {
1583 [self addAttribute:NSBackgroundColorAttributeName
1584 value:[typingAttributes objectForKey:NSBackgroundColorAttributeName]
1588 [self removeAttribute:NSBackgroundColorAttributeName range:fullRange];
1591 if ([typingAttributes objectForKey:NSForegroundColorAttributeName]) {
1592 [self addAttribute:NSForegroundColorAttributeName
1593 value:[typingAttributes objectForKey:NSForegroundColorAttributeName]
1597 [self removeAttribute:NSForegroundColorAttributeName range:fullRange];
1600 if ([typingAttributes objectForKey:NSParagraphStyleAttributeName]) {
1601 [self addAttribute:NSParagraphStyleAttributeName
1602 value:[typingAttributes objectForKey:NSParagraphStyleAttributeName]
1606 [self removeAttribute:NSParagraphStyleAttributeName range:fullRange];
1609 [self removeAttribute:NSBaselineOffsetAttributeName range:fullRange];
1610 [self removeAttribute:NSCursorAttributeName range:fullRange];
1611 [self removeAttribute:NSExpansionAttributeName range:fullRange];
1612 [self removeAttribute:NSKernAttributeName range:fullRange];
1613 [self removeAttribute:NSLigatureAttributeName range:fullRange];
1614 [self removeAttribute:NSObliquenessAttributeName range:fullRange];
1615 [self removeAttribute:NSShadowAttributeName range:fullRange];
1616 [self removeAttribute:NSStrokeWidthAttributeName range:fullRange];
1618 NSRange searchRange = NSMakeRange(0, fullRange.length);
1619 NSFontManager *fontManager = [NSFontManager sharedFontManager];
1620 NSFont *myFont = [typingAttributes objectForKey:NSFontAttributeName];
1622 while (searchRange.location < fullRange.length) {
1624 NSRange effectiveRange;
1625 font = [self attribute:NSFontAttributeName
1626 atIndex:searchRange.location
1627 longestEffectiveRange:&effectiveRange
1628 inRange:searchRange];
1631 NSFontTraitMask thisFontTraits = [fontManager traitsOfFont:font];
1632 NSFontTraitMask traits = 0;
1634 if (thisFontTraits & NSBoldFontMask) {
1635 traits |= NSBoldFontMask;
1637 traits |= NSUnboldFontMask;
1640 if (thisFontTraits & NSItalicFontMask) {
1641 traits |= NSItalicFontMask;
1643 traits |= NSUnitalicFontMask;
1646 font = [fontManager fontWithFamily:[myFont familyName]
1648 weight:[fontManager weightOfFont:myFont]
1649 size:[myFont pointSize]];
1652 [self addAttribute:NSFontAttributeName
1654 range:effectiveRange];
1658 searchRange.location = effectiveRange.location + effectiveRange.length;
1659 searchRange.length = fullRange.length - searchRange.location;
1662 //Replace attachments with nothing! Absolutely nothing!
1663 [self convertAttachmentsToStringsUsingPlaceholder:@""];