/** NSTextView Copyright (C) 1996, 1998, 2000, 2001, 2002, 2003, 2008, 2020 Free Software Foundation, Inc. Much code of this class was originally derived from code which was in NSText.m. Author: Scott Christley Date: 1996 Author: Felipe A. Rodriguez Date: July 1998 Author: Daniel Boehringer Date: August 1998 Author: Fred Kiefer Date: March 2000, September 2000, January 2008 Author: Nicola Pero Date: 2000, 2001, 2002 Author: Pierre-Yves Rivaille Date: September 2002 Extensive reworking: Alexander Malmberg Date: December 2002 - February 2003 Implementing Catalina Extensions: Gregory Casamento Date: August 2020 This file is part of the GNUstep GUI Library. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; see the file COPYING.LIB. If not, see or write to the Free Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #import "config.h" #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import "AppKit/NSApplication.h" #import "AppKit/NSAttributedString.h" #import "AppKit/NSClipView.h" #import "AppKit/NSColor.h" #import "AppKit/NSColorPanel.h" #import "AppKit/NSControl.h" #import "AppKit/NSCursor.h" #import "AppKit/NSDragging.h" #import "AppKit/NSEvent.h" #import "AppKit/NSFileWrapper.h" #import "AppKit/NSFileWrapperExtensions.h" #import "AppKit/NSGraphics.h" #import "AppKit/NSImage.h" #import "AppKit/NSKeyValueBinding.h" #import "AppKit/NSLayoutManager.h" #import "AppKit/NSMenu.h" #import "AppKit/NSMenuItem.h" #import "AppKit/NSParagraphStyle.h" #import "AppKit/NSPasteboard.h" #import "AppKit/NSRulerMarker.h" #import "AppKit/NSRulerView.h" #import "AppKit/NSScrollView.h" #import "AppKit/NSSpellChecker.h" #import "AppKit/NSTextAttachment.h" #import "AppKit/NSTextContainer.h" #import "AppKit/NSTextStorage.h" #import "AppKit/NSTextView.h" #import "AppKit/NSWindow.h" #import "GSGuiPrivate.h" #import "GSTextFinder.h" #import "GSToolTips.h" #import "GSFastEnumeration.h" #import "GSAutocompleteWindow.h" /* NSTextView_actions.m has comments about what methods dealing with user actions and user-initiated changes need to do. These also apply to some methods in this file. */ /* TODOs: think hard about insertion point management How should resizing work? If the text view is set to track the used part of the text container, when does it actually update its size? Might need a new internal method called from NSLayoutManager when text has changed. (Currently NSLayoutManager calls -sizeToFit when text has changed.) */ @interface NSTextViewUndoObject : NSObject { NSRange range; NSAttributedString *string; } - (id) initWithRange: (NSRange)aRange attributedString: (NSAttributedString *)aString; - (NSRange) range; - (void) setRange: (NSRange)aRange; - (void) performUndo: (NSTextStorage *)aTextStorage; - (NSTextView *) bestTextViewForTextStorage: (NSTextStorage *)aTextStorage; @end /* Interface for a bunch of internal methods that need to be cleaned up. */ @interface NSTextView (GNUstepPrivate) - (NSUInteger) _characterIndexForPoint: (NSPoint)point respectFraction: (BOOL)respectFraction; /* * Used to implement the blinking insertion point */ - (void) _blink: (NSTimer *)t; - (void) _stopInsertionTimer; - (void) _startInsertionTimer; /* * these NSLayoutManager- like method is here only informally */ - (NSRect) rectForCharacterRange: (NSRange)aRange; // // GNU utility methods // - (void) copySelection; - (void) pasteSelection; /* * Text checking */ - (void) _scheduleTextCheckingInVisibleRectIfNeeded; - (void) _textDidChange: (NSNotification*)notif; - (void) _textCheckingTimerFired: (NSTimer *)t; /* * helper method for ruler view */ - (void) _becomeRulerClient; - (void) _resignRulerClient; @end // This class is a helper for keyed unarchiving only @interface NSTextViewSharedData : NSObject { @private NSColor *backgroundColor; NSParagraphStyle *paragraphStyle; unsigned int flags; NSColor *insertionColor; NSDictionary *linkAttr; NSDictionary *markAttr; NSDictionary *selectedAttr; NSTextView *textView; } - (NSColor *) backgroundColor; - (NSParagraphStyle *) paragraphStyle; - (unsigned int) flags; - (NSColor *) insertionColor; - (NSDictionary *) linkAttributes; - (NSDictionary *) markAttributes; - (NSDictionary *) selectedAttributes; - (NSTextView *) textView; @end @implementation NSTextViewSharedData - (id) initWithTextView: (NSTextView *)tv { if ((self = [super init]) != nil) { flags = (([tv isSelectable]?0x01:0) | ([tv isEditable]?0x02:0) | ([tv isRichText]?0x04:0) | ([tv importsGraphics]?0x08:0) | ([tv isFieldEditor]?0x10:0) | ([tv usesFontPanel]?0x20:0) | ([tv isRulerVisible]?0x40:0) | ([tv isContinuousSpellCheckingEnabled]?0x80:0) | ([tv usesRuler]?0x100:0) | ([tv smartInsertDeleteEnabled]?0x200:0) | ([tv allowsUndo]?0x400:0) | ([tv drawsBackground]?0x800:0) | ([tv usesFindPanel]?0x2000:0)); ASSIGN(backgroundColor, [tv backgroundColor]); ASSIGN(paragraphStyle, [tv defaultParagraphStyle]); ASSIGN(insertionColor, [tv insertionPointColor]); ASSIGN(markAttr, [tv markedTextAttributes]); ASSIGN(linkAttr, [tv linkTextAttributes]); ASSIGN(selectedAttr, [tv selectedTextAttributes]); linkAttr = nil; textView = tv; } return self; } - (id) initWithCoder: (NSCoder*)aDecoder { if ([aDecoder allowsKeyedCoding]) { ASSIGN(backgroundColor, [aDecoder decodeObjectForKey: @"NSBackgroundColor"]); ASSIGN(paragraphStyle, [aDecoder decodeObjectForKey: @"NSDefaultParagraphStyle"]); flags = [aDecoder decodeIntForKey: @"NSFlags"]; ASSIGN(insertionColor, [aDecoder decodeObjectForKey: @"NSInsertionColor"]); ASSIGN(linkAttr, [aDecoder decodeObjectForKey: @"NSLinkAttributes"]); ASSIGN(markAttr, [aDecoder decodeObjectForKey: @"NSMarkedAttributes"]); ASSIGN(selectedAttr, [aDecoder decodeObjectForKey: @"NSSelectedAttributes"]); } return self; } - (void) encodeWithCoder: (NSCoder *)coder { if ([coder allowsKeyedCoding]) { [coder encodeObject: backgroundColor forKey: @"NSBackgroundColor"]; [coder encodeObject: paragraphStyle forKey: @"NSDefaultParagraphStyle"]; [coder encodeInt: flags forKey: @"NSFlags"]; [coder encodeObject: insertionColor forKey: @"NSInsertionColor"]; [coder encodeObject: linkAttr forKey: @"NSLinkAttributes"]; [coder encodeObject: markAttr forKey: @"NSMarkedAttributes"]; [coder encodeObject: selectedAttr forKey: @"NSSelectedAttributes"]; } } - (void) dealloc { RELEASE(backgroundColor); RELEASE(paragraphStyle); RELEASE(insertionColor); RELEASE(linkAttr); RELEASE(markAttr); RELEASE(selectedAttr); [super dealloc]; } - (NSColor *)backgroundColor { return backgroundColor; } - (NSParagraphStyle *) paragraphStyle { return paragraphStyle; } - (unsigned int) flags { return flags; } - (NSColor *) insertionColor { return insertionColor; } - (NSDictionary *) linkAttributes { return linkAttr; } - (NSDictionary *) markAttributes { return markAttr; } - (NSDictionary *) selectedAttributes { return selectedAttr; } - (NSTextView *) textView { return textView; } @end @interface NSTextStorage(NSTextViewUndoSupport) - (void) _undoTextChange: (NSTextViewUndoObject *)anObject; @end /**** Misc. helpers and stuff ****/ static const int currentVersion = 4; static BOOL noLayoutManagerException(void) { [NSException raise: NSGenericException format: @"Can't edit a NSTextView without a layout manager!"]; return YES; } /* The shared notification center */ static NSNotificationCenter *notificationCenter; /* Default max. size. Don't change this without understanding and checking for roundoff issues. Whole points should be representable. */ #define GSHUGE 1e7 /**** Synchronization stuff ****/ /* For when more than one text view is connected to a layout manager. Helpers are here, the actual methods are in the main implementation. */ @implementation NSTextView (GSTextViewSync) #define SET_DELEGATE_NOTIFICATION(notif_name) \ if ([_delegate respondsToSelector: @selector(text##notif_name:)]) \ [notificationCenter addObserver: _delegate \ selector: @selector(text##notif_name:) \ name: NSText##notif_name##Notification \ object: _notifObject] /* * Synchronizing flags. Used to manage synchronizing shared * attributes between textviews coupled with the same layout manager. * These synchronizing flags are only accessed when * _tf.multiple_textviews == YES and this can only happen if we have * a non-nil NSLayoutManager - so we don't check. */ /* YES when in the process of synchronizing text view attributes. Used to avoid recursive synchronizations. */ #define IS_SYNCHRONIZING_FLAGS _layoutManager->_isSynchronizingFlags /* YES when in the process of synchronizing delegates. Used to avoid recursive synchronizations. */ #define IS_SYNCHRONIZING_DELEGATES _layoutManager->_isSynchronizingDelegates /* This gets sent to all text views connected to a layout manager whenever a text container is added or removed, or if a text container changes text view. When a text container is removed, it is also sent to the text view of the removed text view. It is also sent by -setTextContainer:. This will be sent several times to the same text view for one change in some cases, so it needs to be safe wrt. that. */ - (void) _updateMultipleTextViews { id oldNotifObject = _notifObject; if ([[_layoutManager textContainers] count] > 1) { _tf.multiple_textviews = YES; _notifObject = [_layoutManager firstTextView]; } else { _tf.multiple_textviews = NO; _notifObject = self; } /* The notifications only need to be reset once for each change, but doing it several times doesn't hurt. */ if ((_delegate != nil) && (oldNotifObject != _notifObject)) { [notificationCenter removeObserver: _delegate name: nil object: oldNotifObject]; if ([_delegate respondsToSelector: @selector(textView:shouldChangeTextInRange:replacementString:)]) { _tf.delegate_responds_to_should_change = YES; } else { _tf.delegate_responds_to_should_change = NO; } /* SET_DELEGATE_NOTIFICATION defined at the beginning of file */ /* NSText notifications */ SET_DELEGATE_NOTIFICATION(DidBeginEditing); SET_DELEGATE_NOTIFICATION(DidChange); SET_DELEGATE_NOTIFICATION(DidEndEditing); /* NSTextView notifications */ SET_DELEGATE_NOTIFICATION(ViewDidChangeSelection); SET_DELEGATE_NOTIFICATION(ViewWillChangeNotifyingTextView); SET_DELEGATE_NOTIFICATION(ViewDidChangeTypingAttributes); } } /* _syncTextViewsByCalling:withFlag: calls a set method on all text views sharing the same layout manager as this one. It sets the IS_SYNCHRONIZING_FLAGS flag to YES to prevent recursive calls; calls the specified action on all the textviews (this one included) with the specified flag; sets back the IS_SYNCHRONIZING_FLAGS flag to NO; then returns. We need to explicitly call the methods - we can't copy the flags directly from one textview to another - to allow subclasses to override eg. -setEditable: to take some particular action when editing is turned on or off. */ - (void) _syncTextViewsByCalling: (SEL)action withFlag: (BOOL)flag { NSArray *array; NSUInteger i, count; if (IS_SYNCHRONIZING_FLAGS == YES) { [NSException raise: NSGenericException format: @"_syncTextViewsByCalling:withFlag: called recursively"]; } array = [_layoutManager textContainers]; count = [array count]; IS_SYNCHRONIZING_FLAGS = YES; for (i = 0; i < count; i++) { NSTextView *tv; void (*msg)(id, SEL, BOOL); tv = [(NSTextContainer *)[array objectAtIndex: i] textView]; msg = (void (*)(id, SEL, BOOL))[tv methodForSelector: action]; if (msg != NULL) { (*msg) (tv, action, flag); } else { /* Ahm. What shall we do here. It can't happen, can it ? */ NSLog(@"Weird error - _syncTextViewsByCalling:withFlag: couldn't find method for selector"); } } IS_SYNCHRONIZING_FLAGS = NO; } /* This must be called whenever the delegate changes (directly, through calls to -setDelegate:, or indirectly due to changes in the text system, or being decoded. */ - (void) _recacheDelegateResponses { SEL selector; selector = @selector(textView:shouldChangeTextInRange:replacementString:); if ([_delegate respondsToSelector: selector]) { _tf.delegate_responds_to_should_change = YES; } else { _tf.delegate_responds_to_should_change = NO; } selector = @selector(textView:willChangeSelectionFromCharacterRange:toCharacterRange:); if ([_delegate respondsToSelector: selector]) { _tf.delegate_responds_to_will_change_sel = YES; } else { _tf.delegate_responds_to_will_change_sel = NO; } } /* Called when our state needs updating due to external changes. Currently, this happens when layout has been invalidated, and when we are resized. */ - (void) _updateState: (id)sender { [self sizeToFit]; /* TODO: we don't have to redisplay the entire view */ [self setNeedsDisplay: YES]; [self updateInsertionPointStateAndRestartTimer: [self shouldDrawInsertionPoint]]; [self _updateInputMethodState]; /* In case any sections of text with custom cursors were moved */ [[self window] invalidateCursorRectsForView: self]; } - (void) _layoutManagerDidInvalidateLayout { /* We don't want to do the update right away, since the invalidation might be followed by further invalidation if the user of the text system is making large rearrangements of the text network. This also makes us a lot more forgiving; no real works is done until "later", so the user can have the text network in an inconsistent state for short periods of time. (This shouldn't be relied on, though. If possible, avoid it.) Actually, we want to run as late as possible. However, it seems to me that we must run before [NSWindow -_handleWindowNeedsDisplay:] (or that it would be a very good idea to do so, since we might end up resizing ourselves and stuff like that). Thus, we use -performSelector:... with an order 1 under that for window updating. If the update method is not called before the real redisplay (if someone forces a redisplay right away), we might draw incorrectly, but it shouldn't cause any other harm. */ [[NSRunLoop currentRunLoop] cancelPerformSelector: @selector(_updateState:) target: self argument: nil]; [[NSRunLoop currentRunLoop] performSelector: @selector(_updateState:) target: self argument: nil order: 599999 modes: [NSArray arrayWithObjects: /* TODO: set up array in advance */ NSDefaultRunLoopMode, NSModalPanelRunLoopMode, NSEventTrackingRunLoopMode, nil]]; } @end @interface NSUndoManager(UndoCoalescing) /* Auxiliary method to support coalescing undo actions for typing events in NSTextView. However, the implementation is not restricted to that purpose. */ - (BOOL) _canCoalesceUndoWithTarget: (id)target selector: (SEL)aSelector object: (id)anObject; @end @implementation NSTextView /* The sets smartLeftChars and smartRightChars are used for smart insert and delete. If users paste text into a text view and the character to the left (right) side of the new word is not in the set smartLeftChars (smartRightChars), an extra space is added to that side. Inversely, if users delete a word followed (preceded) by a character in smartRightChars (smartLeftChars), a space character on the other side of the word is deleted as well. */ static NSCharacterSet *smartLeftChars; static NSCharacterSet *smartRightChars; /* Private pasteboard type for adjoining smart paste information with text on the pasteboard. */ static NSString *NSSmartPastePboardType = @"NSSmartPastePboardType"; static NSMenu *textViewMenu; /**** Misc. class methods ****/ +(void) initialize { if ([self class] == [NSTextView class]) { NSMutableCharacterSet *temp; [self setVersion: currentVersion]; notificationCenter = [NSNotificationCenter defaultCenter]; /* The sets smartLeftChars and smartRightChars were derived from OS X. */ temp = [[NSCharacterSet whitespaceAndNewlineCharacterSet] mutableCopy]; [temp addCharactersInString: @"\"#$\'(-/[`{"]; smartLeftChars = [temp copy]; [temp formUnionWithCharacterSet: [NSCharacterSet punctuationCharacterSet]]; smartRightChars = [temp copy]; [temp release]; [self exposeBinding: NSEditableBinding]; } } static BOOL did_register_for_services; +(void) registerForServices { NSArray *types; did_register_for_services = YES; types = [NSArray arrayWithObjects: NSStringPboardType, NSRTFPboardType, NSRTFDPboardType, nil]; NSAssert(NSApp, @"Called before the shared application object was created."); [NSApp registerServicesMenuSendTypes: types returnTypes: types]; } +(NSDictionary *) defaultTypingAttributes { static NSDictionary *defaultTypingAttributes; if (!defaultTypingAttributes) { defaultTypingAttributes = [[NSDictionary alloc] initWithObjectsAndKeys: [NSParagraphStyle defaultParagraphStyle], NSParagraphStyleAttributeName, [NSFont userFontOfSize: 0], NSFontAttributeName, [NSColor textColor], NSForegroundColorAttributeName, nil]; } return defaultTypingAttributes; } + (NSMenu *) defaultMenu { if (!textViewMenu) { textViewMenu = [[NSMenu alloc] initWithTitle: @""]; [textViewMenu insertItemWithTitle: _(@"Cut") action:@selector(cut:) keyEquivalent:@"x" atIndex:0]; [textViewMenu insertItemWithTitle: _(@"Copy") action:@selector(copy:) keyEquivalent:@"c" atIndex:1]; [textViewMenu insertItemWithTitle: _(@"Paste") action:@selector(paste:) keyEquivalent:@"v" atIndex:2]; } return textViewMenu; } /**** Initialization ****/ /* Note that -init* must be completely side-effect-less (outside this NSTextView). In particular, they must _not_ touch the font panel or rulers until whoever created us has a chance to eg. -setUsesFontPanel: NO. Calling any other methods here is dangerous as a sub-class might have overridden them and given them side-effects, so we don't. Note also that if a text view is added to an existing text network, the new text view must not change any attributes of the existing text views. Instead, it sets its own attributes to match the others'. This also applies when a text view's moves to another text network. The only method that is allowed to change which text network a text view belongs to is -setTextContainer:, and it must be called when it changes (NSLayoutManager and NSTextContainer do this for us). Since we can't have any side-effects, we can't call methods to set the values. A sub-class that wants to react to changes caused by moving to a different text network will have to override -setTextContainer: and do whatever it needs to do after calling [super setTextContainer: foo]. (TODO: check that this behavior is acceptable) If a text view is added to an empty text network, it keeps its attributes. */ - (NSTextContainer *) buildUpTextNetwork: (NSSize)aSize { NSTextContainer *textContainer; NSLayoutManager *layoutManager; NSTextStorage *textStorage; textStorage = [[NSTextStorage alloc] init]; layoutManager = [[NSLayoutManager alloc] init]; [textStorage addLayoutManager: layoutManager]; RELEASE(layoutManager); textContainer = [[NSTextContainer alloc] initWithContainerSize: aSize]; [layoutManager addTextContainer: textContainer]; RELEASE(textContainer); /* We keep a flag to remember that we are directly responsible for managing the text network. */ _tf.owns_text_network = YES; // This value gets set once more in setTextContainer: to the same value. _textStorage = textStorage; /* The situation at this point is as follows: textView (us) --RETAINs--> textStorage textStorage --RETAINs--> layoutManager layoutManager --RETAINs--> textContainer */ return textContainer; } /* Designated initializer. */ - (id) initWithFrame: (NSRect)frameRect textContainer: (NSTextContainer *)container { self = [super initWithFrame: frameRect]; if (!self) return nil; if (!did_register_for_services) [[self class] registerForServices]; _minSize = frameRect.size; _maxSize = NSMakeSize(frameRect.size.width, GSHUGE); _textContainerInset = NSZeroSize; ASSIGN(_insertionPointColor, [NSColor textColor]); ASSIGN(_backgroundColor, [NSColor textBackgroundColor]); ASSIGN(_defaultParagraphStyle, [NSParagraphStyle defaultParagraphStyle]); _linkTextAttributes = [[NSDictionary alloc] initWithObjectsAndKeys: [NSNumber numberWithBool: YES], NSUnderlineStyleAttributeName, [NSColor blueColor], NSForegroundColorAttributeName, nil]; _tf.draws_background = YES; _tf.is_horizontally_resizable = NO; _tf.is_vertically_resizable = NO; /* We set defaults for all shared attributes here. If container is already part of a text network, we reset the attributes in -setTextContainer:. */ _tf.is_field_editor = NO; _tf.is_editable = YES; _tf.is_selectable = YES; _tf.is_rich_text = YES; _tf.imports_graphics = NO; _tf.uses_font_panel = YES; _tf.uses_ruler = YES; _tf.is_ruler_visible = NO; _tf.allows_undo = NO; _tf.smart_insert_delete = YES; _tf.uses_find_panel = NO; _tf.accepts_glyph_info = NO; _tf.allows_document_background_color_change = YES; _dragTargetLocation = NSNotFound; _markedRange = NSMakeRange(NSNotFound, 0); [container setTextView: self]; [self invalidateTextContainerOrigin]; [self setPostsFrameChangedNotifications: YES]; [notificationCenter addObserver: self selector: @selector(_updateState:) name: NSViewFrameDidChangeNotification object: self]; [notificationCenter addObserver: self selector: @selector(_textDidChange:) name: NSTextDidChangeNotification object: self]; return self; } - (id) initWithFrame: (NSRect)frameRect { NSTextContainer *aTextContainer; NSSize containerSize = NSMakeSize(NSWidth(frameRect), 1e7); aTextContainer = [self buildUpTextNetwork: containerSize]; self = [self initWithFrame: frameRect textContainer: aTextContainer]; [self setVerticallyResizable: YES]; [aTextContainer setWidthTracksTextView: YES]; /* At this point the situation is as follows: textView (us) --RETAINs--> textStorage textStorage --RETAINs--> layoutManager layoutManager --RETAINs--> textContainer textContainer --RETAINs--> textView (us) */ /* The text system should be destroyed when the textView (us) is released. To get this result, we send a RELEASE message to us breaking the RETAIN cycle. */ RELEASE(self); return self; } /* In earlier versions, some of these ivar:s were in NSText instead of NSTextView, and parts of their handling (including encoding and decoding) were there. This has been fixed and the ivar:s moved here, but in a way that makes decoding and encoding compatible with the old code. */ - (void) encodeWithCoder: (NSCoder *)aCoder { BOOL flag; NSSize containerSize = [_textContainer containerSize]; [super encodeWithCoder: aCoder]; if ([aCoder allowsKeyedCoding]) { int flags = (_tf.is_horizontally_resizable ? 0x01 : 0) | (_tf.is_vertically_resizable ? 0x02 : 0) | (_tf.owns_text_network ? 0x04 : 0); NSTextViewSharedData *tvsd = [[NSTextViewSharedData alloc] initWithTextView: self]; [aCoder encodeConditionalObject: _delegate forKey: @"NSDelegate"]; [aCoder encodeSize: [self maxSize] forKey: @"NSMaxSize"]; [aCoder encodeSize: [self minSize] forKey: @"NSMinSize"]; [aCoder encodeObject: tvsd forKey: @"NSSharedData"]; RELEASE(tvsd); [aCoder encodeObject: [self textStorage] forKey: @"NSTextStorage"]; [aCoder encodeObject: [self textContainer] forKey: @"NSTextContainer"]; [aCoder encodeInt: flags forKey: @"NSTVFlags"]; } else { [aCoder encodeConditionalObject: _delegate]; flag = _tf.is_field_editor; [aCoder encodeValueOfObjCType: @encode(BOOL) at: &flag]; flag = _tf.is_editable; [aCoder encodeValueOfObjCType: @encode(BOOL) at: &flag]; flag = _tf.is_selectable; [aCoder encodeValueOfObjCType: @encode(BOOL) at: &flag]; flag = _tf.is_rich_text; [aCoder encodeValueOfObjCType: @encode(BOOL) at: &flag]; flag = _tf.imports_graphics; [aCoder encodeValueOfObjCType: @encode(BOOL) at: &flag]; flag = _tf.draws_background; [aCoder encodeValueOfObjCType: @encode(BOOL) at: &flag]; flag = _tf.is_horizontally_resizable; [aCoder encodeValueOfObjCType: @encode(BOOL) at: &flag]; flag = _tf.is_vertically_resizable; [aCoder encodeValueOfObjCType: @encode(BOOL) at: &flag]; flag = _tf.uses_font_panel; [aCoder encodeValueOfObjCType: @encode(BOOL) at: &flag]; flag = _tf.uses_ruler; [aCoder encodeValueOfObjCType: @encode(BOOL) at: &flag]; flag = _tf.is_ruler_visible; [aCoder encodeValueOfObjCType: @encode(BOOL) at: &flag]; [aCoder encodeObject: _backgroundColor]; [aCoder encodeValueOfObjCType: @encode(NSSize) at: &_minSize]; [aCoder encodeValueOfObjCType: @encode(NSSize) at: &_maxSize]; flag = _tf.smart_insert_delete; [aCoder encodeValueOfObjCType: @encode(BOOL) at: &flag]; flag = _tf.allows_undo; [aCoder encodeValueOfObjCType: @encode(BOOL) at: &flag]; // version 3 flag = _tf.uses_find_panel; [aCoder encodeValueOfObjCType: @encode(BOOL) at: &flag]; // version 2 [aCoder encodeObject: _insertionPointColor]; [aCoder encodeValueOfObjCType: @encode(NSSize) at: &containerSize]; flag = [_textContainer widthTracksTextView]; [aCoder encodeValueOfObjCType: @encode(BOOL) at: &flag]; flag = [_textContainer heightTracksTextView]; [aCoder encodeValueOfObjCType: @encode(BOOL) at: &flag]; // version 4 [aCoder encodeObject: _defaultParagraphStyle]; [aCoder encodeObject: _markedTextAttributes]; [aCoder encodeObject: _linkTextAttributes]; flag = _tf.continuous_spell_checking; [aCoder encodeValueOfObjCType: @encode(BOOL) at: &flag]; } } - (id) initWithCoder: (NSCoder *)aDecoder { self = [super initWithCoder: aDecoder]; if (!self) return nil; if ([aDecoder allowsKeyedCoding]) { if ([aDecoder containsValueForKey: @"NSDelegate"]) { [self setDelegate: [aDecoder decodeObjectForKey: @"NSDelegate"]]; } if ([aDecoder containsValueForKey: @"NSMaxSize"]) { [self setMaxSize: [aDecoder decodeSizeForKey: @"NSMaxSize"]]; } else { [self setMaxSize: NSMakeSize(_frame.size.width, GSHUGE)]; } if ([aDecoder containsValueForKey: @"NSMinSize"]) { // If NSMinSize is present we want to use it. [self setMinSize: [aDecoder decodeSizeForKey: @"NSMinSize"]]; } else if ([aDecoder containsValueForKey: @"NSMinize"]) { // it's NSMinize in pre-10.3 formats. [self setMinSize: [aDecoder decodeSizeForKey: @"NSMinize"]]; } else { [self setMinSize: _frame.size]; } if ([aDecoder containsValueForKey: @"NSSharedData"]) { NSTextViewSharedData *shared; unsigned int flags; shared = [aDecoder decodeObjectForKey: @"NSSharedData"]; flags = [shared flags]; ASSIGN(_insertionPointColor, [shared insertionColor]); ASSIGN(_backgroundColor, [shared backgroundColor]); ASSIGN(_defaultParagraphStyle, [shared paragraphStyle]); ASSIGN(_markedTextAttributes, [shared markAttributes]); ASSIGN(_linkTextAttributes, [shared linkAttributes]); _tf.is_selectable = ((0x01 & flags) > 0); _tf.is_editable = ((0x02 & flags) > 0); _tf.is_rich_text = ((0x04 & flags) > 0); _tf.imports_graphics = ((0x08 & flags) > 0); _tf.is_field_editor = ((0x10 & flags) > 0); _tf.uses_font_panel = ((0x20 & flags) > 0); _tf.is_ruler_visible = ((0x40 & flags) > 0); _tf.continuous_spell_checking = ((0x80 & flags) > 0); _tf.uses_ruler = ((0x100 & flags) > 0); _tf.smart_insert_delete = ((0x200 & flags) > 0); _tf.allows_undo = ((0x400 & flags) > 0); _tf.draws_background = ((0x800 & flags) > 0); _tf.uses_find_panel = ((0x2000 & flags) > 0); } if ([aDecoder containsValueForKey: @"NSTVFlags"]) { int flags = [aDecoder decodeIntForKey: @"NSTVFlags"]; _tf.is_horizontally_resizable = ((flags & 0x01) != 0); _tf.is_vertically_resizable = ((flags & 0x02) != 0); _tf.owns_text_network = ((flags & 0x04) != 0); } // Get the text container to retrieve the text which was previously archived // so that it can be inserted into the current textview. if ([aDecoder containsValueForKey: @"NSTextContainer"]) { NSTextContainer *container = [aDecoder decodeObjectForKey: @"NSTextContainer"]; [container setTextView: self]; } else { NSTextContainer *container = [self buildUpTextNetwork: _frame.size]; [container setTextView: self]; // These calls are here to make the text container aware of these settings [self setHorizontallyResizable: _tf.is_horizontally_resizable]; [self setVerticallyResizable: _tf.is_vertically_resizable]; } //@"NSDragTypes" if (_tf.owns_text_network && _textStorage != nil) { // See initWithFrame: for comments on this RELEASE RELEASE(self); } } else { BOOL flag; NSTextContainer *aTextContainer; int version = [aDecoder versionForClassName: @"NSTextView"]; /* Common stuff for all versions. */ _delegate = [aDecoder decodeObject]; [aDecoder decodeValueOfObjCType: @encode(BOOL) at: &flag]; _tf.is_field_editor = flag; [aDecoder decodeValueOfObjCType: @encode(BOOL) at: &flag]; _tf.is_editable = flag; [aDecoder decodeValueOfObjCType: @encode(BOOL) at: &flag]; _tf.is_selectable = flag; [aDecoder decodeValueOfObjCType: @encode(BOOL) at: &flag]; _tf.is_rich_text = flag; [aDecoder decodeValueOfObjCType: @encode(BOOL) at: &flag]; _tf.imports_graphics = flag; [aDecoder decodeValueOfObjCType: @encode(BOOL) at: &flag]; _tf.draws_background = flag; [aDecoder decodeValueOfObjCType: @encode(BOOL) at: &flag]; _tf.is_horizontally_resizable = flag; [aDecoder decodeValueOfObjCType: @encode(BOOL) at: &flag]; _tf.is_vertically_resizable = flag; [aDecoder decodeValueOfObjCType: @encode(BOOL) at: &flag]; _tf.uses_font_panel = flag; [aDecoder decodeValueOfObjCType: @encode(BOOL) at: &flag]; _tf.uses_ruler = flag; [aDecoder decodeValueOfObjCType: @encode(BOOL) at: &flag]; _tf.is_ruler_visible = flag; _backgroundColor = RETAIN([aDecoder decodeObject]); [aDecoder decodeValueOfObjCType: @encode(NSSize) at: &_minSize]; [aDecoder decodeValueOfObjCType: @encode(NSSize) at: &_maxSize]; [aDecoder decodeValueOfObjCType: @encode(BOOL) at: &flag]; _tf.smart_insert_delete = flag; [aDecoder decodeValueOfObjCType: @encode(BOOL) at: &flag]; _tf.allows_undo = flag; if (version >= 3) { [aDecoder decodeValueOfObjCType: @encode(BOOL) at: &flag]; _tf.uses_find_panel = flag; } /* build up the rest of the text system, which doesn't get stored . */ aTextContainer = [self buildUpTextNetwork: _frame.size]; [aTextContainer setTextView: (NSTextView *)self]; /* See initWithFrame: for comments on this RELEASE */ RELEASE(self); if (version >= 2) { NSSize containerSize; _insertionPointColor = RETAIN([aDecoder decodeObject]); [aDecoder decodeValueOfObjCType: @encode(NSSize) at: &containerSize]; [aTextContainer setContainerSize: containerSize]; [aDecoder decodeValueOfObjCType: @encode(BOOL) at: &flag]; [aTextContainer setWidthTracksTextView: flag]; [aDecoder decodeValueOfObjCType: @encode(BOOL) at: &flag]; [aTextContainer setHeightTracksTextView: flag]; } if (version >= 4) { _defaultParagraphStyle = RETAIN([aDecoder decodeObject]); _markedTextAttributes = RETAIN([aDecoder decodeObject]); _linkTextAttributes = RETAIN([aDecoder decodeObject]); [aDecoder decodeValueOfObjCType: @encode(BOOL) at: &flag]; _tf.continuous_spell_checking = flag; } } _dragTargetLocation = NSNotFound; [self _recacheDelegateResponses]; [self invalidateTextContainerOrigin]; // register for services... if (!did_register_for_services) [[self class] registerForServices]; [self updateDragTypeRegistration]; [self setPostsFrameChangedNotifications: YES]; [notificationCenter addObserver: self selector: @selector(_updateState:) name: NSViewFrameDidChangeNotification object: self]; [notificationCenter addObserver: self selector: @selector(_textDidChange:) name: NSTextDidChangeNotification object: self]; return self; } - (void) dealloc { if (_tf.owns_text_network == YES) { if (_textStorage != nil) { /* * Destroying our _textStorage releases all the text objects * (us included) which means this method will be called again ... * so this time round we should just return, and the rest of * the deallocation can be done on the next call to dealloc. * * However, the dealloc methods of any subclasses should * already have been called before this method is called, * and those subclasses don't know how to cope with being * deallocated more than once ... to deal with that we * set the isa pointer so that the subclass dealloc methods * won't get called again. */ GSClassSwizzle(self, [NSTextView class]); DESTROY(_textStorage); return; } } [notificationCenter removeObserver: self name: NSViewFrameDidChangeNotification object: self]; [notificationCenter removeObserver: self name: NSTextDidChangeNotification object: self]; [_textCheckingTimer invalidate]; [self _stopInsertionTimer]; [[NSRunLoop currentRunLoop] cancelPerformSelector: @selector(_updateState:) target: self argument: nil]; if (_delegate) { [notificationCenter removeObserver: _delegate name: nil object: _notifObject]; } DESTROY(_selectedTextAttributes); DESTROY(_markedTextAttributes); DESTROY(_insertionPointColor); DESTROY(_backgroundColor); DESTROY(_defaultParagraphStyle); DESTROY(_linkTextAttributes); DESTROY(_undoObject); [super dealloc]; } /**** Managing the text network ****/ /* This should only be called by [NSTextContainer -setTextView:]. If the text container has had its layout manager changed, it will make a dummy call to this method and container==_textContainer. We still need to do a full update in that case. This is assumed to be the __only__ place where _textContainer, _layoutManager, or _textStorage changes. Re-synchronizing the text network is hairy, and this is the only place where it happens. TODO: Make sure the assumption holds; might need to add more dummy calls to this method from the text container or layout manager. */ - (void) setTextContainer: (NSTextContainer *)container { ENTER_POOL NSUInteger i, c; NSArray *tcs; NSTextView *other; /* Any of these three might be nil. */ _textContainer = container; _layoutManager = (NSLayoutManager *)[container layoutManager]; if (_tf.owns_text_network) { ASSIGN(_textStorage, [_layoutManager textStorage]); } else { _textStorage = [_layoutManager textStorage]; } /* Search for an existing text view attached to this layout manager. */ tcs = [_layoutManager textContainers]; c = [tcs count]; for (i = 0; i < c; i++) { other = [[tcs objectAtIndex: i] textView]; if (other && other != self) break; } if (i < c) { /* There is already an NSTextView attached to this text network, so all shared attributes, including those in the layout manager, are already set up. We copy the shared attributes to us. */ _delegate = other->_delegate; _tf.is_field_editor = other->_tf.is_field_editor; _tf.is_editable = other->_tf.is_editable; _tf.is_selectable = other->_tf.is_selectable; _tf.is_rich_text = other->_tf.is_rich_text; _tf.imports_graphics = other->_tf.imports_graphics; _tf.uses_font_panel = other->_tf.uses_font_panel; _tf.uses_ruler = other->_tf.uses_ruler; _tf.is_ruler_visible = other->_tf.is_ruler_visible; _tf.allows_undo = other->_tf.allows_undo; _tf.smart_insert_delete = other->_tf.smart_insert_delete; /* TODO: might need to update other things */ [self _recacheDelegateResponses]; [self updateDragTypeRegistration]; } else if (_layoutManager) { /* There is no text network, and the layout manager's attributes might not be set up. We reset them to standard values. */ DESTROY(_layoutManager->_typingAttributes); _layoutManager->_typingAttributes = [[[self class] defaultTypingAttributes] mutableCopy]; _layoutManager->_original_selected_range.location = NSNotFound; _layoutManager->_selected_range = NSMakeRange(0,0); } _currentInsertionPointMovementDirection = 0; [self _updateMultipleTextViews]; LEAVE_POOL } - (void) replaceTextContainer: (NSTextContainer *)newContainer { NSLayoutManager *lm = _layoutManager; NSUInteger index = [[lm textContainers] indexOfObject: _textContainer]; NSTextStorage *ts = _textStorage; RETAIN(self); RETAIN(ts); [_textContainer setTextView: nil]; [lm removeTextContainerAtIndex: index]; [lm insertTextContainer: newContainer atIndex: index]; [newContainer setTextView: self]; RELEASE(ts); RELEASE(self); } - (NSTextContainer *) textContainer { return _textContainer; } - (NSLayoutManager *) layoutManager { return _layoutManager; } - (NSTextStorage *) textStorage { return _textStorage; } /**** Managing shared attributes ****/ /* Note: You might override these methods in subclasses, as in the following example: - (void) setEditable: (BOOL)flag { [super setEditable: flag]; XXX your custom code here XXX } If you override them in this way, they are automatically synchronized between multiple text views - ie., when it is called on one, it will be automatically called on all other text views attached to thesame layout manager. TODO: Not all combinations of flags are allowed, eg. editable implies selectable, imports graphics implies rich-text, etc. In these cases, when making a change that forces a value on another attribute, the attribute is directly changed. Need to check if we should call the method instead. Need to make sure syncing is handled properly in all cases. */ /* Helper macro for these methods. */ #define NSTEXTVIEW_SYNC \ if (_tf.multiple_textviews && (IS_SYNCHRONIZING_FLAGS == NO)) \ { [self _syncTextViewsByCalling: _cmd withFlag: flag]; \ return; } /* Delegate */ - (id) delegate { return _delegate; } - (void) setDelegate: (id)anObject { /* Code to allow sharing the delegate */ if (_tf.multiple_textviews && (IS_SYNCHRONIZING_DELEGATES == NO)) { /* Invoke setDelegate: on all the textviews which share this delegate. */ NSArray *array; NSUInteger i, count; IS_SYNCHRONIZING_DELEGATES = YES; array = [_layoutManager textContainers]; count = [array count]; for (i = 0; i < count; i++) { NSTextView *view; view = [(NSTextContainer *)[array objectAtIndex: i] textView]; [view setDelegate: anObject]; } IS_SYNCHRONIZING_DELEGATES = NO; } /* Now the real code to set the delegate */ if (_delegate != nil) { [notificationCenter removeObserver: _delegate name: nil object: _notifObject]; } _delegate = anObject; /* SET_DELEGATE_NOTIFICATION defined near the beginning of file */ /* NSText notifications */ SET_DELEGATE_NOTIFICATION(DidBeginEditing); SET_DELEGATE_NOTIFICATION(DidChange); SET_DELEGATE_NOTIFICATION(DidEndEditing); /* NSTextView notifications */ SET_DELEGATE_NOTIFICATION(ViewDidChangeSelection); SET_DELEGATE_NOTIFICATION(ViewWillChangeNotifyingTextView); [self _recacheDelegateResponses]; } /* Editable */ - (BOOL) isEditable { return _tf.is_editable; } - (void) setEditable: (BOOL)flag { NSTEXTVIEW_SYNC; _tf.is_editable = flag; if (flag) { _tf.is_selectable = YES; } if ([self shouldDrawInsertionPoint]) { [self updateInsertionPointStateAndRestartTimer: YES]; } else { /* TODO: insertion point */ } [self updateDragTypeRegistration]; } /* Selectable */ - (BOOL) isSelectable { return _tf.is_selectable; } - (void) setSelectable: (BOOL)flag { NSTEXTVIEW_SYNC; _tf.is_selectable = flag; if (flag == NO) { _tf.is_editable = NO; } [self updateDragTypeRegistration]; } /* Field editor */ - (BOOL) isFieldEditor { return _tf.is_field_editor; } - (void) setFieldEditor: (BOOL)flag { NSTEXTVIEW_SYNC; if (flag) { [self setHorizontallyResizable: NO]; /* TODO: why? */ [self setVerticallyResizable: NO]; [_textContainer setLineFragmentPadding: 0]; } else { [self setHorizontallyResizable: NO]; /* TODO: why? */ [self setVerticallyResizable: YES]; [_textContainer setLineFragmentPadding: 5.0]; } _tf.is_field_editor = flag; } /* Rich-text */ - (BOOL) isRichText { return _tf.is_rich_text; } - (void) setRichText: (BOOL)flag { NSTEXTVIEW_SYNC; _tf.is_rich_text = flag; if (flag == NO) { _tf.imports_graphics = NO; /* TODO: convert text to plain text, ie. make attributes the same in all text? */ } [self updateDragTypeRegistration]; } /* Imports graphics */ - (BOOL) importsGraphics { return _tf.imports_graphics; } - (void) setImportsGraphics: (BOOL)flag { NSTEXTVIEW_SYNC; _tf.imports_graphics = flag; if (flag == YES) { _tf.is_rich_text = YES; } [self updateDragTypeRegistration]; } /* Uses ruler */ - (BOOL) usesRuler { return _tf.uses_ruler; } /* TODO: set ruler visible to NO if flag==NO? */ - (void) setUsesRuler: (BOOL)flag { NSTEXTVIEW_SYNC; _tf.uses_ruler = flag; } /* Ruler visible (TODO: is this really supposed to be shared??) */ - (BOOL) isRulerVisible { return _tf.is_ruler_visible; } - (void) setRulerVisible: (BOOL)flag { NSScrollView *sv; NSRulerView *rv; NSTEXTVIEW_SYNC; sv = [self enclosingScrollView]; _tf.is_ruler_visible = flag; if (sv != nil) { if (_tf.is_ruler_visible && ![sv hasHorizontalRuler]) { [sv setHasHorizontalRuler: YES]; } [sv setRulersVisible: _tf.is_ruler_visible]; if (self == [_window firstResponder] && (rv = [sv horizontalRulerView]) != nil) { if (flag) { [rv setClientView: self]; } else { [rv setClientView: nil]; } } } } /* Uses font panel */ - (BOOL) usesFontPanel { return _tf.uses_font_panel; } - (void) setUsesFontPanel: (BOOL)flag { NSTEXTVIEW_SYNC; _tf.uses_font_panel = flag; } - (BOOL) usesFindPanel { return _tf.uses_find_panel; } - (void) setUsesFindPanel: (BOOL)flag { _tf.uses_find_panel = flag; } /* Smart insert/delete */ - (BOOL) smartInsertDeleteEnabled { return _tf.smart_insert_delete; } - (void) setSmartInsertDeleteEnabled: (BOOL)flag { NSTEXTVIEW_SYNC; _tf.smart_insert_delete = flag; } /* Undo */ - (BOOL) allowsUndo { return _tf.allows_undo; } - (void) setAllowsUndo: (BOOL)flag { NSTEXTVIEW_SYNC; _tf.allows_undo = flag; } /* Continuous spell checking */ - (BOOL) isContinuousSpellCheckingEnabled { return _tf.continuous_spell_checking; } - (void) setContinuousSpellCheckingEnabled: (BOOL)flag { NSTEXTVIEW_SYNC; if (_tf.continuous_spell_checking && !flag) { const NSRange allRange = NSMakeRange(0, [[self string] length]); _tf.continuous_spell_checking = 0; [_layoutManager removeTemporaryAttribute: @"NSTextChecked" forCharacterRange: allRange]; [_layoutManager removeTemporaryAttribute: NSSpellingStateAttributeName forCharacterRange: allRange]; _lastCheckedRect = NSZeroRect; [_textCheckingTimer invalidate]; _textCheckingTimer = nil; } else if (!_tf.continuous_spell_checking && flag) { _tf.continuous_spell_checking = 1; [self _scheduleTextCheckingInVisibleRectIfNeeded]; } } /* Don't need this anymore. */ #undef NSTEXTVIEW_SYNC /**** Basic view stuff ****/ - (BOOL) isFlipped { return YES; } - (BOOL) isOpaque { if (_tf.draws_background == NO || _backgroundColor == nil || [_backgroundColor alphaComponent] < 1.0) return NO; else return YES; } - (BOOL) needsPanelToBecomeKey { return _tf.is_editable || _tf.is_selectable; } - (BOOL) acceptsFirstResponder { if (_tf.is_selectable) { return YES; } else { return NO; } } - (BOOL) resignFirstResponder { /* Check if another text view attached to the same layout manager is the new first responder. If so, we always let it become first responder, and we don't send any notifications. */ if (_tf.multiple_textviews == YES) { id futureFirstResponder; NSArray *textContainers; NSUInteger i, count; futureFirstResponder = [_window _futureFirstResponder]; textContainers = [_layoutManager textContainers]; count = [textContainers count]; for (i = 0; i < count; i++) { NSTextContainer *container; NSTextView *view; container = (NSTextContainer *)[textContainers objectAtIndex: i]; view = [container textView]; if (view == futureFirstResponder) { /* NB: We do not reset the BEGAN_EDITING flag so that no spurious notification is generated. */ return YES; } } } /* NB: Possible change: ask always - not only if editable - but we need to change NSTextField etc to allow this. */ if ((_tf.is_editable) && ([_delegate respondsToSelector: @selector(textShouldEndEditing:)]) && ([_delegate textShouldEndEditing: self] == NO)) { return NO; } /* Add any clean-up stuff here */ [self _resignRulerClient]; if ([self shouldDrawInsertionPoint]) { [self updateInsertionPointStateAndRestartTimer: NO]; } if (_layoutManager != nil) { _layoutManager->_beganEditing = NO; } /* NB: According to the doc (and to the tradition), we post this notification even if no real editing was actually done (only selection of text) [Note: in this case, no editing was started, so the notification does not come after a NSTextDidBeginEditingNotification!]. The notification only means that we are resigning first responder status. This makes sense because many objects inside the gui need this notification anyway - typically, it is needed to remove a field editor (editable or not) when the user presses TAB to move to the next view. Anyway yes, the notification name is misleading. */ [notificationCenter postNotificationName: NSTextDidEndEditingNotification object: _notifObject]; if (_tf.is_field_editor) { [[self undoManager] removeAllActions]; } return YES; } /* Note that when this method is called, editing might already have started (in another text view attached to the same layout manager). */ - (BOOL) becomeFirstResponder { if (_tf.is_selectable == NO) { return NO; } /* Note: Notifications (NSTextBeginEditingNotification etc) are sent the first time the user tries to edit us. */ [self _becomeRulerClient]; /* Draw selection, update insertion point */ if ([self shouldDrawInsertionPoint]) { [self updateInsertionPointStateAndRestartTimer: YES]; } return YES; } - (void) resignKeyWindow { if ([self shouldDrawInsertionPoint]) { [self updateInsertionPointStateAndRestartTimer: NO]; } } - (void) becomeKeyWindow { if ([self shouldDrawInsertionPoint]) { [self updateInsertionPointStateAndRestartTimer: YES]; } } /**** Unshared attributes ****/ - (NSColor *) backgroundColor { return _backgroundColor; } - (BOOL) drawsBackground { return _tf.draws_background; } - (void) setBackgroundColor: (NSColor *)color { if (![_backgroundColor isEqual: color]) { ASSIGN(_backgroundColor, color); [self setNeedsDisplay: YES]; } } - (void) setDrawsBackground: (BOOL)flag { if (_tf.draws_background != flag) { _tf.draws_background = flag; [self setNeedsDisplay: YES]; } } - (void) setInsertionPointColor: (NSColor *)color { ASSIGN(_insertionPointColor, color); } - (NSColor *) insertionPointColor { return _insertionPointColor; } - (void) setSelectedTextAttributes: (NSDictionary *)attributeDictionary { ASSIGN(_selectedTextAttributes, attributeDictionary); } - (NSDictionary *) selectedTextAttributes { return _selectedTextAttributes; } - (void) setMarkedTextAttributes: (NSDictionary *)attributeDictionary { ASSIGN(_markedTextAttributes, attributeDictionary); } - (NSDictionary *) markedTextAttributes { return _markedTextAttributes; } - (NSDictionary *) linkTextAttributes { return _linkTextAttributes; } - (void) setLinkTextAttributes: (NSDictionary *)attributeDictionary { ASSIGN(_linkTextAttributes, attributeDictionary); } /**** Size management ****/ /* If we are the document view of a clip view, we always have an effective minimum size of the clip view's bounds size. This ensures that we always fill the clip view with our background, and that we always handle clicks events everywhere inside it (and not just on a small part of it if we are partially filled with text and track the text container vertically). Note that this only happens if the text view is the document view of a clip view; if several text views are embedded in a clip view (eg. a multi-column text document in a scroll view), we do _not_ want to extend any of the text views to cover the entire clip view. TODO: what if the max. size is smaller than the effective min. size? TODO: Check and fix bounds vs. frame issues. */ /* TODO: The safety calls below don't protect against all misuse (doing that would be very tricky), so is it worth having them there at all? The docs are pretty clear about what happens (ie. breakage) if you set the flags incorrectly. */ - (void) setHorizontallyResizable: (BOOL)flag { /* Safety call */ [_textContainer setWidthTracksTextView: !flag]; if (flag) { NSSize size; NSSize inset; inset = [self textContainerInset]; size = [_textContainer containerSize]; size.width = MAX(_maxSize.width - (inset.width * 2.0), 0.0); [_textContainer setContainerSize: size]; } _tf.is_horizontally_resizable = flag; } - (void) setVerticallyResizable: (BOOL)flag { /* Safety call */ [_textContainer setHeightTracksTextView: !flag]; if (flag) { NSSize size; NSSize inset; inset = [self textContainerInset]; size = [_textContainer containerSize]; size.height = MAX(_maxSize.height - (inset.height * 2.0), 0.0); [_textContainer setContainerSize: size]; } _tf.is_vertically_resizable = flag; } - (BOOL) isHorizontallyResizable { return _tf.is_horizontally_resizable; } - (BOOL) isVerticallyResizable { return _tf.is_vertically_resizable; } - (NSSize) maxSize { return _maxSize; } - (NSSize) minSize { return _minSize; } - (void) setMaxSize: (NSSize)newMaxSize { _maxSize = newMaxSize; } - (void) setMinSize: (NSSize)newMinSize { _minSize = newMinSize; } - (void) sizeToFit { NSSize size; if (!_layoutManager) return; size = _bounds.size; if (_tf.is_horizontally_resizable || _tf.is_vertically_resizable) { NSRect r = [_layoutManager usedRectForTextContainer: _textContainer]; NSSize s2 = NSMakeSize(NSMaxX(r), NSMaxY(r)); if (_tf.is_horizontally_resizable) size.width = s2.width + 2 * _textContainerInset.width; if (_tf.is_vertically_resizable) size.height = s2.height + 2 * _textContainerInset.height; } [self setConstrainedFrameSize: size]; } /* TODO: There is code in TextEdit that implies that the minimum size is mostly ignored, and that the size of the containing clip view is always used instead. Should test on OS to find out what the proper behavior is. UPDATE: current behavior is correct, but must be documented properly before this TODO can be removed */ - (void) setConstrainedFrameSize: (NSSize)desiredSize { NSSize newSize; NSSize effectiveMinSize = _minSize; NSClipView *cv = (NSClipView *)[self superview]; if (cv && [cv isKindOfClass: [NSClipView class]] && [cv documentView] == self) { NSSize b = [cv bounds].size; effectiveMinSize.width = MAX(effectiveMinSize.width , b.width); effectiveMinSize.height = MAX(effectiveMinSize.height, b.height); } if (_tf.is_horizontally_resizable) { newSize.width = desiredSize.width; newSize.width = MAX(newSize.width, effectiveMinSize.width); newSize.width = MIN(newSize.width, _maxSize.width); } else { newSize.width = _frame.size.width; } if (_tf.is_vertically_resizable) { newSize.height = desiredSize.height; newSize.height = MAX(newSize.height, effectiveMinSize.height); newSize.height = MIN(newSize.height, _maxSize.height); } else { newSize.height = _frame.size.height; } if (NSEqualSizes(_frame.size, newSize) == NO) { [self setFrameSize: newSize]; } } - (void) resizeSubviewsWithOldSize: (NSSize)oldSize { NSSize curSize = [self frame].size; if (_minSize.width > curSize.width) _minSize.width = curSize.width; if (_minSize.height > curSize.height) _minSize.height = curSize.height; if (_maxSize.width < curSize.width) _maxSize.width = curSize.width; if (_maxSize.height < curSize.height) _maxSize.height = curSize.height; [super resizeSubviewsWithOldSize: oldSize]; } /**** Text container origin ****/ /* The text container origin is the origin of the text container's coordinate system in our coordinate system. */ - (void) setTextContainerInset: (NSSize)inset { _textContainerInset = inset; [self invalidateTextContainerOrigin]; /* We send this so our text container can react to the change (it might need to resize itself it it's set to track our size). */ [notificationCenter postNotificationName: NSViewFrameDidChangeNotification object: self]; } - (NSSize) textContainerInset { return _textContainerInset; } - (NSPoint) textContainerOrigin { return _textContainerOrigin; } /* TODO: There used to be a bunch of complex code in here, but I couldn't really see what it did, and I didn't see anything in the docs that imply that anything complex needs to be done, so I removed it. Should double-check and bring it back if necessary. */ - (void) invalidateTextContainerOrigin { _textContainerOrigin.x = NSMinX(_bounds); _textContainerOrigin.x += _textContainerInset.width; _textContainerOrigin.y = NSMinY(_bounds); _textContainerOrigin.y += _textContainerInset.height; } /**** Methods of the NSTextInput protocol ****/ /* -selectedRange is a part of this protocol, but it's also a method in NSText. The implementation is among the selection handling methods and not here. */ /* TODO: currently no support for marked text */ - (NSAttributedString *) attributedSubstringFromRange: (NSRange)theRange { if (theRange.location >= [_textStorage length]) return nil; if (theRange.location + theRange.length > [_textStorage length]) theRange.length = [_textStorage length] - theRange.location; return [_textStorage attributedSubstringFromRange: theRange]; } // This method takes screen coordinates as input. - (NSUInteger) characterIndexForPoint: (NSPoint)point { point = [[self window] convertScreenToBase: point]; point = [self convertPoint:point fromView: nil]; return [self _characterIndexForPoint: point respectFraction: NO]; } - (NSRange) markedRange { return _markedRange; } - (void) setMarkedText: (id)aString selectedRange: (NSRange)selRange { NSAttributedString *as; _markedRange = selRange; as = [[NSAttributedString alloc] initWithString: aString attributes: [self markedTextAttributes]]; [self replaceCharactersInRange: selRange withAttributedString: as]; DESTROY(as); } - (BOOL) hasMarkedText { return (_markedRange.location != NSNotFound); } - (void) unmarkText { if (![self hasMarkedText]) return; [_textStorage setAttributes: _layoutManager->_typingAttributes range: _markedRange]; _markedRange = NSMakeRange(NSNotFound, 0); } - (NSArray *) validAttributesForMarkedText { return [NSArray arrayWithObjects: NSBackgroundColorAttributeName, NSForegroundColorAttributeName, NSFontAttributeName, nil]; } - (NSInteger) conversationIdentifier { return (NSInteger)_textStorage; } - (NSRect) firstRectForCharacterRange: (NSRange)theRange { NSUInteger rectCount = 0; NSRect *rects; if (!_layoutManager) return NSZeroRect; rects = [_layoutManager rectArrayForCharacterRange: theRange withinSelectedCharacterRange: NSMakeRange(NSNotFound, 0) inTextContainer: _textContainer rectCount: &rectCount]; if (rectCount) return rects[0]; else return NSZeroRect; } /* Unlike NSResponder, we should _not_ send the selector down the responder chain if we can't handle it. */ - (void) doCommandBySelector: (SEL)aSelector { if (!_layoutManager) { NSBeep(); return; } /* Give the delegate a chance to handle it. */ if ([_delegate respondsToSelector: @selector(textView:doCommandBySelector:)] && [_delegate textView: _notifObject doCommandBySelector: aSelector]) { return; } if ([self respondsToSelector: aSelector]) { [self performSelector: aSelector withObject: nil]; } else { NSBeep(); } } /* insertString may actually be an NSAttributedString. If it is, and the text view isn't rich-text, we ignore the attributes and use the typing attributes. This method is for user changes; see NSTextView_actions.m. */ - (void) insertText: (id)insertString { NSRange insertRange = [self rangeForUserTextChange]; NSString *string; BOOL isAttributed; if (insertRange.location == NSNotFound) { NSBeep(); return; } if (insertString == nil) { return; } isAttributed = [insertString isKindOfClass: [NSAttributedString class]]; if (isAttributed) string = [(NSAttributedString *)insertString string]; else string = insertString; if (![self shouldChangeTextInRange: insertRange replacementString: string]) { return; } if (_tf.is_rich_text) { if (isAttributed) { [_textStorage replaceCharactersInRange: insertRange withAttributedString: (NSAttributedString *)insertString]; } else { [_textStorage replaceCharactersInRange: insertRange withAttributedString: AUTORELEASE([[NSAttributedString alloc] initWithString: insertString attributes: _layoutManager->_typingAttributes])]; } } else { if (isAttributed) { [self replaceCharactersInRange: insertRange withString: [(NSAttributedString *)insertString string]]; } else { [self replaceCharactersInRange: insertRange withString: insertString]; } } [self didChangeText]; } /**** Text modification methods (programmatic) ****/ /* These are methods for programmatic changes, ie. they do _not_ check with the delegate, they don't send notifications, and they work in un- editable text views, etc. */ /* Replace the characters in the given range with the given string. In a non-rich-text view, we use the same attributes as the rest of the text storage (ie. the typing attributes). In a rich-text text view, attributes for the new characters are computed thusly: 1. If there is a first character in the replaced range, its attributes are used. (If not, the range must have length 0.) 2. If there is a character right before the range, its attributed are used. (If not, the range must have location 0, so the range must be length=0, location=0.) 3. If there is a character after the range, we use its attributes. (If not, the text storage must be empty.) 4. We use the typing attributes. (Note that if the text storage is empty, this is the only valid case.) Since 1. - 3. correspond to the normal NSMutableAttributedString behavior, we only need to handle 4. explicitly, and we can detect it by checking if the text storage is empty. */ - (void) replaceCharactersInRange: (NSRange)aRange withString: (NSString *)aString { if (aRange.location == NSNotFound) /* TODO: throw exception instead? */ return; if ([_textStorage length] == 0) { NSAttributedString *as; as = [[NSAttributedString alloc] initWithString: aString attributes: _layoutManager->_typingAttributes]; [_textStorage replaceCharactersInRange: aRange withAttributedString: as]; DESTROY(as); } else { [_textStorage replaceCharactersInRange: aRange withString: aString]; } } /* GNUstep extension. Like the above, but uses the attributes from the string if the text view is rich-text, and otherwise the typing attributes. */ - (void) replaceCharactersInRange: (NSRange)aRange withAttributedString: (NSAttributedString *)aString { if (aRange.location == NSNotFound) /* TODO: throw exception instead? */ return; if (_tf.is_rich_text) { [_textStorage replaceCharactersInRange: aRange withAttributedString: aString]; } else { /* Let the other method deal with the empty text storage case. */ [self replaceCharactersInRange: aRange withString: [aString string]]; } } /* Some attribute-modification methods. The range-less methods change the attribute for all text and update the typing attributes. The range methods, which only work in rich-text text views, only change the attributes for the range, and do not update the typing attributes. */ - (void) setFont: (NSFont *)font { if (!font) return; [_textStorage addAttribute: NSFontAttributeName value: font range: NSMakeRange(0,[_textStorage length])]; [_layoutManager->_typingAttributes setObject: font forKey: NSFontAttributeName]; [notificationCenter postNotificationName: NSTextViewDidChangeTypingAttributesNotification object: _notifObject]; } - (void) setFont: (NSFont *)font range: (NSRange)aRange { if (!_tf.is_rich_text || !font) return; [_textStorage addAttribute: NSFontAttributeName value: font range: aRange]; } - (void) setAlignment: (NSTextAlignment)alignment { NSParagraphStyle *style; NSMutableParagraphStyle *mstyle; [_textStorage setAlignment: alignment range: NSMakeRange(0, [_textStorage length])]; /* Update the typing attributes. */ style = [_layoutManager->_typingAttributes objectForKey: NSParagraphStyleAttributeName]; if (style == nil) style = [self defaultParagraphStyle]; mstyle = [style mutableCopy]; [mstyle setAlignment: alignment]; [_layoutManager->_typingAttributes setObject: mstyle forKey: NSParagraphStyleAttributeName]; DESTROY(mstyle); [notificationCenter postNotificationName: NSTextViewDidChangeTypingAttributesNotification object: _notifObject]; } - (void) setAlignment: (NSTextAlignment)alignment range: (NSRange)range { if (!_tf.is_rich_text) return; [_textStorage setAlignment: alignment range: range]; } - (void) setTextColor: (NSColor *)color { if (!color) { [_textStorage removeAttribute: NSForegroundColorAttributeName range: NSMakeRange(0, [_textStorage length])]; [_layoutManager->_typingAttributes removeObjectForKey: NSForegroundColorAttributeName]; } else { [_textStorage addAttribute: NSForegroundColorAttributeName value: color range: NSMakeRange(0, [_textStorage length])]; [_layoutManager->_typingAttributes setObject: color forKey: NSForegroundColorAttributeName]; } [notificationCenter postNotificationName: NSTextViewDidChangeTypingAttributesNotification object: _notifObject]; } - (void) setTextColor: (NSColor *)color range: (NSRange)aRange { if (!_tf.is_rich_text) return; if (color) { [_textStorage addAttribute: NSForegroundColorAttributeName value: color range: aRange]; } else { [_textStorage removeAttribute: NSForegroundColorAttributeName range: aRange]; } } - (NSParagraphStyle *) defaultParagraphStyle { return _defaultParagraphStyle; } - (void) setDefaultParagraphStyle: (NSParagraphStyle *)style { ASSIGN(_defaultParagraphStyle, style); } /**** Text access methods ****/ - (NSData *) RTFDFromRange: (NSRange)aRange { return [_textStorage RTFDFromRange: aRange documentAttributes: nil]; } - (NSData *) RTFFromRange: (NSRange)aRange { return [_textStorage RTFFromRange: aRange documentAttributes: nil]; } - (NSString *) string { return [_textStorage string]; } - (NSUInteger) textLength { return [_textStorage length]; } - (NSFont *) font { if ([_textStorage length] > 0) { return [_textStorage attribute: NSFontAttributeName atIndex: 0 effectiveRange: NULL]; } return [_layoutManager->_typingAttributes objectForKey: NSFontAttributeName]; } /* Returns alignment of first selected paragraph (which will be the alignment for all text in a non-rich-text text view). Since the alignment of the typing attributes will always be the same as the alignment of the first selected paragraph, we can simply return the typing attributes' alignment. (TODO: double-check this assumption) */ - (NSTextAlignment) alignment { return [[_layoutManager->_typingAttributes objectForKey: NSParagraphStyleAttributeName] alignment]; } - (NSColor *) textColor { if ([_textStorage length] > 0) { return [_textStorage attribute: NSForegroundColorAttributeName atIndex: 0 effectiveRange: NULL]; } return [_layoutManager->_typingAttributes objectForKey: NSForegroundColorAttributeName]; } /**** Pasteboard actions ****/ /* TODO: Move to NSTextView_actions.m? */ - (void) copy: (id)sender { NSMutableArray *types = [NSMutableArray array]; if (_tf.imports_graphics) [types addObject: NSRTFDPboardType]; if (_tf.is_rich_text) [types addObject: NSRTFPboardType]; if ([self smartInsertDeleteEnabled] && [self selectionGranularity] == NSSelectByWord) [types addObject: NSSmartPastePboardType]; [types addObject: NSStringPboardType]; [self writeSelectionToPasteboard: [NSPasteboard generalPasteboard] types: types]; } /* Copy the current font to the font pasteboard */ - (void) copyFont: (id)sender { NSPasteboard *pb = [NSPasteboard pasteboardWithName: NSFontPboard]; [self writeSelectionToPasteboard: pb types: [NSArray arrayWithObject: NSFontPboardType]]; } /* Copy the current ruler settings to the ruler pasteboard */ - (void) copyRuler: (id)sender { NSPasteboard *pb = [NSPasteboard pasteboardWithName: NSRulerPboard]; [self writeSelectionToPasteboard: pb types: [NSArray arrayWithObject: NSRulerPboardType]]; } - (void) paste: (id)sender { [self readSelectionFromPasteboard: [NSPasteboard generalPasteboard]]; } - (void) pasteFont: (id)sender { NSPasteboard *pb = [NSPasteboard pasteboardWithName: NSFontPboard]; [self readSelectionFromPasteboard: pb type: NSFontPboardType]; } - (void) pasteRuler: (id)sender { NSPasteboard *pb = [NSPasteboard pasteboardWithName: NSRulerPboard]; [self readSelectionFromPasteboard: pb type: NSRulerPboardType]; } - (void) pasteAsPlainText: (id)sender { [self readSelectionFromPasteboard: [NSPasteboard generalPasteboard] type: NSStringPboardType]; } - (void) pasteAsRichText: (id)sender { [self readSelectionFromPasteboard: [NSPasteboard generalPasteboard] type: NSRTFPboardType]; } /**** Handling user changes ****/ - (NSUndoManager *) undoManager { NSUndoManager *undo = nil; if (![_delegate respondsToSelector: @selector(undoManagerForTextView:)] || ((undo = [_delegate undoManagerForTextView: self]) == nil)) { undo = [super undoManager]; } return undo; } /* * Began editing flag. There are quite some different ways in which * editing can be started. Each time editing is started, we need to check * with the delegate if it is OK to start editing - but we need to check * only once. So, we use a flag. */ /* TODO: comment seems incorrect. macros are only used here. _beganEditing flag is set to NO in -resignFirstResponder. */ /* YES when editing has already began. If NO, then we need to ask to the delegate for permission to begin editing before allowing any change to be made. We explicitly check for a layout manager, and raise an exception if not found. */ #define BEGAN_EDITING \ (_layoutManager ? _layoutManager->_beganEditing : noLayoutManagerException ()) #define SET_BEGAN_EDITING(X) \ if (_layoutManager != nil) _layoutManager->_beganEditing = X /* Whenever text is to be changed due to some user-induced action, this method should be called with information on the change. This method will check if the change is allowed (by checking if the text view is editable and by asking the delegate for permission). It will return YES if the change is allowed. It will also send notifications as necessary. replacementString should be the string that will replace the affected range (disregarding attributes), or nil if only attributes are changed. If the affected range or replacement string can't be determined, pass in NSNotFound for the range's location and nil for the string. (Ie. even if you know the range, you should pass in NSNotFound so the delegate can tell the difference between a pure attribute change and an unknown change.) TODO: What if the view isn't first responder? It should be impossible for a user change to occur in that case, but user change methods might be called anyway. To be safe, changes are currently always disallowed if the text view isn't the first responder. (2003-02-01): GNUMail does it by having an "attach" button when editing mails. It adds an attachment to the text using -insertText:, and it is a fairly reasonable thing to do. Thus, if we aren't the first responder, we still proceed as normal here. In -didChangeText, if we still aren't the first responder, we send the TextDidEndEditing notification _without_ asking the delegate (-; since we can't handle a NO return). */ - (BOOL) shouldChangeTextInRange: (NSRange)affectedCharRange replacementString: (NSString *)replacementString { BOOL result = YES; if (_tf.is_editable == NO) return NO; /* We need to send the textShouldBeginEditing: / textDidBeginEditingNotification only once. */ if (BEGAN_EDITING == NO) { if (([_delegate respondsToSelector: @selector(textShouldBeginEditing:)]) && ([_delegate textShouldBeginEditing: _notifObject] == NO)) return NO; SET_BEGAN_EDITING(YES); [notificationCenter postNotificationName: NSTextDidBeginEditingNotification object: _notifObject]; } if (_tf.delegate_responds_to_should_change) { result = [_delegate textView: self shouldChangeTextInRange: affectedCharRange replacementString: replacementString]; } if (result && [self allowsUndo]) { NSUndoManager *undo; NSRange undoRange; NSAttributedString *undoString; NSTextViewUndoObject *undoObject; BOOL isTyping; NSEvent *event; static BOOL undoManagerCanCoalesce = NO; { // FIXME This code (together with undoManagerCanCoalesce) is a // temporary workaround to allow using an out of date version of // base. Removed this upon the next release of base. static BOOL didCheck = NO; if (!didCheck) { undoManagerCanCoalesce = [NSUndoManager instancesRespondToSelector: @selector(_canCoalesceUndoWithTarget:selector:object:)]; if (!undoManagerCanCoalesce) { NSLog(@"This version of NSUndoManager does not\n" @"support coalescing undo operations. " @"Upgrade gnustep-base to r29163 or newer to\n" @"get rid of this one-time warning."); } didCheck = YES; } } undo = [self undoManager]; /* Coalesce consecutive typing events into a single undo action using currently private undo manager functionality. An event is considered a typing event if it is a keyboard event and the event's characters match our replacement string. Note: Typing events are coalesced only when the previous action was a typing event too and the current character follows the previous one immediately. We never coalesce actions when the current selection is not empty. */ event = [NSApp currentEvent]; isTyping = [event type] == NSKeyDown && [[event characters] isEqualToString: replacementString]; if (undoManagerCanCoalesce && _undoObject) { undoRange = [_undoObject range]; if (isTyping && NSMaxRange(undoRange) == affectedCharRange.location && affectedCharRange.length == 0 && [undo _canCoalesceUndoWithTarget: _textStorage selector: @selector(_undoTextChange:) object: _undoObject]) { undoRange.length += [replacementString length]; [_undoObject setRange: undoRange]; return result; } DESTROY(_undoObject); } // The length of the undoRange is the length of the replacement, if any. if (replacementString != nil) { undoRange = NSMakeRange(affectedCharRange.location, [replacementString length]); } else { undoRange = affectedCharRange; } undoString = [self attributedSubstringFromRange: affectedCharRange]; undoObject = [[NSTextViewUndoObject alloc] initWithRange: undoRange attributedString: undoString]; [undo registerUndoWithTarget: _textStorage selector: @selector(_undoTextChange:) object: undoObject]; if (isTyping) _undoObject = undoObject; else RELEASE(undoObject); } return result; } - (BOOL) shouldChangeTextInRanges: (NSArray *)ranges replacementStrings: (NSArray *)strings { // FIXME NSRange range; range = [(NSValue*)[ranges objectAtIndex: 0] rangeValue]; return [self shouldChangeTextInRange: range replacementString: [strings objectAtIndex: 0]]; } /* After each user-induced change, this method should be called. */ - (void) didChangeText { [self scrollRangeToVisible: [self selectedRange]]; [notificationCenter postNotificationName: NSTextDidChangeNotification object: _notifObject]; if ([_window firstResponder] != self) { /* Copied from -resignFirstResponder . See comment above. */ if ([self shouldDrawInsertionPoint]) { [self updateInsertionPointStateAndRestartTimer: NO]; } if (_layoutManager != nil) { _layoutManager->_beganEditing = NO; } [notificationCenter postNotificationName: NSTextDidEndEditingNotification object: _notifObject]; } } /* Returns the ranges to which various kinds of user changes should apply. */ - (NSRange) rangeForUserCharacterAttributeChange { if (!_tf.is_editable || !_tf.uses_font_panel || !_layoutManager) { return NSMakeRange(NSNotFound, 0); } if (_tf.is_rich_text) { return _layoutManager->_selected_range; } else { return NSMakeRange(0, [_textStorage length]); } } - (NSRange) rangeForUserParagraphAttributeChange { if (!_tf.is_editable || !_tf.uses_ruler || !_layoutManager) { return NSMakeRange(NSNotFound, 0); } if (_tf.is_rich_text) { return [self selectionRangeForProposedRange: _layoutManager->_selected_range granularity: NSSelectByParagraph]; } else { return NSMakeRange(0, [_textStorage length]); } } - (NSRange) rangeForUserTextChange { if (!_tf.is_editable || !_layoutManager) { return NSMakeRange(NSNotFound, 0); } return _layoutManager->_selected_range; } - (NSRange) rangeForUserCompletion { NSUInteger length, location; NSRange range, space; // Get the current location. location = [self selectedRange].location; // Find the first space starting from current location, backwards. space = [[self string] rangeOfCharacterFromSet: [NSCharacterSet whitespaceAndNewlineCharacterSet] options: NSBackwardsSearch range: NSMakeRange(0, location)]; if (space.location == NSNotFound) { // No space was found. if (location > 0) { // Return the range of the whole substring. range = NSMakeRange(0, location); } else { // There isn't word. range = NSMakeRange(NSNotFound, 0); } } else { length = location - space.location - 1; if (length > 0) { // Return the range of the last word. range = NSMakeRange(space.location + 1, length); } else { // There isn't word at the end. range = NSMakeRange(NSNotFound, 0); } } return range; } - (NSArray *) rangesForUserCharacterAttributeChange { // FIXME NSRange range; range = [self rangeForUserCharacterAttributeChange]; if (range.location == NSNotFound) return nil; return [NSArray arrayWithObject: [NSValue valueWithRange: range]]; } - (NSArray *) rangesForUserParagraphAttributeChange { // FIXME NSRange range; range = [self rangeForUserParagraphAttributeChange]; if (range.location == NSNotFound) return nil; return [NSArray arrayWithObject: [NSValue valueWithRange: range]]; } - (NSArray *) rangesForUserTextChange { // FIXME NSRange range; range = [self rangeForUserTextChange]; if (range.location == NSNotFound) return nil; return [NSArray arrayWithObject: [NSValue valueWithRange: range]]; } /**** Misc. ****/ /* Scroll so that the beginning of the range is visible. */ - (void) scrollRangeToVisible: (NSRange)aRange { NSRect rect, r; NSView *cv; CGFloat width; NSPoint p0, p1; NSRange ourCharRange; /* Make sure that our size is up-to-date. If the scroll is in response to a change, the delayed size updating might not have run yet. To avoid scrolling outside the view, we force an update now. (An alternative would be to delay the scroll, too. If problems using this method turn up, it could be changed.) */ [self sizeToFit]; if (_layoutManager == nil) return; /* See if the requested range lies outside of the receiver. If so * forward the call to the appropriate text view. */ ourCharRange = [_layoutManager characterRangeForGlyphRange: [_layoutManager glyphRangeForTextContainer: [self textContainer]] actualGlyphRange: NULL]; if (NSMaxRange(aRange) < ourCharRange.location || aRange.location > NSMaxRange(ourCharRange)) { // FIXME: The following snippet could be refactored to a method called // _textContainerForCharacterRange:effectiveCharacterRange: NSTextContainer *tc; NSTextView *tv; NSUInteger glyphIndex = [_layoutManager glyphRangeForCharacterRange: aRange actualCharacterRange: NULL].location; // If we are asked to scroll to the empty range at the end of the string, // adjust the glyph index to be the last glyph, instead of one after the last glyph if (![_layoutManager isValidGlyphIndex: glyphIndex] && [_layoutManager isValidGlyphIndex: glyphIndex - 1]) { glyphIndex--; } tc = [_layoutManager textContainerForGlyphAtIndex: glyphIndex effectiveRange: NULL]; tv = [tc textView]; if (tv != self) { [tv scrollRangeToVisible: aRange]; return; } } if (aRange.length > 0) { aRange.length = 1; rect = [self rectForCharacterRange: aRange]; } else { rect = [_layoutManager insertionPointRectForCharacterIndex: aRange.location inTextContainer: _textContainer]; rect.origin.x += _textContainerOrigin.x; rect.origin.y += _textContainerOrigin.y; } cv = [self superview]; /* If we are a non-rich-text field editor in a clip view, we use some magic to get scrolling to behave better; if not, just scroll and return. */ if (!_tf.is_field_editor || _tf.is_rich_text || ![cv isKindOfClass: [NSClipView class]]) { [self scrollRectToVisible: rect]; return; } /* The basic principle is that we want to keep as much text as possible visible, and we want the text to appear to keep its alignment. This is especially important for centered text where the auto-scrolling will scroll so it appears right- or left-aligned if we don't do anything here. We also want to avoid spurious scrolling when the user is just moving the insertion point. */ width = [cv frame].size.width; /* Get the left and right edges of the text. */ r = [_layoutManager insertionPointRectForCharacterIndex: 0 inTextContainer: _textContainer]; p0 = r.origin; r = [_layoutManager insertionPointRectForCharacterIndex: [_textStorage length] inTextContainer: _textContainer]; p1 = r.origin; p1.x += r.size.width; /* We only try to be smart for single-line text. */ if (p0.y != p1.y) { [self scrollRectToVisible: rect]; return; } p0.x += _textContainerOrigin.x; p1.x += _textContainerOrigin.x; switch ([self alignment]) { case NSLeftTextAlignment: case NSNaturalTextAlignment: /* TODO? check default writing direction for language? */ case NSJustifiedTextAlignment: /* We don't want blank space to the right of the text; scroll further to the left if possible. */ p1.x -= width; if (p1.x < 0) p1.x = 0; if (p1.x < rect.origin.x) { rect.size.width += rect.origin.x - p1.x; rect.origin.x = p1.x; } break; case NSRightTextAlignment: /* We don't want blank space to the left of the text; scroll further to the right if possible. */ p0.x += width; if (p0.x > p1.x) p0.x = p1.x; if (p0.x > NSMaxX(rect)) { rect.size.width += p0.x - NSMaxX(rect); } break; case NSCenterTextAlignment: /* If all the text fits in the clip view, just center on the text. */ if (p1.x - p0.x <= width) { rect.origin.x = (p1.x + p0.x - width) / 2; rect.size.width = width; } else { /* Otherwise, fill the clip view with text; no blank space on either side. */ CGFloat x; x = p1.x - width; if (x < rect.origin.x) { rect.size.width += rect.origin.x - x; rect.origin.x = x; } x = p0.x + width; if (x > NSMaxX(rect)) { rect.size.width += x - NSMaxX(rect); } } break; } [self scrollRectToVisible: rect]; } - (NSRange) selectedRange { if (!_layoutManager) return NSMakeRange(0, 0); return _layoutManager->_selected_range; } - (BOOL) validateMenuItem: (NSMenuItem *)item { return [self validateUserInterfaceItem: item]; } - (BOOL) validateUserInterfaceItem: (id)item { SEL action = [item action]; // FIXME The list of validated actions below is far from complete if (sel_isEqual(action, @selector(cut:)) || sel_isEqual(action, @selector(delete:))) return [self isEditable] && [self selectedRange].length > 0; if (sel_isEqual(action, @selector(copy:))) return [self selectedRange].length > 0; if (sel_isEqual(action, @selector(copyFont:)) || sel_isEqual(action, @selector(copyRuler:))) return [self selectedRange].location != NSNotFound; if (sel_isEqual(action, @selector(paste:)) || sel_isEqual(action, @selector(pasteAsPlainText:)) || sel_isEqual(action, @selector(pasteAsRichText:)) || sel_isEqual(action, @selector(pasteFont:)) || sel_isEqual(action, @selector(pasteRuler:))) { if ([self isEditable]) { NSArray *types = nil; NSPasteboard *pb = nil; NSString *available; if (sel_isEqual(action, @selector(paste:))) { types = [self readablePasteboardTypes]; pb = [NSPasteboard generalPasteboard]; } else if (sel_isEqual(action, @selector(pasteAsPlainText:))) { types = [NSArray arrayWithObject: NSStringPboardType]; pb = [NSPasteboard generalPasteboard]; } else if (sel_isEqual(action, @selector(pasteAsRichText:))) { types = [NSArray arrayWithObject: NSRTFPboardType]; pb = [NSPasteboard generalPasteboard]; } else if (sel_isEqual(action, @selector(pasteFont:))) { types = [NSArray arrayWithObject: NSFontPboardType]; pb = [NSPasteboard pasteboardWithName: NSFontPboard]; } else if (sel_isEqual(action, @selector(pasteRuler:))) { types = [NSArray arrayWithObject: NSRulerPboard]; pb = [NSPasteboard pasteboardWithName: NSRulerPboard]; } available = [pb availableTypeFromArray: types]; return available != nil; } else return NO; } if (sel_isEqual(action, @selector(selectAll:)) || sel_isEqual(action, @selector(centerSelectionInVisibleArea:))) return [self isSelectable]; if (sel_isEqual(action, @selector(performFindPanelAction:))) { if ([self usesFindPanel] == NO) { return NO; } return [[GSTextFinder sharedTextFinder] validateFindPanelAction: item withTextView: self]; } return YES; } /* Private, internal methods to help input method handling in some backends (XIM, currently). Backends may override these in categories with the real (backend-specific) handling. */ - (void) _updateInputMethodState { } - (void) _updateInputMethodWithInsertionPoint: (NSPoint)insertionPoint { } @end /**** TODO: below this line hasn't been done yet ****/ /* not the same as NSMakeRange! */ static inline NSRange MakeRangeFromAbs (unsigned a1, unsigned a2) { if (a1 < a2) return NSMakeRange(a1, a2 - a1); else return NSMakeRange(a2, a1 - a2); } @implementation NSTextView (leftovers) /**** Misc. stuff not yet categorized ****/ /* This method is for user changes; see NSTextView_actions.m. */ - (void) changeColor: (id)sender { NSColor *aColor = (NSColor *)[sender color]; NSRange aRange = [self rangeForUserCharacterAttributeChange]; // FIXME: support undo /* Set typing attributes */ if (_layoutManager) { [_layoutManager->_typingAttributes setObject: aColor forKey: NSForegroundColorAttributeName]; [notificationCenter postNotificationName: NSTextViewDidChangeTypingAttributesNotification object: _notifObject]; } if (aRange.location == NSNotFound || aRange.length == 0) return; if (![self shouldChangeTextInRange: aRange replacementString: nil]) return; [self setTextColor: aColor range: aRange]; [self didChangeText]; } /* This method is for user changes; see NSTextView_actions.m. */ - (void) changeFont: (id)sender { NSRange foundRange; NSUInteger maxSelRange; NSRange aRange= [self rangeForUserCharacterAttributeChange]; NSRange searchRange = aRange; NSFont *font; if (aRange.location == NSNotFound) return; if (![self shouldChangeTextInRange: aRange replacementString: nil]) return; [_textStorage beginEditing]; for (maxSelRange = NSMaxRange (aRange); searchRange.location < maxSelRange; searchRange = NSMakeRange (NSMaxRange (foundRange), maxSelRange - NSMaxRange (foundRange))) { font = [_textStorage attribute: NSFontAttributeName atIndex: searchRange.location longestEffectiveRange: &foundRange inRange: searchRange]; if (font != nil) { [self setFont: [sender convertFont: font] ofRange: foundRange]; } } [_textStorage endEditing]; [self didChangeText]; /* Set typing attributes */ font = [_layoutManager->_typingAttributes objectForKey: NSFontAttributeName]; if (font != nil) { [_layoutManager->_typingAttributes setObject: [sender convertFont: font] forKey: NSFontAttributeName]; [notificationCenter postNotificationName: NSTextViewDidChangeTypingAttributesNotification object: _notifObject]; } } /* * [NSText] Reading and Writing RTFD files */ - (BOOL) readRTFDFromFile: (NSString *)path { NSAttributedString *peek; peek = [[NSAttributedString alloc] initWithPath: path documentAttributes: NULL]; if (peek != nil) { if (!_tf.is_rich_text) { [self setRichText: YES]; } [self replaceCharactersInRange: NSMakeRange (0, [_textStorage length]) withAttributedString: peek]; RELEASE(peek); return YES; } return NO; } - (BOOL) writeRTFDToFile: (NSString *)path atomically: (BOOL)flag { NSFileWrapper *wrapper; NSRange range = NSMakeRange (0, [_textStorage length]); wrapper = [_textStorage RTFDFileWrapperFromRange: range documentAttributes: nil]; return [wrapper writeToFile: path atomically: flag updateFilenames: YES]; } /* * [NSResponder] Handle enabling/disabling of services menu items. */ - (id) validRequestorForSendType: (NSString *)sendType returnType: (NSString *)returnType { BOOL sendOK = NO; BOOL returnOK = NO; if (!_layoutManager) return [super validRequestorForSendType: sendType returnType: returnType]; if (sendType == nil) { sendOK = YES; } else if (_layoutManager->_selected_range.length && [sendType isEqual: NSStringPboardType]) { sendOK = YES; } if (returnType == nil) { returnOK = YES; } else if (_tf.is_editable && [returnType isEqual: NSStringPboardType]) { returnOK = YES; } if (sendOK && returnOK) { return self; } return [super validRequestorForSendType: sendType returnType: returnType]; } - (void) setTypingAttributes: (NSDictionary *)attrs { NSDictionary *old_attrs; NSString *names[] = {NSParagraphStyleAttributeName, NSFontAttributeName, NSForegroundColorAttributeName}; int i; if (_layoutManager == nil) return; if (attrs == nil) { attrs = [[self class] defaultTypingAttributes]; } old_attrs = _layoutManager->_typingAttributes; if (_delegate) { SEL selector = @selector(textView:shouldChangeTypingAttributes:toAttributes:); if ([_delegate respondsToSelector: selector]) { if (![_delegate textView: self shouldChangeTypingAttributes: old_attrs toAttributes: attrs]) return; } } _layoutManager->_typingAttributes = [[NSMutableDictionary alloc] initWithDictionary: attrs]; // make sure to keep the main attributes set. for (i = 0; i < 3; i++) { NSString *name = names[i]; if ([attrs objectForKey: name] == nil) { [_layoutManager->_typingAttributes setObject: [old_attrs objectForKey: name] forKey: name]; } } RELEASE(old_attrs); [self updateFontPanel]; [self updateRuler]; [notificationCenter postNotificationName: NSTextViewDidChangeTypingAttributesNotification object: _notifObject]; } - (NSDictionary *) typingAttributes { return [NSDictionary dictionaryWithDictionary: _layoutManager->_typingAttributes]; } - (void) clickedOnLink: (id)link atIndex: (NSUInteger)charIndex { if (_delegate != nil) { SEL selector = @selector(textView:clickedOnLink:atIndex:); if ([_delegate respondsToSelector: selector]) { [_delegate textView: self clickedOnLink: link atIndex: charIndex]; } } } - (void) updateFontPanel { /* Update fontPanel only if told so */ if (_tf.uses_font_panel && _layoutManager) { NSRange longestRange; NSFontManager *fm = [NSFontManager sharedFontManager]; NSFont *currentFont; if (_layoutManager->_selected_range.length > 0) /* Multiple chars selection */ { currentFont = [_textStorage attribute: NSFontAttributeName atIndex: _layoutManager->_selected_range.location longestEffectiveRange: &longestRange inRange: _layoutManager->_selected_range]; [fm setSelectedFont: currentFont isMultiple: !NSEqualRanges (longestRange, _layoutManager->_selected_range)]; } else /* Just Insertion Point. */ { currentFont = [_layoutManager->_typingAttributes objectForKey: NSFontAttributeName]; [fm setSelectedFont: currentFont isMultiple: NO]; } } } /**** Smart insert/delete ****/ static inline BOOL is_preceded_by_smart_char(NSString *string, NSRange range) { NSUInteger start = range.location; return start == 0 || [smartLeftChars characterIsMember: [string characterAtIndex: start - 1]]; } static inline BOOL is_followed_by_smart_char(NSString *string, NSRange range) { NSUInteger end = NSMaxRange(range); return end == [string length] || [smartRightChars characterIsMember: [string characterAtIndex: end]]; } - (NSRange) smartDeleteRangeForProposedRange: (NSRange)proposedCharRange { if ([self smartInsertDeleteEnabled]) { NSString *string = [_textStorage string]; NSUInteger start = proposedCharRange.location; NSUInteger end = NSMaxRange(proposedCharRange); /* Like Mac OS X, we only propose a space character (0x20) for deletion and we choose either the character preceding the selection or the one after the selection, but not both. */ if (start > 0 && [string characterAtIndex: start - 1] == ' ' && is_followed_by_smart_char(string, proposedCharRange)) { proposedCharRange.location--; proposedCharRange.length++; } else if (end < [string length] && [string characterAtIndex: end] == ' ' && is_preceded_by_smart_char(string, proposedCharRange)) { proposedCharRange.length++; } } return proposedCharRange; } - (NSString *)smartInsertAfterStringForString: (NSString *)aString replacingRange: (NSRange)charRange { if ([self smartInsertDeleteEnabled] && aString != nil && !is_followed_by_smart_char([_textStorage string], charRange)) { NSUInteger l = [aString length]; NSCharacterSet *ws = [NSCharacterSet whitespaceAndNewlineCharacterSet]; if (l == 0 || ![ws characterIsMember: [aString characterAtIndex: l - 1]]) return @" "; } return nil; } - (NSString *)smartInsertBeforeStringForString: (NSString *)aString replacingRange: (NSRange)charRange { if ([self smartInsertDeleteEnabled] && aString != nil && !is_preceded_by_smart_char([_textStorage string], charRange)) { NSUInteger l = [aString length]; NSCharacterSet *ws = [NSCharacterSet whitespaceAndNewlineCharacterSet]; if (l == 0 || ![ws characterIsMember: [aString characterAtIndex: 0]]) return @" "; } return nil; } - (void) smartInsertForString: (NSString *)aString replacingRange: (NSRange)charRange beforeString: (NSString **)beforeString afterString: (NSString **)afterString { /* Determines whether whitespace needs to be added around aString to preserve proper spacing and punctuation when it's inserted into the receiver's text over charRange. Returns by reference in beforeString and afterString any whitespace that should be added, unless either or both is nil. Both are returned as nil if aString is nil or if smart insertion and deletion is disabled. As part of its implementation, this method calls -smartInsertAfterStringForString: replacingRange: and -smartInsertBeforeStringForString: replacingRange: .To change this method's behavior, override those two methods instead of this one. NSTextView uses this method as necessary. You can also use it in implementing your own methods that insert text. To do so, invoke this method with the proper arguments, then insert beforeString, aString, and afterString in order over charRange. */ if (beforeString) *beforeString = [self smartInsertBeforeStringForString: aString replacingRange: charRange]; if (afterString) *afterString = [self smartInsertAfterStringForString: aString replacingRange: charRange]; } - (void) toggleSmartInsertDelete: (id)sender { [self setSmartInsertDeleteEnabled: ![self smartInsertDeleteEnabled]]; } /**** Selection management ****/ - (void) setSelectedRange: (NSRange)charRange { [self setSelectedRange: charRange affinity: [self selectionAffinity] stillSelecting: NO]; } /** * Return a range of text which encompasses proposedCharRange but is * extended (if necessary) to match the type of selection specified by gr. */ - (NSRange) selectionRangeForProposedRange: (NSRange)proposedCharRange granularity: (NSSelectionGranularity)gr { NSUInteger index; NSRange aRange; NSRange newRange; NSString *string = [self string]; NSUInteger length = [string length]; if (NSMaxRange (proposedCharRange) > length) { proposedCharRange.length = length - proposedCharRange.location; } if (length == 0) { return proposedCharRange; } switch (gr) { case NSSelectByWord: index = proposedCharRange.location; if (index >= length) { index = length - 1; } newRange = [_textStorage doubleClickAtIndex: index]; if (proposedCharRange.length > 1) { index = NSMaxRange(proposedCharRange) - 1; if (index >= length) { index = length - 1; } aRange = [_textStorage doubleClickAtIndex: index]; newRange = NSUnionRange(newRange, aRange); } return newRange; case NSSelectByParagraph: return [string lineRangeForRange: proposedCharRange]; case NSSelectByCharacter: default: if (proposedCharRange.length == 0) return proposedCharRange; /* Expand the beginning character */ index = proposedCharRange.location; newRange = [string rangeOfComposedCharacterSequenceAtIndex: index]; /* If the proposedCharRange is empty we only ajust the beginning */ if (proposedCharRange.length == 0) { return newRange; } /* Expand the finishing character */ index = NSMaxRange (proposedCharRange) - 1; aRange = [string rangeOfComposedCharacterSequenceAtIndex: index]; newRange.length = NSMaxRange(aRange) - newRange.location; return newRange; } } - (void) setSelectedRange: (NSRange)charRange affinity: (NSSelectionAffinity)affinity stillSelecting: (BOOL)stillSelectingFlag { /* Note that this method might be called from the layout manager to update the selection after the text storage has been changed. If text was deleted, the old selected range might extend outside the current string. Also note that this happens during the processing of the changes in the text storage. Thus, it isn't safe to modify the text storage. */ /* The `official' (the last one the delegate approved of) selected range before this one. */ NSRange oldRange; /* If the user was interactively changing the selection, the last displayed selection could have been a temporary selection, different from the last official one: */ NSRange oldDisplayedRange; NSUInteger length = [_textStorage length]; oldDisplayedRange = _layoutManager->_selected_range; if (NSMaxRange(charRange) > length) { if (charRange.location > length) { charRange = NSMakeRange(length, 0); } else { charRange.length = length - charRange.location; } } if (stillSelectingFlag == YES) { /* Store the original range before the interactive selection process begin. That's because we will need to ask the delegate if it's all right for him to do the change, and then notify him we did. In both cases, we need to post the original selection together with the new one. */ if (_layoutManager->_original_selected_range.location == NSNotFound) { oldRange = _layoutManager->_original_selected_range = _layoutManager->_selected_range; } } else { /* Retrieve the original range */ if (_layoutManager->_original_selected_range.location != NSNotFound) { oldRange = _layoutManager->_original_selected_range; _layoutManager->_original_selected_range.location = NSNotFound; } else { oldRange = _layoutManager->_selected_range; } /* Ask delegate to modify the range */ if (_tf.delegate_responds_to_will_change_sel) { charRange = [_delegate textView: _notifObject willChangeSelectionFromCharacterRange: oldRange toCharacterRange: charRange]; } } /* Set the new selected range */ _layoutManager->_selected_range = charRange; /* Clear the remembered position and direction for insertion point movement. Note that we must _not_ clear the index. Many movement actions will reset the direction to a valid value, and they assume that the index remains unchanged here. */ _currentInsertionPointMovementDirection = 0; /* TODO: when and if to restart timer */ [self updateInsertionPointStateAndRestartTimer: !stillSelectingFlag]; if (stillSelectingFlag == NO) { [self updateFontPanel]; /* Insertion Point */ if (charRange.length) { /* TODO: this is a big bottle-neck since it involves talking to the pasteboard server. should do this "later" */ // Store the selected text in the selection pasteboard [self copySelection]; if (_tf.is_rich_text) { NSDictionary *dict; /* always set the typing attributes to the attributes of the first selected character */ dict = [_textStorage attributesAtIndex: charRange.location effectiveRange: NULL]; [self setTypingAttributes: dict]; } /* TODO: insertion point */ } else /* no selection, only insertion point */ { if (_tf.is_rich_text && length) { NSDictionary *dict; if (charRange.location > 0 && [[_textStorage string] characterAtIndex: (charRange.location - 1)] != '\n') { /* If the insertion point is after a bold word, for example, we need to use bold for further insertions - this is why we take the attributes from range.location - 1. */ dict = [_textStorage attributesAtIndex: (charRange.location - 1) effectiveRange: NULL]; } else { /* Unless we are at the beginning of text - we use the first valid attributes then */ dict = [_textStorage attributesAtIndex: charRange.location effectiveRange: NULL]; } [self setTypingAttributes: dict]; } } } if (_window != nil) { [_layoutManager invalidateDisplayForCharacterRange: oldDisplayedRange]; [_layoutManager invalidateDisplayForCharacterRange: charRange]; } [self setSelectionGranularity: NSSelectByCharacter]; _layoutManager->_selectionAffinity = affinity; /* TODO: Remove the marking from marked text if the new selection is greater than the marked region. */ if (stillSelectingFlag == NO) { NSDictionary *userInfo; userInfo = [NSDictionary dictionaryWithObjectsAndKeys: [NSValue valueWithBytes: &oldRange objCType: @encode(NSRange)], NSOldSelectedCharacterRange, nil]; [notificationCenter postNotificationName: NSTextViewDidChangeSelectionNotification object: _notifObject userInfo: userInfo]; } } - (NSSelectionAffinity) selectionAffinity { return _layoutManager->_selectionAffinity; } - (void) setSelectionGranularity: (NSSelectionGranularity)granularity { _layoutManager->_selectionGranularity = granularity; } - (NSSelectionGranularity) selectionGranularity { return _layoutManager->_selectionGranularity; } - (NSArray *) selectedRanges { // FIXME NSRange range; range = [self selectedRange]; return [NSArray arrayWithObject: [NSValue valueWithRange: range]]; } - (void) setSelectedRanges: (NSArray *)ranges { [self setSelectedRanges: ranges affinity: [self selectionAffinity] stillSelecting: NO]; } - (void) setSelectedRanges: (NSArray *)ranges affinity: (NSSelectionAffinity)affinity stillSelecting: (BOOL)flag { // FIXME NSRange range; range = [(NSValue*)[ranges objectAtIndex: 0] rangeValue]; [self setSelectedRange: range affinity: affinity stillSelecting: flag]; } /**** Drawing ****/ /* TODO: Figure out how the additional layout stuff is supposed to work. */ - (void) setNeedsDisplayInRect: (NSRect)rect avoidAdditionalLayout: (BOOL)flag { /* TODO: This is here until the layout manager is working */ /* This is very important */ [super setNeedsDisplayInRect: rect]; } /* We override NSView's setNeedsDisplayInRect: */ - (void) setNeedsDisplayInRect: (NSRect)aRect { [self setNeedsDisplayInRect: aRect avoidAdditionalLayout: NO]; } - (BOOL) shouldDrawInsertionPoint { if (!_layoutManager) return NO; if (_dragTargetLocation != NSNotFound) return YES; if (_layoutManager->_selected_range.length != 0) return NO; if (_tf.is_editable == NO) return NO; if ([_window isKeyWindow] == YES && [_window firstResponder] == self) return YES; return NO; } /* * It only makes real sense to call this method with `flag == YES'. * If you want to delete the insertion point, what you want is rather * to redraw what was under the insertion point - which can't be done * here - you need to set the rect as needing redisplay (without * additional layout) instead. NB: You need to flush the window after * calling this method if you want the insertion point to appear on * the screen immediately. This could only be needed to implement * blinking insertion point - but even there, it could probably be * done without. */ - (void) drawInsertionPointInRect: (NSRect)rect color: (NSColor *)color turnedOn: (BOOL)flag { if (_window == nil) { return; } if (flag) { if (color == nil) { color = _insertionPointColor; } [color set]; NSRectFill (rect); } else { [_backgroundColor set]; NSRectFill (rect); } } - (void) drawViewBackgroundInRect: (NSRect)rect { if (_tf.draws_background) { [_backgroundColor set]; NSRectFill(rect); } } - (void) drawRect: (NSRect)rect { NSRange drawnRange; NSRect containerRect = rect; containerRect.origin.x -= _textContainerOrigin.x; containerRect.origin.y -= _textContainerOrigin.y; if (_layoutManager) { drawnRange = [_layoutManager glyphRangeForBoundingRect: containerRect inTextContainer: _textContainer]; } else { drawnRange = NSMakeRange(0, 0); } [self _scheduleTextCheckingInVisibleRectIfNeeded]; /* FIXME: We should only draw inside of rect. This code is necessary * to remove markings of old glyphs. These would not be removed * by the following call to the layout manager because that only * paints the background of new glyphs. Depending on the * situation, there might be no new glyphs where the old glyphs * were! */ [self drawViewBackgroundInRect: [self bounds]]; [self drawCharactersInRange: drawnRange forContentView: self]; if ([self shouldDrawInsertionPoint] && [NSGraphicsContext currentContextDrawingToScreen]) { if (NSIntersectsRect(rect, _insertionPointRect)) { [self drawInsertionPointInRect: _insertionPointRect color: _insertionPointColor turnedOn: _drawInsertionPointNow]; } } // Remove any existing tooltips in the redrawn rectangle. if (_rFlags.has_tooltips != 0) { [[GSToolTips tipsForView: self] removeToolTipsInRect: rect]; } { NSRange r; NSUInteger i = drawnRange.location; NSUInteger end = i + drawnRange.length; while (i < end) { NSUInteger rectCount; NSRectArray rects; NSUInteger j; // Find the next tooltip id text = [_textStorage attribute: NSToolTipAttributeName atIndex: i effectiveRange: &r]; if (r.location > end) { return; } if (nil == text) { i = r.location + r.length; continue; } // If there is one, find the rectangles it uses. rects = [_layoutManager rectArrayForCharacterRange: r withinSelectedCharacterRange: NSMakeRange(0, 0) inTextContainer: _textContainer rectCount: &rectCount]; // Add this object as the tooltip provider for each rectangle for (j=0 ; jtag == tag) { NSPoint origin; NSUInteger startIndex; id value; // Origin is in window coordinate space origin = rect->rectangle.origin; // Point is an offset from this origin - translate it to the window's // coordinate space point.x += origin.x; point.y += origin.y; // Then translate it to the view's coordinate space point = [self convertPoint: point fromView: nil]; // Find out what the corresponding text is. startIndex = [self _characterIndexForPoint: point respectFraction: NO]; // Look up what the tooltip text should be. value = [_textStorage attribute: NSToolTipAttributeName atIndex: startIndex effectiveRange: NULL]; // The value of NSToolTipAttributeName should be an NSString, // but we deal with it being something else too. if (![value isKindOfClass: [NSString class]]) { value = [value description]; } return value; } END_FOR_IN(_tracking_rects) return nil; } - (void) updateInsertionPointStateAndRestartTimer: (BOOL)restartFlag { NSRect new; if (!_layoutManager) { _insertionPointRect = NSZeroRect; return; } if (_dragTargetLocation != NSNotFound) { _tf.drag_target_hijacks_insertion_point = YES; new = [_layoutManager insertionPointRectForCharacterIndex: _dragTargetLocation inTextContainer: _textContainer]; new.origin.x += _textContainerOrigin.x; new.origin.y += _textContainerOrigin.y; /* If the insertion would extend outside the view (e.g. because it's just to the right of a character on the far right edge of the view, a common case for right-aligned text), we force it back in. */ if (NSMaxX(new) > NSMaxX(_bounds)) { new.origin.x = NSMaxX(_bounds) - new.size.width; } } else if (_layoutManager->_selected_range.length > 0 || _layoutManager->_selected_range.location == NSNotFound || !restartFlag) { new = NSZeroRect; } else { new = [_layoutManager insertionPointRectForCharacterIndex: _layoutManager->_selected_range.location inTextContainer: _textContainer]; new.origin.x += _textContainerOrigin.x; new.origin.y += _textContainerOrigin.y; /* If the insertion would extend outside the view (e.g. because it's just to the right of a character on the far right edge of the view, a common case for right-aligned text), we force it back in. */ if (NSMaxX(new) > NSMaxX(_bounds)) { new.origin.x = NSMaxX(_bounds) - new.size.width; } } /* Handle hijacked insertion point (either set above when entering this method or during the previous call to this method) */ if (_tf.drag_target_hijacks_insertion_point) { _drawInsertionPointNow = NO; [self setNeedsDisplayInRect: _insertionPointRect avoidAdditionalLayout: YES]; _insertionPointRect = new; if (_dragTargetLocation != NSNotFound) { _drawInsertionPointNow = YES; [self setNeedsDisplayInRect: _insertionPointRect avoidAdditionalLayout: YES]; } else _tf.drag_target_hijacks_insertion_point = NO; } /* Otherwise, draw insertion point only if there is a need to do so */ else if ([self shouldDrawInsertionPoint] || _drawInsertionPointNow) { if (restartFlag) { /* Start blinking timer if not yet started */ if (_insertionPointTimer == nil && [self shouldDrawInsertionPoint]) { // Save new insertion point rectangle before starting the insertion timer... _insertionPointRect = new; [self _startInsertionTimer]; } else if (_insertionPointTimer != nil) { if (!NSEqualRects(new, _insertionPointRect)) { // Erase previous insertion point line... _drawInsertionPointNow = NO; [self setNeedsDisplayInRect: _insertionPointRect avoidAdditionalLayout: YES]; _insertionPointRect = new; } } /* Ok - blinking has just been turned on. Make sure we start * the on/off/on/off blinking from the 'on', because in that way * the user can see where the insertion point is as soon as * possible. */ _drawInsertionPointNow = YES; [self setNeedsDisplayInRect: _insertionPointRect avoidAdditionalLayout: YES]; } else if ([self shouldDrawInsertionPoint] && (_insertionPointTimer != nil)) { // restartFlag is set to NO when control resigns first responder // status or window resings key window status. So we invalidate // timer to avoid extra method calls [self _stopInsertionTimer]; _drawInsertionPointNow = NO; [self setNeedsDisplayInRect: _insertionPointRect avoidAdditionalLayout: YES]; _insertionPointRect = new; } [self _updateInputMethodWithInsertionPoint: _insertionPointRect.origin]; } } /**** Pagination *****/ - (void) adjustPageHeightNew: (CGFloat*)newBottom top: (CGFloat)oldTop bottom: (CGFloat)oldBottom limit: (CGFloat)bottomLimit { BOOL needsToMoveBottom = NO; NSRect proposedPage = NSMakeRect([self bounds].origin.x, oldTop, [self bounds].size.width, oldBottom - oldTop); NSRange pageGlyphRange = [_layoutManager glyphRangeForBoundingRect: proposedPage inTextContainer: _textContainer]; CGFloat actualTextBottom = oldBottom; // the maximum Y value of text less than oldBottom NSInteger i; for (i = NSMaxRange(pageGlyphRange) - 1; i >= (NSInteger)pageGlyphRange.location; ) { NSRange lineFragGlyphRange = NSMakeRange(0, 0); NSRect lineFragRect = [_layoutManager lineFragmentRectForGlyphAtIndex: i effectiveRange: &lineFragGlyphRange]; if (NSMaxY(lineFragRect) <= NSMaxY(proposedPage)) { actualTextBottom = NSMaxY(lineFragRect); break; } else { // We encountered a visible glyph fragment which extents below // the bottom of the page needsToMoveBottom = YES; } i = lineFragGlyphRange.location - 1; } if (needsToMoveBottom) { *newBottom = actualTextBottom; } else { *newBottom = oldBottom; } // FIXME: Do we need a special case so text attachments aren't split in half? } - (CGFloat)heightAdjustLimit { return 1.0; } /**** Ruler handling ****/ /** * Mote movement of marker */ - (void) rulerView: (NSRulerView *)ruler didMoveMarker: (NSRulerMarker *)marker { NSTextTab *old_tab = (NSTextTab*)[marker representedObject]; NSTextTab *new_tab = [[NSTextTab alloc] initWithType: [old_tab tabStopType] location: [marker markerLocation]]; NSRange range = [self rangeForUserParagraphAttributeChange]; NSUInteger loc = range.location; NSParagraphStyle *style; NSMutableParagraphStyle *mstyle; [_textStorage beginEditing]; while (loc < NSMaxRange(range)) { id value; NSRange effRange; NSRange newRange; value = [_textStorage attribute: NSParagraphStyleAttributeName atIndex: loc effectiveRange: &effRange]; newRange = NSIntersectionRange (effRange, range); if (value == nil) { value = [self defaultParagraphStyle]; } value = [value mutableCopy]; [value removeTabStop: old_tab]; [value addTabStop: new_tab]; [_textStorage addAttribute: NSParagraphStyleAttributeName value: value range: newRange]; RELEASE(value); loc = NSMaxRange (effRange); } [_textStorage endEditing]; [self didChangeText]; // Set the typing attributes style = [_layoutManager->_typingAttributes objectForKey: NSParagraphStyleAttributeName]; if (style == nil) style = [self defaultParagraphStyle]; mstyle = [style mutableCopy]; [mstyle removeTabStop: old_tab]; [mstyle addTabStop: new_tab]; // TODO: Should use setTypingAttributes [_layoutManager->_typingAttributes setObject: mstyle forKey: NSParagraphStyleAttributeName]; [notificationCenter postNotificationName: NSTextViewDidChangeTypingAttributesNotification object: _notifObject]; RELEASE(mstyle); [marker setRepresentedObject: new_tab]; RELEASE(new_tab); } /** * Handle removal of marker. */ - (void) rulerView: (NSRulerView *)ruler didRemoveMarker: (NSRulerMarker *)marker { NSTextTab *tab = (NSTextTab *)[marker representedObject]; NSRange range = [self rangeForUserParagraphAttributeChange]; NSUInteger loc = range.location; NSParagraphStyle *style; NSMutableParagraphStyle *mstyle; [_textStorage beginEditing]; while (loc < NSMaxRange(range)) { id value; NSRange effRange; NSRange newRange; value = [_textStorage attribute: NSParagraphStyleAttributeName atIndex: loc effectiveRange: &effRange]; newRange = NSIntersectionRange (effRange, range); if (value == nil) { value = [self defaultParagraphStyle]; } value = [value mutableCopy]; [value removeTabStop: tab]; [_textStorage addAttribute: NSParagraphStyleAttributeName value: value range: newRange]; RELEASE(value); loc = NSMaxRange (effRange); } [_textStorage endEditing]; [self didChangeText]; // Set the typing attributes style = [_layoutManager->_typingAttributes objectForKey: NSParagraphStyleAttributeName]; if (style == nil) style = [self defaultParagraphStyle]; mstyle = [style mutableCopy]; [mstyle removeTabStop: tab]; // TODO: Should use setTypingAttributes [_layoutManager->_typingAttributes setObject: mstyle forKey: NSParagraphStyleAttributeName]; [notificationCenter postNotificationName: NSTextViewDidChangeTypingAttributesNotification object: _notifObject]; RELEASE(mstyle); } - (void) rulerView: (NSRulerView *)ruler didAddMarker: (NSRulerMarker *)marker { NSTextTab *old_tab = (NSTextTab *)[marker representedObject]; NSTextTab *new_tab = [[NSTextTab alloc] initWithType: [old_tab tabStopType] location: [marker markerLocation]]; NSRange range = [self rangeForUserParagraphAttributeChange]; NSUInteger loc = range.location; NSParagraphStyle *style; NSMutableParagraphStyle *mstyle; [_textStorage beginEditing]; while (loc < NSMaxRange(range)) { id value; NSRange effRange; NSRange newRange; value = [_textStorage attribute: NSParagraphStyleAttributeName atIndex: loc effectiveRange: &effRange]; newRange = NSIntersectionRange (effRange, range); if (value == nil) { value = [self defaultParagraphStyle]; } value = [value mutableCopy]; [value addTabStop: new_tab]; [_textStorage addAttribute: NSParagraphStyleAttributeName value: value range: newRange]; RELEASE(value); loc = NSMaxRange (effRange); } [_textStorage endEditing]; [self didChangeText]; // Set the typing attributes style = [_layoutManager->_typingAttributes objectForKey: NSParagraphStyleAttributeName]; if (style == nil) style = [self defaultParagraphStyle]; mstyle = [style mutableCopy]; [mstyle addTabStop: new_tab]; // TODO: Should use setTypingAttributes [_layoutManager->_typingAttributes setObject: mstyle forKey: NSParagraphStyleAttributeName]; [notificationCenter postNotificationName: NSTextViewDidChangeTypingAttributesNotification object: _notifObject]; RELEASE(mstyle); [marker setRepresentedObject: new_tab]; RELEASE(new_tab); } /** * Set new marker position from mouse down location. */ - (void) rulerView: (NSRulerView *)ruler handleMouseDown: (NSEvent *)event { NSPoint point = [ruler convertPoint: [event locationInWindow] fromView: nil]; CGFloat location = point.x; NSRulerMarker *marker = [[NSRulerMarker alloc] initWithRulerView: ruler markerLocation: location image: [NSImage imageNamed: @"common_LeftTabStop"] imageOrigin: NSMakePoint(0, 0)]; NSTextTab *tab = [[NSTextTab alloc] initWithType: NSLeftTabStopType location: location]; [marker setRepresentedObject: tab]; [ruler trackMarker: marker withMouseEvent: event]; RELEASE(marker); RELEASE(tab); } /** * Return YES if the marker should be added, NO otherwise. */ - (BOOL) rulerView: (NSRulerView *)ruler shouldAddMarker: (NSRulerMarker *)marker { return [self shouldChangeTextInRange: [self rangeForUserParagraphAttributeChange] replacementString: nil]; } /** * Return YES if the marker should be moved, NO otherwise. */ - (BOOL) rulerView: (NSRulerView *)ruler shouldMoveMarker: (NSRulerMarker *)marker { return [self shouldChangeTextInRange: [self rangeForUserParagraphAttributeChange] replacementString: nil]; } /** * Return YES if the marker should be removed, NO otherwise. */ - (BOOL) rulerView: (NSRulerView *)ruler shouldRemoveMarker: (NSRulerMarker *)marker { return [(NSObject *)[marker representedObject] isKindOfClass: [NSTextTab class]]; } /** * Return a position for adding by constraining the specified location. */ - (CGFloat) rulerView: (NSRulerView *)ruler willAddMarker: (NSRulerMarker *)marker atLocation: (CGFloat)location { NSSize size = [_textContainer containerSize]; if (location < 0.0) return 0.0; if (location > size.width) return size.width; return location; } /** * Return a new position by constraining the specified location. */ - (CGFloat) rulerView: (NSRulerView *)ruler willMoveMarker: (NSRulerMarker *)marker toLocation: (CGFloat)location { NSSize size = [_textContainer containerSize]; if (location < 0.0) return 0.0; if (location > size.width) return size.width; return location; } - (void) updateRuler { NSScrollView *sv; NSRulerView *rv; /* Update ruler view only if told so */ if (_tf.uses_ruler && _tf.is_ruler_visible && (sv = [self enclosingScrollView]) != nil && (rv = [sv horizontalRulerView]) != nil && [rv clientView] == self) { NSParagraphStyle *paraStyle; NSArray *makers; if (_layoutManager->_selected_range.length > 0) /* Multiple chars selection */ { paraStyle = [_textStorage attribute: NSParagraphStyleAttributeName atIndex: _layoutManager->_selected_range.location effectiveRange: NULL]; } else { paraStyle = [_layoutManager->_typingAttributes objectForKey: NSParagraphStyleAttributeName]; } makers = [_layoutManager rulerMarkersForTextView: self paragraphStyle: paraStyle ruler: rv]; [rv setMarkers: makers]; } } /**** Spell checking ****/ - (void) checkSpelling: (id)sender { NSSpellChecker *sp = [NSSpellChecker sharedSpellChecker]; NSString *misspelledWord = nil; NSRange errorRange; int count = 0; if (sp == nil) { return; } errorRange = [sp checkSpellingOfString: [self string] startingAt: NSMaxRange (_layoutManager->_selected_range) language: [sp language] wrap: YES inSpellDocumentWithTag: [self spellCheckerDocumentTag] wordCount: &count]; if (errorRange.length) { [self setSelectedRange: errorRange]; [self scrollRangeToVisible: errorRange]; misspelledWord = [[self string] substringFromRange: errorRange]; [sp updateSpellingPanelWithMisspelledWord: misspelledWord]; } else { [sp updateSpellingPanelWithMisspelledWord: @""]; } } - (void) changeSpelling: (id)sender { [self insertText: [[(NSControl *)sender selectedCell] stringValue]]; } - (void) ignoreSpelling: (id)sender { NSSpellChecker *sp = [NSSpellChecker sharedSpellChecker]; [sp ignoreWord: [[(NSControl *)sender selectedCell] stringValue] inSpellDocumentWithTag: [self spellCheckerDocumentTag]]; } - (NSInteger) spellCheckerDocumentTag { if (!_spellCheckerDocumentTag) _spellCheckerDocumentTag = [NSSpellChecker uniqueSpellDocumentTag]; return _spellCheckerDocumentTag; } /**** Pasteboard handling ****/ - (NSString *) preferredPasteboardTypeFromArray: (NSArray *)availableTypes restrictedToTypesFromArray: (NSArray *)allowedTypes { NSEnumerator *enumerator; NSString *type; if (availableTypes == nil) return nil; if (allowedTypes == nil) return [availableTypes objectAtIndex: 0]; enumerator = [allowedTypes objectEnumerator]; while ((type = [enumerator nextObject]) != nil) { if ([availableTypes containsObject: type]) { return type; } } return nil; } - (BOOL) readSelectionFromPasteboard: (NSPasteboard *)pboard { /* Reads the text view's preferred type of data from the pasteboard specified by the pboard parameter. This method invokes the preferredPasteboardTypeFromArray: restrictedToTypesFromArray: method to determine the text view's preferred type of data and then reads the data using the readSelectionFromPasteboard: type: method. Returns YES if the data was successfully read. */ NSString *type; type = [self preferredPasteboardTypeFromArray: [pboard types] restrictedToTypesFromArray: [self readablePasteboardTypes]]; if (type == nil) return NO; return [self readSelectionFromPasteboard: pboard type: type]; } /* This method is for user changes; see NSTextView_actions.m. (Note that this makes it impossible to paste programmatically, but it's necessary to get the -shouldChangeTextInRange:replacementString: calls right.) */ - (BOOL) readSelectionFromPasteboard: (NSPasteboard *)pboard type: (NSString *)type { /* Reads data of the given type from pboard. The new data is placed at the current insertion point, replacing the current selection if one exists. Returns YES if the data was successfully read. You should override this method to read pasteboard types other than the default types. Use the rangeForUserTextChange method to obtain the range of characters (if any) to be replaced by the new data. */ NSRange changeRange = [self rangeForUserTextChange]; if ([type isEqualToString: NSStringPboardType]) { if (changeRange.location != NSNotFound) { NSString *s = [pboard stringForType: NSStringPboardType]; if (s != nil) { if ([self smartInsertDeleteEnabled] && [pboard dataForType: NSSmartPastePboardType] != nil) { NSString *after, *before; [self smartInsertForString: s replacingRange: changeRange beforeString: &before afterString: &after]; if (before) s = [before stringByAppendingString: s]; if (after) s = [s stringByAppendingString: after]; } if ([self shouldChangeTextInRange: changeRange replacementString: s]) { [self replaceCharactersInRange: changeRange withString: s]; [self didChangeText]; changeRange.length = [s length]; [self setSelectedRange: NSMakeRange(NSMaxRange(changeRange),0)]; } } else return NO; } return YES; } /* TODO: When inserting attributed strings, I call -shouldChangeTextInRange:replacementString: with the string from the attributed string. Need to decide if this is correct (probably; not much else to do), or if I need to do something else. */ if (_tf.is_rich_text) { if ([type isEqualToString: NSRTFPboardType]) { if (changeRange.location != NSNotFound) { NSMutableAttributedString *as; NSData *d = [pboard dataForType: NSRTFPboardType]; as = [[NSMutableAttributedString alloc] initWithRTF: d documentAttributes: NULL]; if ([self smartInsertDeleteEnabled] && [pboard dataForType: NSSmartPastePboardType] != nil) { NSString *after, *before; [self smartInsertForString: [as string] replacingRange: changeRange beforeString: &before afterString: &after]; if (before) { [as replaceCharactersInRange: NSMakeRange(0, 0) withString: before]; } if (after) { [as replaceCharactersInRange: NSMakeRange([as length], 0) withString: after]; } } if ([self shouldChangeTextInRange: changeRange replacementString: [as string]]) { [self replaceCharactersInRange: changeRange withAttributedString: as]; [self didChangeText]; changeRange.length = [as length]; [self setSelectedRange: NSMakeRange(NSMaxRange(changeRange),0)]; } DESTROY(as); } return YES; } } if (_tf.imports_graphics) { if ([type isEqualToString: NSRTFDPboardType]) { if (changeRange.location != NSNotFound) { NSMutableAttributedString *as; NSData *d = [pboard dataForType: NSRTFDPboardType]; as = [[NSMutableAttributedString alloc] initWithRTFD: d documentAttributes: NULL]; if ([self smartInsertDeleteEnabled] && [pboard dataForType: NSSmartPastePboardType] != nil) { NSString *after, *before; [self smartInsertForString: [as string] replacingRange: changeRange beforeString: &before afterString: &after]; if (before) { [as replaceCharactersInRange: NSMakeRange(0, 0) withString: before]; } if (after) { [as replaceCharactersInRange: NSMakeRange([as length], 0) withString: after]; } } if ([self shouldChangeTextInRange: changeRange replacementString: [as string]]) { [self replaceCharactersInRange: changeRange withAttributedString: as]; [self didChangeText]; changeRange.length = [as length]; [self setSelectedRange: NSMakeRange(NSMaxRange(changeRange),0)]; } DESTROY(as); } return YES; } if ([type isEqualToString: NSTIFFPboardType]) { if (changeRange.location != NSNotFound) { NSData *pboardData = [pboard dataForType: NSTIFFPboardType]; NSFileWrapper *wrapper = [[NSFileWrapper alloc] initRegularFileWithContents: pboardData]; NSImage *image = [[NSImage alloc] initWithData: pboardData]; NSTextAttachment *attachment = [[NSTextAttachment alloc] initWithFileWrapper: wrapper]; NSAttributedString *as = [NSAttributedString attributedStringWithAttachment: attachment]; [wrapper setIcon: image]; /* We changed the wrapper's icon. Reset the attachment to force an update of the attachment's cell's image. */ [attachment setFileWrapper: wrapper]; if ([self shouldChangeTextInRange: changeRange replacementString: [as string]]) { [self replaceCharactersInRange: changeRange withAttributedString: as]; [self didChangeText]; changeRange.length = [as length]; [self setSelectedRange: NSMakeRange(NSMaxRange(changeRange),0)]; } RELEASE(attachment); RELEASE(image); RELEASE(wrapper); } return YES; } if ([type isEqualToString: NSFileContentsPboardType]) { NSTextAttachment *attachment = [[NSTextAttachment alloc] initWithFileWrapper: [pboard readFileWrapper]]; NSAttributedString *as = [NSAttributedString attributedStringWithAttachment: attachment]; if (changeRange.location != NSNotFound && [self shouldChangeTextInRange: changeRange replacementString: [as string]]) { [self replaceCharactersInRange: changeRange withAttributedString: as]; [self didChangeText]; changeRange.length = [as length]; [self setSelectedRange: NSMakeRange(NSMaxRange(changeRange),0)]; } RELEASE(attachment); return YES; } } // color accepting if ([type isEqualToString: NSColorPboardType]) { /* This is an attribute change, so we use a different range. */ NSRange aRange = [self rangeForUserCharacterAttributeChange]; if (aRange.location != NSNotFound && [self shouldChangeTextInRange: aRange replacementString: nil]) { NSColor *color = [NSColor colorFromPasteboard: pboard]; NSMutableDictionary *d = [[self typingAttributes] mutableCopy]; [self setTextColor: color range: aRange]; [d setObject: color forKey: NSForegroundColorAttributeName]; [self setTypingAttributes: d]; RELEASE(d); [self didChangeText]; } return YES; } // font pasting if ([type isEqualToString: NSFontPboardType]) { NSData *data = [pboard dataForType: NSFontPboardType]; NSDictionary *dict = [NSUnarchiver unarchiveObjectWithData: data]; if (dict != nil) { /* This is an attribute change, so we use a different range. */ NSRange aRange = [self rangeForUserCharacterAttributeChange]; NSMutableDictionary *d; if (aRange.location != NSNotFound && [self shouldChangeTextInRange: aRange replacementString: nil]) { [_textStorage addAttributes: dict range: aRange]; d = [[self typingAttributes] mutableCopy]; [d addEntriesFromDictionary: dict]; [self setTypingAttributes: d]; RELEASE(d); [self didChangeText]; } return YES; } return NO; } // ruler pasting if ([type isEqualToString: NSRulerPboardType]) { NSData *data = [pboard dataForType: NSRulerPboardType]; NSDictionary *dict = [NSUnarchiver unarchiveObjectWithData: data]; if (dict != nil) { /* This is an attribute change, so we use a different range. */ NSRange aRange = [self rangeForUserParagraphAttributeChange]; NSMutableDictionary *d; if (aRange.location != NSNotFound && [self shouldChangeTextInRange: aRange replacementString: nil]) { [_textStorage addAttributes: dict range: aRange]; d = [[self typingAttributes] mutableCopy]; [d addEntriesFromDictionary: dict]; [self setTypingAttributes: d]; RELEASE(d); [self didChangeText]; } return YES; } return NO; } return NO; } - (NSArray *) readablePasteboardTypes { // get default types, what are they? NSMutableArray *ret = [NSMutableArray arrayWithObjects: NSRulerPboardType, NSColorPboardType, NSFontPboardType, nil]; if (_tf.imports_graphics) { [ret addObject: NSRTFDPboardType]; [ret addObject: NSTIFFPboardType]; [ret addObject: NSFileContentsPboardType]; } if (_tf.is_rich_text) { [ret addObject: NSRTFPboardType]; } [ret addObject: NSStringPboardType]; return ret; } - (NSArray *) writablePasteboardTypes { // the selected text can be written to the pasteboard with which types. if ([self smartInsertDeleteEnabled]) { /* We write this type to the pasteboard, but it does not have a meaning by itself. Therefore, we do not add it in -readablePasteboardTypes. */ NSMutableArray *ret = [[self readablePasteboardTypes] mutableCopy]; [ret addObject: NSSmartPastePboardType]; return [ret autorelease]; } return [self readablePasteboardTypes]; } - (BOOL) writeSelectionToPasteboard: (NSPasteboard *)pboard type: (NSString *)type { /* Writes the current selection to pboard using the given type. Returns YES if the data was successfully written. You can override this method to add support for writing new types of data to the pasteboard. You should invoke super's implementation of the method to handle any types of data your overridden version does not. */ BOOL ret = NO; if ([type isEqualToString: NSStringPboardType]) { ret = [pboard setString: [[self string] substringWithRange: _layoutManager->_selected_range] forType: NSStringPboardType]; } else if ([type isEqualToString: NSRTFPboardType]) { ret = [pboard setData: [self RTFFromRange: _layoutManager->_selected_range] forType: NSRTFPboardType]; } else if ([type isEqualToString: NSRTFDPboardType]) { ret = [pboard setData: [self RTFDFromRange: _layoutManager->_selected_range] forType: NSRTFDPboardType]; } else if ([type isEqualToString: NSSmartPastePboardType] && [self selectionGranularity] == NSSelectByWord) { ret = [pboard setData: [NSData data] forType: NSSmartPastePboardType]; } else if ([type isEqualToString: NSColorPboardType]) { NSColor *color; color = [_textStorage attribute: NSForegroundColorAttributeName atIndex: _layoutManager->_selected_range.location effectiveRange: 0]; if (color != nil) { [color writeToPasteboard: pboard]; ret = YES; } } else if ([type isEqualToString: NSFontPboardType]) { NSDictionary *dict; dict = [_textStorage fontAttributesInRange: _layoutManager->_selected_range]; if (dict != nil) { ret = [pboard setData: [NSArchiver archivedDataWithRootObject: dict] forType: NSFontPboardType]; } } else if ([type isEqualToString: NSRulerPboardType]) { NSDictionary *dict; dict = [_textStorage rulerAttributesInRange: _layoutManager->_selected_range]; if (dict != nil) { ret = [pboard setData: [NSArchiver archivedDataWithRootObject: dict] forType: NSRulerPboardType]; } } return ret; } - (BOOL) writeSelectionToPasteboard: (NSPasteboard *)pboard types: (NSArray *)types { /* Writes the current selection to pboard under each type in the types array. Returns YES if the data for any single type was written successfully. You should not need to override this method. You might need to invoke this method if you are implementing a new type of pasteboard to handle services other than copy/paste or dragging. */ BOOL ret = NO; NSEnumerator *enumerator; NSString *type; if (types == nil) { return NO; } if (_layoutManager->_selected_range.location == NSNotFound) { return NO; } [pboard declareTypes: types owner: self]; enumerator = [types objectEnumerator]; while ((type = [enumerator nextObject]) != nil) { if ([self writeSelectionToPasteboard: pboard type: type]) { ret = YES; } } return ret; } - (NSArray *) acceptableDragTypes { return [self readablePasteboardTypes]; } - (void) updateDragTypeRegistration { /* TODO: Should change registration for all our text views This seems to be handled already; this will be called when the flags change, and those calls are made on all connected text views. */ if (_tf.is_editable) [self registerForDraggedTypes: [self acceptableDragTypes]]; else [self unregisterDraggedTypes]; } /**** Drag and drop handling ****/ /* * Note: Dragging uses a transient insertion point distinct from the text * view's current selection. This allows subclasses of NSTextView to determine * the origin of the dragged selection during a drag operation and provides * better visual feedback for users, since the origin of the dragged selection * remains visible. */ // dragging of text, colors and files - (NSDragOperation)draggingSourceOperationMaskForLocal:(BOOL)isLocal { return (NSDragOperationGeneric | NSDragOperationCopy); } - (void) _updateDragTargetLocation: (id )sender operation: (NSDragOperation *)flags { if (*flags != NSDragOperationNone) { NSPoint dragPoint; NSUInteger dragIndex; dragPoint = [sender draggingLocation]; dragPoint = [self convertPoint: dragPoint fromView: nil]; dragIndex = [self _characterIndexForPoint: dragPoint respectFraction: YES]; if ([sender draggingSource] != self || dragIndex <= [self selectedRange].location || dragIndex >= NSMaxRange([self selectedRange])) { _dragTargetLocation = dragIndex; } else { _dragTargetLocation = NSNotFound; *flags = NSDragOperationNone; } } else if (_dragTargetLocation != NSNotFound) { _dragTargetLocation = NSNotFound; } [self updateInsertionPointStateAndRestartTimer: NO]; [self displayIfNeeded]; } - (NSDragOperation) draggingEntered: (id )sender { NSPasteboard *pboard = [sender draggingPasteboard]; NSArray *types = [self readablePasteboardTypes]; NSString *type = [self preferredPasteboardTypeFromArray: [pboard types] restrictedToTypesFromArray: types]; NSDragOperation flags = [self dragOperationForDraggingInfo: sender type: type]; if (![type isEqual: NSColorPboardType]) { [self _updateDragTargetLocation: sender operation: &flags]; } return flags; } - (NSDragOperation) draggingUpdated: (id )sender { NSPasteboard *pboard = [sender draggingPasteboard]; NSArray *types = [self readablePasteboardTypes]; NSString *type = [self preferredPasteboardTypeFromArray: [pboard types] restrictedToTypesFromArray: types]; NSDragOperation flags = [self dragOperationForDraggingInfo: sender type: type]; NSPoint dragPoint; NSRect vRect; dragPoint = [sender draggingLocation]; dragPoint = [self convertPoint: dragPoint fromView: nil]; /* Note: Keep DRAGGING_SCROLL_BORDER in sync with a similar value used * in -draggingUpdated: of NSOutlineView and NSTableView. * FIXME: Is the value of DRAGGING_SCROLL_DIST reasonable? */ #define DRAGGING_SCROLL_BORDER 3 #define DRAGGING_SCROLL_DIST 10 vRect = [self visibleRect]; if (dragPoint.x <= NSMinX(vRect) + DRAGGING_SCROLL_BORDER) { vRect.origin.x -= DRAGGING_SCROLL_DIST; if (NSMinX(vRect) < NSMinX(_bounds)) vRect.origin.x = NSMinX(_bounds); } else if (dragPoint.x >= NSMaxX(vRect) - DRAGGING_SCROLL_BORDER) { vRect.origin.x += DRAGGING_SCROLL_DIST; if (NSMaxX(vRect) > NSMaxX(_bounds)) vRect.origin.x = NSMaxX(_bounds) - NSWidth(vRect); } if (dragPoint.y <= NSMinY(vRect) + DRAGGING_SCROLL_BORDER) { vRect.origin.y -= DRAGGING_SCROLL_DIST; if (NSMinY(vRect) < NSMinY(_bounds)) vRect.origin.y = NSMinY(_bounds); } else if (dragPoint.y >= NSMaxY(vRect) - DRAGGING_SCROLL_BORDER) { vRect.origin.y += DRAGGING_SCROLL_DIST; if (NSMaxY(vRect) > NSMaxY(_bounds)) vRect.origin.y = NSMaxY(_bounds) - NSHeight(vRect); } [self scrollPoint: vRect.origin]; #undef DRAGGING_SCROLL_BORDER #undef DRAGGING_SCROLL_DIST if (![type isEqual: NSColorPboardType]) { [self _updateDragTargetLocation: sender operation: &flags]; } return flags; } - (void) draggingExited: (id )sender { NSDragOperation flags = NSDragOperationNone; [self _updateDragTargetLocation: sender operation: &flags]; } - (BOOL) prepareForDragOperation: (id )sender { return YES; } - (BOOL) performDragOperation: (id )sender { /* In general, the position where the dragging source was dropped is given by _dragTargetLocation. However, when dragging a color onto a text view, the cursor is not updated, since the color change always effects the current selection and hence _dragTargetLocation==NSNotFound in that case. */ NSSelectionGranularity gran = [self selectionGranularity]; NSRange sourceRange = [self selectedRange]; NSRange changeRange; NSPasteboard *pboard; NSString *type; if (_dragTargetLocation != NSNotFound) { changeRange = NSMakeRange(_dragTargetLocation, 0); _dragTargetLocation = NSNotFound; [self setSelectedRange: changeRange]; } if ([sender draggingSource] == self && ([sender draggingSourceOperationMask] & NSDragOperationGeneric)) { if ([self smartInsertDeleteEnabled] && gran == NSSelectByWord) sourceRange = [self smartDeleteRangeForProposedRange: sourceRange]; if (![self shouldChangeTextInRange: sourceRange replacementString: @""]) { return NO; } [self replaceCharactersInRange: sourceRange withString: @""]; } changeRange = [self rangeForUserTextChange]; pboard = [sender draggingPasteboard]; type = [self preferredPasteboardTypeFromArray: [pboard types] restrictedToTypesFromArray: [self readablePasteboardTypes]]; if ([self readSelectionFromPasteboard: pboard type: type]) { if (![type isEqual: NSColorPboardType]) { changeRange.length = [self selectedRange].location - changeRange.location; [self setSelectedRange: changeRange]; } return YES; } return NO; } - (void) concludeDragOperation: (id )sender { [self cleanUpAfterDragOperation]; } - (void) cleanUpAfterDragOperation { // Cleanup information after dragging operation completes... _dragTargetLocation = NSNotFound; [self updateInsertionPointStateAndRestartTimer: NO]; [self displayIfNeeded]; } - (NSDragOperation) dragOperationForDraggingInfo: (id )dragInfo type: (NSString *)type { //TODO return NSDragOperationCopy | NSDragOperationGeneric; } - (NSImage *) dragImageForSelectionWithEvent: (NSEvent *)event origin: (NSPoint *)origin { if (origin) *origin = NSMakePoint(0, 0); // FIXME return nil; } - (BOOL) dragSelectionWithEvent: (NSEvent *)event offset: (NSSize)mouseOffset slideBack: (BOOL)slideBack { NSPoint point; NSImage *image = [self dragImageForSelectionWithEvent: event origin: &point]; NSPasteboard *pboard = [NSPasteboard pasteboardWithName: NSDragPboard]; NSPoint location = [self convertPoint: [event locationInWindow] fromView: nil]; NSMutableArray *types = [NSMutableArray array]; if (_tf.imports_graphics) [types addObject: NSRTFDPboardType]; if (_tf.is_rich_text) [types addObject: NSRTFPboardType]; [types addObject: NSStringPboardType]; if ([self smartInsertDeleteEnabled]) [types addObject: NSSmartPastePboardType]; [self writeSelectionToPasteboard: pboard types: types]; [self dragImage: image at: location offset: mouseOffset event: event pasteboard: pboard source: self slideBack: slideBack]; return YES; } /**** Event handling ****/ - (void) mouseDown: (NSEvent *)theEvent { BOOL canDrag = NO; NSSelectionAffinity affinity = [self selectionAffinity]; NSSelectionGranularity granularity = NSSelectByCharacter; NSRange chosenRange, proposedRange; NSPoint point, startPoint; NSUInteger startIndex, endIndex; /* If non selectable then ignore the mouse down. */ if (_tf.is_selectable == NO) { return; } if (!_layoutManager) return; /* Otherwise, NSWindow has already made us first responder (if possible) */ startPoint = [self convertPoint: [theEvent locationInWindow] fromView: nil]; startIndex = [self _characterIndexForPoint: startPoint respectFraction: [theEvent clickCount] == 1]; if ([theEvent modifierFlags] & NSShiftKeyMask) { /* Shift-click is for extending or shrinking an existing selection using the existing granularity */ proposedRange = _layoutManager->_selected_range; granularity = _layoutManager->_selectionGranularity; /* If the clicked point is closer to the left end of the current selection adjust the left end of the selected range */ if (startIndex < proposedRange.location + proposedRange.length / 2) { proposedRange = NSMakeRange(startIndex, NSMaxRange(proposedRange) - startIndex); proposedRange = [self selectionRangeForProposedRange: proposedRange granularity: granularity]; /* Prepare for shift-dragging. Anchor is at the right end. */ startIndex = NSMaxRange(proposedRange); } /* otherwise, adjust the right end of the selected range */ else { proposedRange = NSMakeRange(proposedRange.location, startIndex - proposedRange.location); proposedRange = [self selectionRangeForProposedRange: proposedRange granularity: granularity]; /* Prepare for shift-dragging. Anchor is at the left end. */ startIndex = proposedRange.location; } } else /* No shift */ { switch ([theEvent clickCount]) { case 1: granularity = NSSelectByCharacter; break; case 2: granularity = NSSelectByWord; break; case 3: granularity = NSSelectByParagraph; break; } /* A single click into the selected range can start a drag operation */ canDrag = granularity == NSSelectByCharacter && NSLocationInRange(startIndex, _layoutManager->_selected_range); proposedRange = NSMakeRange (startIndex, 0); /* We manage clicks on attachments and links only on the first click, so that if you double-click on them, only the first click gets sent to them; the other clicks select by word/paragraph as usual. */ if (granularity == NSSelectByCharacter) { NSTextAttachment *attachment; NSInteger startIndexNoFraction; /* since we look for an attachment with by-character granularity, recalculate the index without resepecting fraction */ startIndexNoFraction = [self _characterIndexForPoint: startPoint respectFraction: NO]; /* Check if the click was on an attachment cell. */ attachment = [_textStorage attribute: NSAttachmentAttributeName atIndex: startIndexNoFraction effectiveRange: NULL]; if (attachment != nil) { id cell = [attachment attachmentCell]; if (cell != nil) { NSRect cellFrame; NSRect lfRect; NSUInteger glyphIndex; glyphIndex = [_layoutManager glyphRangeForCharacterRange: NSMakeRange(startIndex, 1) actualCharacterRange: NULL].location; lfRect = [_layoutManager lineFragmentRectForGlyphAtIndex: glyphIndex effectiveRange: NULL]; cellFrame.origin = [_layoutManager locationForGlyphAtIndex: glyphIndex]; cellFrame.origin.x += lfRect.origin.x; cellFrame.origin.y += lfRect.origin.y; cellFrame.origin.x += _textContainerOrigin.x; cellFrame.origin.y += _textContainerOrigin.y; cellFrame.size = [_layoutManager attachmentSizeForGlyphAtIndex: glyphIndex]; cellFrame.origin.y -= cellFrame.size.height; /* TODO: What about the insertion point ? */ if ([cell wantsToTrackMouseForEvent: theEvent inRect: cellFrame ofView: self atCharacterIndex: startIndexNoFraction] && [cell trackMouse: theEvent inRect: cellFrame ofView: self atCharacterIndex: startIndexNoFraction untilMouseUp: NO]) { return; } } } /* This is the code for handling click event on a link (a link is some chars with the NSLinkAttributeName set to something which is not-null, a NSURL object usually). */ { /* What exactly is this link object, it's up to the programmer who is using the NSTextView and who originally created the link object and saved it under the NSLinkAttributeName in the text. Normally, a NSURL object is used. */ /* TODO: should call -clickedOnLink:atIndex: instead */ id link = [_textStorage attribute: NSLinkAttributeName atIndex: startIndex effectiveRange: NULL]; if (link != nil && _delegate != nil) { SEL selector = @selector(textView:clickedOnLink:atIndex:); if ([_delegate respondsToSelector: selector]) { /* Move the insertion point over the link. */ chosenRange = [self selectionRangeForProposedRange: proposedRange granularity: granularity]; [self setSelectedRange: chosenRange affinity: affinity stillSelecting: NO]; [self displayIfNeeded]; /* Now 'activate' the link. The _delegate returns YES if it handles the click, NO if it doesn't -- and if it doesn't, we need to pass the click to the next responder. */ if ([_delegate textView: self clickedOnLink: link atIndex: startIndex]) { return; } else { [super mouseDown: theEvent]; return; } } } } } } chosenRange = [self selectionRangeForProposedRange: proposedRange granularity: granularity]; if (canDrag) { NSUInteger mask = NSLeftMouseDraggedMask | NSLeftMouseUpMask; NSEvent *currentEvent; currentEvent = [_window nextEventMatchingMask: mask untilDate: [NSDate distantFuture] inMode: NSEventTrackingRunLoopMode dequeue: NO]; if ([currentEvent type] == NSLeftMouseDragged) { if (![self dragSelectionWithEvent: theEvent offset: NSMakeSize(0, 0) slideBack: YES]) { NSBeep(); } return; } } /* Enter modal loop tracking the mouse */ { NSUInteger mask = NSLeftMouseDraggedMask | NSLeftMouseUpMask | NSPeriodicMask; NSEvent *currentEvent; NSEvent *lastEvent = nil; /* Last non-periodic event. */ NSDate *distantPast = [NSDate distantPast]; BOOL gettingPeriodic, gotPeriodic; [self setSelectedRange: chosenRange affinity: affinity stillSelecting: YES]; currentEvent = [_window nextEventMatchingMask: mask untilDate: [NSDate distantFuture] inMode: NSEventTrackingRunLoopMode dequeue: YES]; gettingPeriodic = NO; do { gotPeriodic = NO; while (currentEvent && [currentEvent type] != NSLeftMouseUp) { if ([currentEvent type] == NSPeriodic) { gotPeriodic = YES; } else lastEvent = currentEvent; currentEvent = [_window nextEventMatchingMask: mask untilDate: distantPast inMode: NSEventTrackingRunLoopMode dequeue: YES]; } if (currentEvent && [currentEvent type] == NSLeftMouseUp) break; /* Automatic scrolling. If we aren't getting periodic events, we check all events. If any of them causes us to scroll, we scroll and start up the periodic events. If we are getting periodic events, we only scroll when we receive a periodic event (and we use the last non-periodic event to determine how far). If no scrolling occurred, we stop the periodic events. */ if (!gettingPeriodic) { if ([self autoscroll: lastEvent]) { gettingPeriodic = YES; [NSEvent startPeriodicEventsAfterDelay: 0.1 withPeriod: 0.1]; } } else if (gotPeriodic) { if (![self autoscroll: lastEvent]) { gettingPeriodic = NO; [NSEvent stopPeriodicEvents]; } } point = [self convertPoint: [lastEvent locationInWindow] fromView: nil]; endIndex = [self _characterIndexForPoint: point respectFraction: YES]; /** * If the mouse is not inside the receiver, see if it is over another * text view with the same layout manager. If so, use that text view * to compute endIndex */ if (![self mouse: point inRect: [self bounds]]) { BOOL found = NO; // FIXME: Is there an easier way to find the view under a point? NSView *winContentView = [[self window] contentView]; NSView *view = [winContentView hitTest: [[winContentView superview] convertPoint: [lastEvent locationInWindow] fromView: nil]]; for (; view != nil; view = [view superview]) { if ([view isKindOfClass: [NSTextView class]] && [(NSTextView*)view layoutManager] == [self layoutManager]) { found = YES; break; } } if (found) { NSTextView *textview = (NSTextView*)view; NSPoint mouse = [textview convertPoint: [lastEvent locationInWindow] fromView: nil]; endIndex = [textview _characterIndexForPoint: mouse respectFraction: YES]; } } proposedRange = MakeRangeFromAbs(endIndex, startIndex); chosenRange = [self selectionRangeForProposedRange: proposedRange granularity: granularity]; [self setSelectedRange: chosenRange affinity: affinity stillSelecting: YES]; currentEvent = [_window nextEventMatchingMask: mask untilDate: [NSDate distantFuture] inMode: NSEventTrackingRunLoopMode dequeue: YES]; } while ([currentEvent type] != NSLeftMouseUp); if (gettingPeriodic) { [NSEvent stopPeriodicEvents]; } } NSDebugLog(@"chosenRange. location = %d, length = %d\n", (int)chosenRange.location, (int)chosenRange.length); [self setSelectedRange: chosenRange affinity: affinity stillSelecting: NO]; /* Remember granularity till a new selection destroys the memory */ [self setSelectionGranularity: granularity]; } static const NSInteger GSSpellingSuggestionMenuItemTag = 1; - (void) _acceptGuess: (id)sender { NSString *guess = [(NSMenuItem*)sender title]; NSRange range = [self selectedRange]; if (![self shouldChangeTextInRange: range replacementString: guess]) return; [_textStorage beginEditing]; [self replaceCharactersInRange: range withString: guess]; [_textStorage endEditing]; [self didChangeText]; } - (void) rightMouseDown: (NSEvent *)theEvent { NSInteger i; NSMenu *menu = [[self class] defaultMenu]; NSPoint point = [self convertPoint: [theEvent locationInWindow] fromView: nil]; NSUInteger index = [self _characterIndexForPoint: point respectFraction: YES]; if (_tf.is_selectable == NO) return; if (!_layoutManager) return; for (i = [[menu itemArray] count] - 1; i >= 0; --i) { NSMenuItem *item = [menu itemAtIndex: i]; if ([item tag] == GSSpellingSuggestionMenuItemTag) { [menu removeItemAtIndex: i]; } } if (!(index >= [self selectedRange].location && index <= NSMaxRange([self selectedRange]))) { NSRange wordRange = [self selectionRangeForProposedRange: NSMakeRange(index, 0) granularity: NSSelectByWord]; [self setSelectedRange: wordRange affinity: [self selectionAffinity] stillSelecting: NO]; } if (NSEqualRanges([self selectedRange], [self selectionRangeForProposedRange: NSMakeRange([self selectedRange].location, 0) granularity: NSSelectByWord]) && [self selectedRange].length > 0 && [self isEditable]) { // Possibly show spelling suggestions NSSpellChecker *sp = [NSSpellChecker sharedSpellChecker]; NSString *word = [[self string] substringWithRange: [self selectedRange]]; if (sp != nil && [sp checkSpellingOfString: word startingAt: 0].location != NSNotFound) { /*NSArray *guesses = [sp guessesForWordRange: wordRange inString: [self string] language: [sp language] inSpellDocumentWithTag: [self spellCheckerDocumentTag]];*/ NSArray *guesses = [sp guessesForWord: word]; if ([guesses count] > 0) { for (i=0; i < [guesses count] && i < 5; i++) { NSString *guess = [guesses objectAtIndex: i]; [menu insertItemWithTitle: guess action:@selector(_acceptGuess:) keyEquivalent:@"" atIndex:i]; [[menu itemAtIndex: i] setTarget: self]; [[menu itemAtIndex: i] setTag: GSSpellingSuggestionMenuItemTag]; } [menu insertItem: [NSMenuItem separatorItem] atIndex: i]; [[menu itemAtIndex: i] setTag: GSSpellingSuggestionMenuItemTag]; } else { [menu insertItemWithTitle: _(@"No Suggestions") action:(SEL)0 keyEquivalent:@"" atIndex:0]; [[menu itemAtIndex: 0] setTag: GSSpellingSuggestionMenuItemTag]; [[menu itemAtIndex: 0] setEnabled: NO]; [menu insertItem: [NSMenuItem separatorItem] atIndex: 1]; [[menu itemAtIndex: 1] setTag: GSSpellingSuggestionMenuItemTag]; } } } [NSMenu popUpContextMenu: menu withEvent: theEvent forView: self]; } - (void) keyDown: (NSEvent *)theEvent { // If not editable, don't recognize the key down if (_tf.is_editable == NO) { [super keyDown: theEvent]; } else { [self interpretKeyEvents: [NSArray arrayWithObject: theEvent]]; } } /* Bind other mouse up to pasteSelection. This should be done via configuation! */ - (void) otherMouseUp: (NSEvent *)theEvent { // TODO: Should we change the insertion point, based on the event position? [self pasteSelection]; } - (void) startSpeaking:(id) sender { // FIXME } - (void) stopSpeaking:(id) sender { // FIXME } - (BOOL) acceptsGlyphInfo { return _tf.accepts_glyph_info; } - (void) setAcceptsGlyphInfo: (BOOL)flag { _tf.accepts_glyph_info = flag; } - (BOOL) allowsDocumentBackgroundColorChange { return _tf.allows_document_background_color_change; } - (void) setAllowsDocumentBackgroundColorChange: (BOOL)flag { _tf.allows_document_background_color_change = flag; } - (void) changeDocumentBackgroundColor: (id)sender { if ([self allowsDocumentBackgroundColorChange]) { NSColor *aColor = (NSColor *)[sender color]; [self setBackgroundColor: aColor]; } } - (void) changeAttributes: (id)sender { NSDictionary *attributes; NSRange aRange = [self rangeForUserCharacterAttributeChange]; NSRange foundRange; NSUInteger location; NSUInteger maxSelRange = NSMaxRange(aRange); if (aRange.location == NSNotFound) return; if (![self shouldChangeTextInRange: aRange replacementString: nil]) return; [_textStorage beginEditing]; for (location = aRange.location; location < maxSelRange; location = NSMaxRange(foundRange)) { attributes = [_textStorage attributesAtIndex: location effectiveRange: &foundRange]; if (attributes != nil) { attributes = [sender convertAttributes: attributes]; [_textStorage setAttributes: attributes range: foundRange]; } } [_textStorage endEditing]; [self didChangeText]; [self setTypingAttributes: [sender convertAttributes: [self typingAttributes]]]; } - (void) breakUndoCoalescing { DESTROY(_undoObject); } - (void) complete: (id)sender { NSRange range = [self rangeForUserCompletion]; if ([self isEditable] && range.location != NSNotFound && range.length != 0) { GSAutocompleteWindow *window = [GSAutocompleteWindow defaultWindow]; [window displayForTextView: self]; } } - (NSArray *) completionsForPartialWordRange: (NSRange)range indexOfSelectedItem: (NSInteger *)index { if ([_delegate respondsToSelector: @selector(textView:completions:forPartialWordRange:indexOfSelectedItem:)]) { return [_delegate textView: self completions: [[GSAutocompleteWindow defaultWindow] words] forPartialWordRange: range indexOfSelectedItem: index]; } return nil; } - (void) insertCompletion: (NSString *)word forPartialWordRange: (NSRange)range movement: (NSInteger)movement isFinal: (BOOL)flag { NSString *complete; NSString *partial = [word substringToIndex: range.length]; if (![self shouldChangeTextInRange: range replacementString: partial]) return; complete = [word stringByDeletingPrefix: partial]; [_textStorage beginEditing]; [self replaceCharactersInRange: range withString: partial]; [_textStorage endEditing]; [self didChangeText]; [self insertText: complete]; [self didChangeText]; if (!flag && ([self selectedRange].length == 0) ) { [self setSelectedRange: NSMakeRange(NSMaxRange(range), [complete length])]; } } - (void) orderFrontLinkPanel: (id)sender { // FIXME } - (void) orderFrontListPanel: (id)sender { // FIXME } - (void) orderFrontTablePanel: (id)sender { // FIXME } - (void) performFindPanelAction: (id)sender { [[GSTextFinder sharedTextFinder] performFindPanelAction: sender withTextView: self]; } // NSTextFinder methods implementation... // isSelectable, isEditable, string, selectedRanges, setSelectedRanges, replaceCharactersInRange:withString: // implemented by NSTextView already... - (BOOL) allowsMultipleSelection { return NO; } - (NSString *) stringAtIndex: (NSUInteger)characterIndex effectiveRange: (NSRangePointer)outRange endsWithSearchBoundary: (BOOL *)outFlag { return [self string]; } - (NSUInteger) stringLength { return [[self string] length]; } - (NSRange) firstSelectedRange { NSValue *r = [[self selectedRanges] objectAtIndex: 0]; return [r rangeValue]; } - (BOOL) shouldReplaceCharactersInRanges: (NSArray *)ranges withStrings: (NSArray *)strings { NSUInteger idx = 0; FOR_IN(NSValue*, rv, ranges) { NSRange r = [rv rangeValue]; NSString *str = [strings objectAtIndex: idx]; if (![self shouldChangeTextInRange: r replacementString: str]) { return NO; } idx++; } END_FOR_IN(ranges); return YES; } - (void) didReplaceCharacters { [self didChangeText]; } - (NSView *) contentViewAtIndex: (NSUInteger)index effectiveCharacterRange: (NSRangePointer)outRange { return nil; } - (NSArray *) rectsForCharacterRange: (NSRange)range { NSUInteger rectCount = 0; NSRect *rects; NSMutableArray *result = [NSMutableArray array]; NSUInteger idx = 0; rects = [_layoutManager rectArrayForCharacterRange: range withinSelectedCharacterRange: NSMakeRange(NSNotFound, 0) inTextContainer: _textContainer rectCount: &rectCount]; for (idx = 0; idx < rectCount; idx++) { NSRect r = rects[idx]; NSValue *v = [NSValue valueWithRect: r]; [result addObject: v]; } return result; } - (NSArray *) visibleCharacterRanges { NSArray *result = nil; if (_layoutManager) { const NSRect visibleRect = [self visibleRect]; NSRange visibleGlyphRange = [_layoutManager glyphRangeForBoundingRect: visibleRect inTextContainer: _textContainer]; NSRange visibleRange = [_layoutManager characterRangeForGlyphRange: visibleGlyphRange actualGlyphRange: NULL]; NSValue *value = [NSValue valueWithRange: visibleRange]; result = [NSArray arrayWithObject: value]; } return result; } - (void) drawCharactersInRange: (NSRange)range forContentView: (NSView *)view { /* Then draw the special background of the new glyphs. */ [_layoutManager drawBackgroundForGlyphRange: range atPoint: _textContainerOrigin]; [_layoutManager drawGlyphsForGlyphRange: range atPoint: _textContainerOrigin]; } @end @implementation NSTextView (GNUstepPrivate) /* TODO: make sure this is only called when _layoutManager is known non-nil, or add guards */ - (NSUInteger) _characterIndexForPoint: (NSPoint)point respectFraction: (BOOL)respectFraction { NSUInteger index; CGFloat fraction; point.x -= _textContainerOrigin.x; point.y -= _textContainerOrigin.y; if ([_layoutManager extraLineFragmentTextContainer] == _textContainer) { NSRect extraRect = [_layoutManager extraLineFragmentRect]; if (point.y >= NSMinY(extraRect)) return [_textStorage length]; } index = [_layoutManager glyphIndexForPoint: point inTextContainer: _textContainer fractionOfDistanceThroughGlyph: &fraction]; // FIXME The layoutManager should never return -1. // Assume that the text is empty if this happens. if (index == NSNotFound) return 0; index = [_layoutManager characterIndexForGlyphAtIndex: index]; if (respectFraction && fraction > 0.5 && index < [_textStorage length] && [[_textStorage string] characterAtIndex:index] != '\n') { index++; } return index; } - (void) _blink: (NSTimer *)t { if (_drawInsertionPointNow) { _drawInsertionPointNow = NO; } else { _drawInsertionPointNow = YES; } [self setNeedsDisplayInRect: _insertionPointRect avoidAdditionalLayout: YES]; /* Because we are called by a timer which is independent of any event processing in the gui runloop, we need to manually update the window. */ [self displayIfNeeded]; } - (void) _stopInsertionTimer { if (_insertionPointTimer != nil) { [_insertionPointTimer invalidate]; DESTROY(_insertionPointTimer); } } - (void) _startInsertionTimer { if (_insertionPointTimer != nil) { NSWarnMLog(@"Starting insertion timer with existing one running"); [self _stopInsertionTimer]; } _insertionPointTimer = [NSTimer scheduledTimerWithTimeInterval: 0.5 target: self selector: @selector(_blink:) userInfo: nil repeats: YES]; [[NSRunLoop currentRunLoop] addTimer: _insertionPointTimer forMode: NSModalPanelRunLoopMode]; RETAIN(_insertionPointTimer); } - (NSRect) rectForCharacterRange: (NSRange)aRange { NSRange glyphRange; NSRect rect; if (!aRange.length || !_layoutManager) return NSZeroRect; glyphRange = [_layoutManager glyphRangeForCharacterRange: aRange actualCharacterRange: NULL]; rect = [_layoutManager boundingRectForGlyphRange: glyphRange inTextContainer: _textContainer]; rect.origin.x += _textContainerOrigin.x; rect.origin.y += _textContainerOrigin.y; return rect; } /** Extension method that copies the current selected text to the special section pasteboard */ - (void) copySelection { #if 1 NSString *newSelection; NSString *oldSelection; NSRange range = _layoutManager->_selected_range; if (range.location != NSNotFound && range.length != 0) { newSelection = [[self string] substringWithRange: range]; oldSelection = [[NSPasteboard pasteboardWithName: @"Selection"] stringForType: NSStringPboardType]; if (oldSelection != nil && ![newSelection isEqualToString: oldSelection]) { NSPasteboard *secondary; secondary = [NSPasteboard pasteboardWithName: @"Secondary"]; [secondary declareTypes: [NSArray arrayWithObject: NSStringPboardType] owner: self]; [secondary setString: oldSelection forType: NSStringPboardType]; } else { return; } } else { return; } #endif [self writeSelectionToPasteboard: [NSPasteboard pasteboardWithName: @"Selection"] types: [NSArray arrayWithObject: NSStringPboardType]]; } /** Extension method that pastes the current selected text from the special section pasteboard */ - (void) pasteSelection { #if 1 NSString *newSelection; NSString *oldSelection; NSRange range = _layoutManager->_selected_range; if (range.location != NSNotFound && range.length != 0) { newSelection = [[self string] substringWithRange: range]; oldSelection = [[NSPasteboard pasteboardWithName: @"Selection"] stringForType: NSStringPboardType]; if (oldSelection != nil && [newSelection isEqualToString: oldSelection]) { [self readSelectionFromPasteboard: [NSPasteboard pasteboardWithName: @"Secondary"] type: NSStringPboardType]; return; } } #endif [self readSelectionFromPasteboard: [NSPasteboard pasteboardWithName: @"Selection"] type: NSStringPboardType]; } /** * Text checking */ - (void) _checkTextInRange: (NSRange)aRange { NSRange longestRange; id value = [_layoutManager temporaryAttribute: @"NSTextChecked" atCharacterIndex: aRange.location longestEffectiveRange: &longestRange inRange: aRange]; longestRange = NSIntersectionRange(longestRange, aRange); if ([value boolValue] && NSEqualRanges(longestRange, aRange)) { //NSLog(@"No need to check in range %@", NSStringFromRange(aRange)); return; } // Check all of aRange [_layoutManager removeTemporaryAttribute: NSSpellingStateAttributeName forCharacterRange: aRange]; { NSSpellChecker *sp = [NSSpellChecker sharedSpellChecker]; NSInteger start = 0; int count; // FIXME: doing the spellcheck on this substring could create false-positives // if a word is split in half.. so the range should be expanded to the // nearest word boundary NSString *substring = [[self string] substringWithRange: aRange]; if (sp == nil) { return; } do { NSRange errorRange = [sp checkSpellingOfString: substring startingAt: start language: [sp language] wrap: YES inSpellDocumentWithTag: [self spellCheckerDocumentTag] wordCount: &count]; if (errorRange.location < start) { break; } if (errorRange.length > 0) { start = NSMaxRange(errorRange); } else { break; } errorRange = NSMakeRange(aRange.location + errorRange.location, errorRange.length); //NSLog(@"highlighting mistake: %@", [[self string] substringWithRange: errorRange]); [_layoutManager addTemporaryAttribute: NSSpellingStateAttributeName value: [NSNumber numberWithInteger: NSSpellingStateSpellingFlag] forCharacterRange: errorRange]; } while (1); [_layoutManager addTemporaryAttribute: @"NSTextChecked" value: [NSNumber numberWithBool: YES] forCharacterRange: aRange]; } } - (void) _textCheckingTimerFired: (NSTimer *)t { _textCheckingTimer = nil; if (nil == _layoutManager) return; { const NSRect visibleRect = [self visibleRect]; NSRange visibleGlyphRange = [_layoutManager glyphRangeForBoundingRect: visibleRect inTextContainer: _textContainer]; NSRange visibleRange = [_layoutManager characterRangeForGlyphRange: visibleGlyphRange actualGlyphRange: NULL]; [self _checkTextInRange: visibleRange]; _lastCheckedRect = visibleRect; } } - (void) _scheduleTextCheckingTimer { if ([[NSUserDefaults standardUserDefaults] boolForKey: @"GSDisableSpellCheckerServer"] == NO) { [_textCheckingTimer invalidate]; _textCheckingTimer = [NSTimer scheduledTimerWithTimeInterval: 0.5 target: self selector: @selector(_textCheckingTimerFired:) userInfo: [NSValue valueWithRect: [self visibleRect]] repeats: NO]; } } - (void) _scheduleTextCheckingInVisibleRectIfNeeded { if (_tf.continuous_spell_checking) { const NSRect visibleRect = [self visibleRect]; if (!NSEqualRects(visibleRect, _lastCheckedRect)) { if (nil != _textCheckingTimer && NSEqualRects(visibleRect, [[_textCheckingTimer userInfo] rectValue])) { return; } [self _scheduleTextCheckingTimer]; } } } /** * If selected has a nonzero length, return it unmodified. * If selected is touching a word, expand it to cover the entire word * Otherwise return selected unmodified */ - (NSRange) _rangeToInvalidateSpellingForSelectionRange: (NSRange)selected { if (selected.length > 0) { return selected; } else { NSRange prevNonCharacter; NSRange nextNonCharacter; NSRange range; NSCharacterSet *boundary = [[NSCharacterSet letterCharacterSet] invertedSet]; if (selected.location == [[self string] length] && selected.location > 0) { selected.location--; } prevNonCharacter = [[self string] rangeOfCharacterFromSet: boundary options: NSBackwardsSearch range: NSMakeRange(0, selected.location)]; nextNonCharacter = [[self string] rangeOfCharacterFromSet: boundary options: 0 range: NSMakeRange(NSMaxRange(selected), [[self string] length] - NSMaxRange(selected))]; if (prevNonCharacter.length == 0) { range.location = 0; } else { range.location = prevNonCharacter.location + 1; } if (nextNonCharacter.length == 0) { range.length = [[self string] length] - range.location; } else { range.length = nextNonCharacter.location - range.location; } return range; } } - (void) _textDidChange: (NSNotification*)notif { if (_tf.continuous_spell_checking) { // FIXME: This uses the caret position to guess what change caused // the NSTextDidChangeNotification and mark that range of the text as requiring re-checking. // // It would be better to use accurate information to decide what range // need its spelling state invalidated. NSRange range = [self _rangeToInvalidateSpellingForSelectionRange: [self selectedRange]]; if (range.length > 0) { [_layoutManager removeTemporaryAttribute: @"NSTextChecked" forCharacterRange: range]; [_layoutManager removeTemporaryAttribute: NSSpellingStateAttributeName forCharacterRange: range]; } [self _scheduleTextCheckingTimer]; } } - (void) _becomeRulerClient { NSScrollView *sv; NSRulerView *rv; if (_tf.uses_ruler && _tf.is_ruler_visible && (sv = [self enclosingScrollView]) != nil && (rv = [sv horizontalRulerView]) != nil) { [rv setClientView: self]; } } - (void) _resignRulerClient { NSScrollView *sv; NSRulerView *rv; if (_tf.uses_ruler && _tf.is_ruler_visible && (sv = [self enclosingScrollView]) != nil && (rv = [sv horizontalRulerView]) != nil) { [rv setClientView: nil]; } } @end @implementation NSTextViewUndoObject - (id) initWithRange: (NSRange)aRange attributedString: (NSAttributedString *)aString { if ((self = [super init]) != nil) { range = aRange; string = RETAIN(aString); } return self; } - (void) dealloc { RELEASE(string); [super dealloc]; } - (NSRange) range { return range; } - (void) setRange: (NSRange)aRange { range = aRange; } - (void) performUndo: (NSTextStorage *)aTextStorage { NSTextView *tv = [self bestTextViewForTextStorage: aTextStorage]; if ([tv shouldChangeTextInRange: range replacementString: (string ? (NSString*)[string string] : (NSString*)@"")]) { if ([string length] > 0) { [aTextStorage replaceCharactersInRange: range withAttributedString: string]; } else { [aTextStorage replaceCharactersInRange: range withString: @""]; } range.length = [string length]; if ([[tv undoManager] isUndoing]) [tv setSelectedRange: range]; else [tv setSelectedRange: NSMakeRange(NSMaxRange(range), 0)]; [tv didChangeText]; } } - (NSTextView *) bestTextViewForTextStorage: (NSTextStorage *)aTextStorage { NSUInteger i, j; NSArray *layoutManagers, *textContainers; NSTextView *tv, *first = nil, *best = nil; NSWindow *win; /* The "best" view will be one in the key window followed by one in the * main window. In either case, we prefer the window's first responder. * If no view is in the key or main window, we simply use the first text * view. Since -shouldChangeTextInRange:replacementString: returns NO * by default if a NSTextView is not editable, we consider only editable * views here. */ layoutManagers = [aTextStorage layoutManagers]; for (i = 0; i < [layoutManagers count]; i++) { textContainers = [[layoutManagers objectAtIndex: i] textContainers]; for (j = 0; j < [textContainers count]; j++) { tv = [[textContainers objectAtIndex: j] textView]; if ([tv isEditable]) { win = [tv window]; if (first == nil) first = tv; if ([win isKeyWindow]) { if ([win firstResponder] == tv) return tv; else if (best == nil) best = tv; } else if ([win isMainWindow]) { if ([win firstResponder] == tv) { if (best == nil || ![[best window] isKeyWindow]) best = tv; } else if (best == nil) best = tv; } } } } return best != nil ? best : first; } @end @implementation NSTextStorage(NSTextViewUndoSupport) - (void) _undoTextChange: (NSTextViewUndoObject *)anObject { [anObject performUndo: self]; } @end