libs-gui/Source/NSTextView.m
fredkiefer 4cb2b0693f * Source/NSTextView.m (-initWithCoder:): Make sure text container is
in sync with the text view resize behaviour.
* Source/NSScrollView.m (-initWithCoder:): Add scrollers as
subviews when decoded.
2020-09-12 21:59:16 +02:00

6707 lines
182 KiB
Objective-C

/** <title>NSTextView</title>
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 <scottc@net-community.com>
Date: 1996
Author: Felipe A. Rodriguez <far@ix.netcom.com>
Date: July 1998
Author: Daniel Boehringer <boehring@biomed.ruhr-uni-bochum.de>
Date: August 1998
Author: Fred Kiefer <FredKiefer@gmx.de>
Date: March 2000, September 2000, January 2008
Author: Nicola Pero <n.pero@mi.flashnet.it>
Date: 2000, 2001, 2002
Author: Pierre-Yves Rivaille <pyrivail@ens-lyon.fr>
Date: September 2002
Extensive reworking: Alexander Malmberg <alexander@malmberg.org>
Date: December 2002 - February 2003
Implementing Catalina Extensions: Gregory Casamento <greg.casamento@gmail.com>
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 <http://www.gnu.org/licenses/> or write to the
Free Software Foundation, 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301, USA.
*/
#import "config.h"
#import <Foundation/NSArchiver.h>
#import <Foundation/NSArray.h>
#import <Foundation/NSCharacterSet.h>
#import <Foundation/NSCoder.h>
#import <Foundation/NSData.h>
#import <Foundation/NSDebug.h>
#import <Foundation/NSEnumerator.h>
#import <Foundation/NSException.h>
#import <Foundation/NSKeyedArchiver.h>
#import <Foundation/NSNotification.h>
#import <Foundation/NSRunLoop.h>
#import <Foundation/NSString.h>
#import <Foundation/NSTimer.h>
#import <Foundation/NSUndoManager.h>
#import <Foundation/NSValue.h>
#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
<doesn't even implement the Coding protocol>. */
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
{
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];
}
- (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<NSValidatedUserInterfaceItem>)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 <and where to stop it before> */
[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.
[[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 ; j<rectCount ; j++)
{
[self addToolTipRect: rects[j]
owner: self
userData: nil];
}
i = r.location + r.length;
}
}
}
- (void) resetCursorRects
{
if ([self isSelectable])
{
const NSRect visibleRect = [self visibleRect];
if (!NSEqualRects(NSZeroRect, visibleRect))
{
[self addCursorRect: visibleRect cursor: [NSCursor IBeamCursor]];
}
/**
* Add pointing hand cursor to any visible links
* FIXME: Also look for NSCursorAttributeName
* FIXME: Too complex, factor out common patterns into private methods
*/
if (_layoutManager != nil && _textContainer != nil)
{
NSInteger i;
NSRange visibleGlyphs, visibleCharacters;
const NSPoint containerOrigin = [self textContainerOrigin];
NSRect visibleRectInContainerCoordinates = visibleRect;
visibleRectInContainerCoordinates.origin.x -= containerOrigin.x;
visibleRectInContainerCoordinates.origin.y -= containerOrigin.y;
visibleGlyphs = [_layoutManager glyphRangeForBoundingRectWithoutAdditionalLayout: visibleRectInContainerCoordinates
inTextContainer: _textContainer];
visibleCharacters = [_layoutManager characterRangeForGlyphRange: visibleGlyphs
actualGlyphRange: NULL];
for (i = visibleCharacters.location; i < NSMaxRange(visibleCharacters); )
{
NSRange linkCharacterRange;
id linkValue = [[self textStorage] attribute: NSLinkAttributeName
atIndex: i
longestEffectiveRange: &linkCharacterRange
inRange: visibleCharacters];
if (linkValue != nil)
{
NSUInteger count, j;
NSRectArray rects = [_layoutManager rectArrayForCharacterRange: linkCharacterRange
withinSelectedCharacterRange: linkCharacterRange
inTextContainer: _textContainer
rectCount: &count];
for (j = 0; j < count; j++)
{
NSRect rectInViewCoordinates = rects[j];
rectInViewCoordinates.origin.x += containerOrigin.x;
rectInViewCoordinates.origin.y += containerOrigin.y;
[self addCursorRect: rectInViewCoordinates
cursor: [NSCursor pointingHandCursor]];
}
}
i += linkCharacterRange.length;
}
}
}
}
- (NSString*)view: (NSView *)view
stringForToolTip: (NSToolTipTag)tag
point: (NSPoint)point
userData: (void*)userData
{
// Find the rectangle for this tag
FOR_IN(GSTrackingRect*, rect, _tracking_rects)
if (rect->tag == 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 <NSDraggingInfo>)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 <NSDraggingInfo>)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 <NSDraggingInfo>)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 <NSDraggingInfo>)sender
{
NSDragOperation flags = NSDragOperationNone;
[self _updateDragTargetLocation: sender operation: &flags];
}
- (BOOL) prepareForDragOperation: (id <NSDraggingInfo>)sender
{
return YES;
}
- (BOOL) performDragOperation: (id <NSDraggingInfo>)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 <NSDraggingInfo>)sender
{
[self cleanUpAfterDragOperation];
}
- (void) cleanUpAfterDragOperation
{
// Cleanup information after dragging operation completes...
_dragTargetLocation = NSNotFound;
[self updateInsertionPointStateAndRestartTimer: NO];
[self displayIfNeeded];
}
- (NSDragOperation) dragOperationForDraggingInfo: (id <NSDraggingInfo>)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;
/* Check if the click was on an attachment cell. */
attachment = [_textStorage attribute: NSAttachmentAttributeName
atIndex: startIndex
effectiveRange: NULL];
if (attachment != nil)
{
id <NSTextAttachmentCell> 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: startIndex]
&& [cell trackMouse: theEvent
inRect: cellFrame
ofView: self
atCharacterIndex: startIndex
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
{
[_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