mirror of
https://github.com/gnustep/libs-gui.git
synced 2025-04-25 04:50:54 +00:00
git-svn-id: svn+ssh://svn.gna.org/svn/gnustep/libs/gui/trunk@12608 72102866-910b-0410-8b05-ffd578937521
4038 lines
100 KiB
Objective-C
4038 lines
100 KiB
Objective-C
/** <title>NSTextView</title>
|
|
|
|
Copyright (C) 1996, 1998, 2000, 2001, 2002 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 Bðhringer <boehring@biomed.ruhr-uni-bochum.de>
|
|
Date: August 1998
|
|
|
|
Author: Fred Kiefer <FredKiefer@gmx.de>
|
|
Date: March 2000, September 2000
|
|
|
|
Author: Nicola Pero <n.pero@mi.flashnet.it>
|
|
Date: 2000, 2001, 2002
|
|
|
|
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 Library 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
|
|
Library General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Library General Public
|
|
License along with this library; see the file COPYING.LIB.
|
|
If not, write to the Free Software Foundation,
|
|
59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */
|
|
|
|
#include <gnustep/gui/config.h>
|
|
#include <Foundation/NSCoder.h>
|
|
#include <Foundation/NSArray.h>
|
|
#include <Foundation/NSException.h>
|
|
#include <Foundation/NSProcessInfo.h>
|
|
#include <Foundation/NSString.h>
|
|
#include <Foundation/NSNotification.h>
|
|
#include <AppKit/NSApplication.h>
|
|
#include <AppKit/NSWindow.h>
|
|
#include <AppKit/NSEvent.h>
|
|
#include <AppKit/NSFont.h>
|
|
#include <AppKit/NSColor.h>
|
|
#include <AppKit/NSTextView.h>
|
|
#include <AppKit/NSParagraphStyle.h>
|
|
#include <AppKit/NSRulerView.h>
|
|
#include <AppKit/NSScrollView.h>
|
|
#include <AppKit/NSPasteboard.h>
|
|
#include <AppKit/NSSpellChecker.h>
|
|
#include <AppKit/NSControl.h>
|
|
#include <AppKit/NSLayoutManager.h>
|
|
#include <AppKit/NSTextStorage.h>
|
|
#include <AppKit/NSColorPanel.h>
|
|
|
|
#define HUGE 1e7
|
|
|
|
/* 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);
|
|
}
|
|
|
|
#define SET_DELEGATE_NOTIFICATION(notif_name) \
|
|
if ([_delegate respondsToSelector: @selector(text##notif_name: )]) \
|
|
[nc 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
|
|
* _tvf.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
|
|
|
|
/*
|
|
* 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. */
|
|
|
|
static BOOL noLayoutManagerException ()
|
|
{
|
|
[NSException raise: NSGenericException
|
|
format: @"Can't edit a NSTextView without a layout manager!"];
|
|
return YES;
|
|
}
|
|
|
|
/* 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
|
|
|
|
/* The shared notification center */
|
|
static NSNotificationCenter *nc;
|
|
|
|
@interface NSTextView (GNUstepPrivate)
|
|
/*
|
|
* Used to implement the blinking insertion point
|
|
*/
|
|
- (void) _blink: (NSTimer *)t;
|
|
|
|
/*
|
|
* these NSLayoutManager- like method is here only informally
|
|
*/
|
|
- (NSRect) rectForCharacterRange: (NSRange)aRange;
|
|
|
|
/*
|
|
* various GNU extensions
|
|
*/
|
|
+ (NSDictionary*) defaultTypingAttributes;
|
|
|
|
//
|
|
// GNU utility methods
|
|
//
|
|
- (void) setAttributes: (NSDictionary *) attributes range: (NSRange) aRange;
|
|
- (void) _illegalMovement: (int) notNumber;
|
|
@end
|
|
|
|
@implementation NSTextView
|
|
|
|
/* Class methods */
|
|
|
|
+ (void) initialize
|
|
{
|
|
if ([self class] == [NSTextView class])
|
|
{
|
|
[self setVersion: 1];
|
|
nc = [NSNotificationCenter defaultCenter];
|
|
[self registerForServices];
|
|
}
|
|
}
|
|
|
|
+ (void) registerForServices
|
|
{
|
|
NSArray *types;
|
|
|
|
types = [NSArray arrayWithObjects: NSStringPboardType,
|
|
NSRTFPboardType, NSRTFDPboardType, nil];
|
|
|
|
[[NSApplication sharedApplication] registerServicesMenuSendTypes: types
|
|
returnTypes: types];
|
|
}
|
|
|
|
/* Initializing Methods */
|
|
|
|
- (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);
|
|
|
|
/* The situation at this point is as follows:
|
|
|
|
textView (us) --RETAINs--> textStorage
|
|
textStorage --RETAINs--> layoutManager
|
|
layoutManager --RETAINs--> textContainer */
|
|
|
|
/* We keep a flag to remember that we are directly responsible for
|
|
managing the text objects. */
|
|
_tvf.owns_text_network = YES;
|
|
|
|
return textContainer;
|
|
}
|
|
|
|
/* Designated initializer */
|
|
- (id) initWithFrame: (NSRect)frameRect
|
|
textContainer: (NSTextContainer*)aTextContainer
|
|
{
|
|
[super initWithFrame: frameRect];
|
|
|
|
[self setMinSize: NSMakeSize (0, 0)];
|
|
[self setMaxSize: NSMakeSize (HUGE,HUGE)];
|
|
|
|
_tf.is_field_editor = NO;
|
|
_tf.is_editable = YES;
|
|
_tf.is_selectable = YES;
|
|
_tf.is_rich_text = NO;
|
|
_tf.imports_graphics = NO;
|
|
_tf.draws_background = YES;
|
|
_tf.is_horizontally_resizable = NO;
|
|
_tf.is_vertically_resizable = NO;
|
|
_tf.uses_font_panel = YES;
|
|
_tf.uses_ruler = YES;
|
|
_tf.is_ruler_visible = NO;
|
|
_original_selected_range.location = NSNotFound;
|
|
ASSIGN (_caret_color, [NSColor blackColor]);
|
|
|
|
[self setTypingAttributes: [isa defaultTypingAttributes]];
|
|
|
|
[self setBackgroundColor: [NSColor textBackgroundColor]];
|
|
|
|
[aTextContainer setTextView: self];
|
|
|
|
[self setEditable: YES];
|
|
[self setUsesFontPanel: YES];
|
|
[self setUsesRuler: YES];
|
|
|
|
[self setSelectedRange: NSMakeRange (0, 0)];
|
|
|
|
return self;
|
|
}
|
|
|
|
- (id) initWithFrame: (NSRect)frameRect
|
|
{
|
|
NSTextContainer *aTextContainer;
|
|
|
|
aTextContainer = [self buildUpTextNetwork: frameRect.size];
|
|
|
|
self = [self initWithFrame: frameRect textContainer: aTextContainer];
|
|
|
|
/* At this point the situation is as follows:
|
|
|
|
textView (us) --RETAINs--> textStorage
|
|
textStorage --RETAINs--> layoutManager
|
|
layoutManager --RETAINs--> textContainer
|
|
textContainter --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;
|
|
}
|
|
|
|
- (void) encodeWithCoder: (NSCoder *)aCoder
|
|
{
|
|
BOOL flag;
|
|
|
|
[super encodeWithCoder: aCoder];
|
|
|
|
flag = _tvf.smart_insert_delete;
|
|
[aCoder encodeValueOfObjCType: @encode(BOOL) at: &flag];
|
|
flag = _tvf.allows_undo;
|
|
[aCoder encodeValueOfObjCType: @encode(BOOL) at: &flag];
|
|
}
|
|
|
|
- (id) initWithCoder: (NSCoder *)aDecoder
|
|
{
|
|
NSTextContainer *aTextContainer;
|
|
BOOL flag;
|
|
|
|
self = [super initWithCoder: aDecoder];
|
|
|
|
[aDecoder decodeValueOfObjCType: @encode(BOOL) at: &flag];
|
|
_tvf.smart_insert_delete = flag;
|
|
[aDecoder decodeValueOfObjCType: @encode(BOOL) at: &flag];
|
|
_tvf.allows_undo = 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);
|
|
|
|
[self setSelectedRange: NSMakeRange (0, 0)];
|
|
|
|
return self;
|
|
}
|
|
|
|
- (void)dealloc
|
|
{
|
|
if (_tvf.owns_text_network == YES)
|
|
{
|
|
/* Prevent recursive dealloc */
|
|
if (_tvf.is_in_dealloc == YES)
|
|
{
|
|
return;
|
|
}
|
|
_tvf.is_in_dealloc = YES;
|
|
/* Balance the RELEASE we sent to us to break the retain cycle
|
|
in initWithFrame: or initWithCoder: (otherwise releasing the
|
|
_textStorage will make our retain count go below zero ;-) */
|
|
RETAIN (self);
|
|
|
|
/* This releases all the text objects (us included) in fall */
|
|
RELEASE (_textStorage);
|
|
}
|
|
|
|
if (_insertionPointTimer != nil)
|
|
{
|
|
[_insertionPointTimer invalidate];
|
|
DESTROY (_insertionPointTimer);
|
|
}
|
|
|
|
RELEASE (_selectedTextAttributes);
|
|
RELEASE (_markedTextAttributes);
|
|
RELEASE (_caret_color);
|
|
RELEASE (_typingAttributes);
|
|
|
|
[super dealloc];
|
|
}
|
|
|
|
/*
|
|
* Implementation of methods declared in superclass but depending
|
|
* on the internals of the NSTextView
|
|
*/
|
|
- (void) replaceCharactersInRange: (NSRange)aRange
|
|
withString: (NSString *)aString
|
|
{
|
|
if (aRange.location == NSNotFound)
|
|
return;
|
|
|
|
if ([self shouldChangeTextInRange: aRange
|
|
replacementString: aString] == NO)
|
|
return;
|
|
|
|
[_textStorage beginEditing];
|
|
|
|
if (_tf.is_rich_text && [_textStorage length] == 0)
|
|
{
|
|
/* In this case, there is nothing in the _textStorage ... so the
|
|
_textStorage can't choose appropriate formatting attributes
|
|
for the string ... we want the typingAttributes to be used in
|
|
this case. So we set the attributes manually. */
|
|
NSAttributedString *as;
|
|
|
|
as = AUTORELEASE ([[NSAttributedString alloc]
|
|
initWithString: aString
|
|
attributes: _typingAttributes]);
|
|
[self replaceRange: aRange withAttributedString: as];
|
|
}
|
|
else
|
|
{
|
|
[_textStorage replaceCharactersInRange: aRange withString: aString];
|
|
}
|
|
[_textStorage endEditing];
|
|
[self didChangeText];
|
|
}
|
|
|
|
- (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];
|
|
}
|
|
|
|
/*
|
|
* [NSText] Managing the Ruler
|
|
*/
|
|
- (void) toggleRuler: (id)sender
|
|
{
|
|
[self setRulerVisible: !_tf.is_ruler_visible];
|
|
}
|
|
|
|
/*
|
|
* [NSText] Managing the Selected Range
|
|
*/
|
|
- (NSRange) selectedRange
|
|
{
|
|
return _selected_range;
|
|
}
|
|
|
|
- (void) setSelectedRange: (NSRange)range
|
|
{
|
|
[self setSelectedRange: range affinity: [self selectionAffinity]
|
|
stillSelecting: NO];
|
|
}
|
|
|
|
/*
|
|
* [NSText] Copy and Paste
|
|
*/
|
|
- (void) copy: (id)sender
|
|
{
|
|
NSMutableArray *types = [NSMutableArray array];
|
|
|
|
if (_tf.imports_graphics)
|
|
[types addObject: NSRTFDPboardType];
|
|
|
|
if (_tf.is_rich_text)
|
|
[types addObject: NSRTFPboardType];
|
|
|
|
[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 type: NSFontPboardType];
|
|
}
|
|
|
|
/* Copy the current ruler settings to the ruler pasteboard */
|
|
- (void) copyRuler: (id)sender
|
|
{
|
|
NSPasteboard *pb = [NSPasteboard pasteboardWithName: NSRulerPboard];
|
|
|
|
[self writeSelectionToPasteboard: pb type: NSRulerPboardType];
|
|
}
|
|
|
|
- (void) delete: (id)sender
|
|
{
|
|
[self deleteForward: sender];
|
|
}
|
|
|
|
- (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];
|
|
}
|
|
|
|
/*
|
|
* [NSText] Managing Fonts
|
|
*/
|
|
- (NSFont*) font
|
|
{
|
|
if ([_textStorage length] > 0)
|
|
{
|
|
NSFont *font;
|
|
|
|
font = [_textStorage attribute: NSFontAttributeName
|
|
atIndex: 0
|
|
effectiveRange: NULL];
|
|
if (font != nil)
|
|
{
|
|
return font;
|
|
}
|
|
}
|
|
|
|
return [_typingAttributes objectForKey: NSFontAttributeName];
|
|
}
|
|
|
|
- (void) changeFont: (id)sender
|
|
{
|
|
NSRange foundRange;
|
|
int 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 = [_typingAttributes objectForKey: NSFontAttributeName];
|
|
if (font != nil)
|
|
{
|
|
[_typingAttributes setObject: [sender convertFont: font]
|
|
forKey: NSFontAttributeName];
|
|
}
|
|
}
|
|
|
|
- (void) setFont: (NSFont*)font
|
|
{
|
|
if (font == nil)
|
|
{
|
|
return;
|
|
}
|
|
|
|
[self setFont: font ofRange: NSMakeRange (0, [_textStorage length])];
|
|
[_typingAttributes setObject: font forKey: NSFontAttributeName];
|
|
}
|
|
|
|
- (void) setFont: (NSFont*)font ofRange: (NSRange)aRange
|
|
{
|
|
if (font != nil)
|
|
{
|
|
if (![self shouldChangeTextInRange: aRange replacementString: nil])
|
|
return;
|
|
|
|
[_textStorage beginEditing];
|
|
[_textStorage addAttribute: NSFontAttributeName value: font
|
|
range: aRange];
|
|
[_textStorage endEditing];
|
|
[self didChangeText];
|
|
}
|
|
}
|
|
|
|
/*
|
|
* [NSText] Managing Alignment
|
|
*/
|
|
- (NSTextAlignment) alignment
|
|
{
|
|
unsigned location = 0;
|
|
NSParagraphStyle *aStyle;
|
|
|
|
if (_tf.is_rich_text)
|
|
{
|
|
location = [self rangeForUserParagraphAttributeChange].location;
|
|
}
|
|
|
|
if (location != NSNotFound)
|
|
{
|
|
aStyle = [_textStorage attribute: NSParagraphStyleAttributeName
|
|
atIndex: location
|
|
effectiveRange: NULL];
|
|
if (aStyle != nil)
|
|
{
|
|
return [aStyle alignment];
|
|
}
|
|
}
|
|
|
|
/* Get alignment from typing attributes */
|
|
return [[_typingAttributes objectForKey: NSParagraphStyleAttributeName]
|
|
alignment];
|
|
}
|
|
|
|
- (void) setAlignment: (NSTextAlignment)mode
|
|
{
|
|
[self setAlignment: mode range: NSMakeRange (0, [_textStorage length])];
|
|
}
|
|
|
|
- (void) alignCenter: (id)sender
|
|
{
|
|
[self setAlignment: NSCenterTextAlignment
|
|
range: [self rangeForUserParagraphAttributeChange]];
|
|
}
|
|
|
|
- (void) alignLeft: (id)sender
|
|
{
|
|
[self setAlignment: NSLeftTextAlignment
|
|
range: [self rangeForUserParagraphAttributeChange]];
|
|
}
|
|
|
|
- (void) alignRight: (id)sender
|
|
{
|
|
[self setAlignment: NSRightTextAlignment
|
|
range: [self rangeForUserParagraphAttributeChange]];
|
|
}
|
|
|
|
/*
|
|
* [NSText] Managing Text Color
|
|
*/
|
|
- (NSColor*) textColor
|
|
{
|
|
if ([_textStorage length] > 0)
|
|
{
|
|
NSColor *color;
|
|
|
|
color = [_textStorage attribute: NSForegroundColorAttributeName
|
|
atIndex: 0
|
|
effectiveRange: NULL];
|
|
if (color != nil)
|
|
{
|
|
return color;
|
|
}
|
|
}
|
|
return [_typingAttributes objectForKey: NSForegroundColorAttributeName];
|
|
}
|
|
|
|
- (void) setTextColor: (NSColor*)color
|
|
{
|
|
/* FIXME: This should work also for non rich text objects */
|
|
NSRange fullRange = NSMakeRange (0, [_textStorage length]);
|
|
|
|
[self setTextColor: color range: fullRange];
|
|
}
|
|
|
|
- (void) setTextColor: (NSColor*)color range: (NSRange)aRange
|
|
{
|
|
/* FIXME: This should only work for rich text object */
|
|
if (aRange.location == NSNotFound)
|
|
return;
|
|
|
|
if (![self shouldChangeTextInRange: aRange replacementString: nil])
|
|
{
|
|
return;
|
|
}
|
|
[_textStorage beginEditing];
|
|
if (color != nil)
|
|
{
|
|
[_textStorage addAttribute: NSForegroundColorAttributeName
|
|
value: color
|
|
range: aRange];
|
|
[_typingAttributes setObject: color
|
|
forKey: NSForegroundColorAttributeName];
|
|
}
|
|
else
|
|
{
|
|
[_textStorage removeAttribute: NSForegroundColorAttributeName
|
|
range: aRange];
|
|
}
|
|
[_textStorage endEditing];
|
|
[self didChangeText];
|
|
}
|
|
|
|
/*
|
|
* [NSText] Text Attributes
|
|
*/
|
|
- (void) subscript: (id)sender
|
|
{
|
|
NSNumber *value = [_typingAttributes
|
|
objectForKey: NSSuperscriptAttributeName];
|
|
int sValue;
|
|
NSRange aRange = [self rangeForUserCharacterAttributeChange];
|
|
|
|
if (aRange.location == NSNotFound)
|
|
return;
|
|
|
|
if (aRange.length)
|
|
{
|
|
if (![self shouldChangeTextInRange: aRange
|
|
replacementString: nil])
|
|
return;
|
|
[_textStorage beginEditing];
|
|
[_textStorage subscriptRange: aRange];
|
|
[_textStorage endEditing];
|
|
[self didChangeText];
|
|
}
|
|
|
|
// Set the typing attributes
|
|
if (value != nil)
|
|
sValue = [value intValue] - 1;
|
|
else
|
|
sValue = -1;
|
|
[_typingAttributes setObject: [NSNumber numberWithInt: sValue]
|
|
forKey: NSSuperscriptAttributeName];
|
|
}
|
|
|
|
- (void) superscript: (id)sender
|
|
{
|
|
NSNumber *value = [_typingAttributes
|
|
objectForKey: NSSuperscriptAttributeName];
|
|
int sValue;
|
|
NSRange aRange = [self rangeForUserCharacterAttributeChange];
|
|
|
|
if (aRange.location == NSNotFound)
|
|
return;
|
|
|
|
if (aRange.length)
|
|
{
|
|
if (![self shouldChangeTextInRange: aRange
|
|
replacementString: nil])
|
|
return;
|
|
[_textStorage beginEditing];
|
|
[_textStorage superscriptRange: aRange];
|
|
[_textStorage endEditing];
|
|
[self didChangeText];
|
|
}
|
|
|
|
// Set the typing attributes
|
|
if (value != nil)
|
|
sValue = [value intValue] + 1;
|
|
else
|
|
sValue = 1;
|
|
[_typingAttributes setObject: [NSNumber numberWithInt: sValue]
|
|
forKey: NSSuperscriptAttributeName];
|
|
}
|
|
|
|
- (void) unscript: (id)sender
|
|
{
|
|
NSRange aRange = [self rangeForUserCharacterAttributeChange];
|
|
|
|
if (aRange.location == NSNotFound)
|
|
return;
|
|
|
|
if (aRange.length)
|
|
{
|
|
if (![self shouldChangeTextInRange: aRange
|
|
replacementString: nil])
|
|
return;
|
|
[_textStorage beginEditing];
|
|
[_textStorage unscriptRange: aRange];
|
|
[_textStorage endEditing];
|
|
[self didChangeText];
|
|
}
|
|
|
|
// Set the typing attributes
|
|
[_typingAttributes removeObjectForKey: NSSuperscriptAttributeName];
|
|
}
|
|
|
|
- (void) underline: (id)sender
|
|
{
|
|
BOOL doUnderline = YES;
|
|
NSRange aRange = [self rangeForUserCharacterAttributeChange];
|
|
|
|
if (aRange.location == NSNotFound)
|
|
return;
|
|
|
|
if ([[_textStorage attribute: NSUnderlineStyleAttributeName
|
|
atIndex: aRange.location
|
|
effectiveRange: NULL] intValue])
|
|
doUnderline = NO;
|
|
|
|
if (aRange.length)
|
|
{
|
|
if (![self shouldChangeTextInRange: aRange
|
|
replacementString: nil])
|
|
return;
|
|
[_textStorage beginEditing];
|
|
[_textStorage addAttribute: NSUnderlineStyleAttributeName
|
|
value: [NSNumber numberWithInt: doUnderline]
|
|
range: aRange];
|
|
[_textStorage endEditing];
|
|
[self didChangeText];
|
|
}
|
|
|
|
[_typingAttributes
|
|
setObject: [NSNumber numberWithInt: doUnderline]
|
|
forKey: NSUnderlineStyleAttributeName];
|
|
}
|
|
|
|
/*
|
|
* [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 replaceRange: 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];
|
|
}
|
|
|
|
/*
|
|
* [NSText] Managing size
|
|
*/
|
|
- (void) setHorizontallyResizable: (BOOL)flag
|
|
{
|
|
/* Safety call */
|
|
[_textContainer setWidthTracksTextView: !flag];
|
|
|
|
[super setHorizontallyResizable: flag];
|
|
}
|
|
|
|
- (void) setVerticallyResizable: (BOOL)flag
|
|
{
|
|
/* Safety call */
|
|
[_textContainer setHeightTracksTextView: !flag];
|
|
|
|
[super setVerticallyResizable: flag];
|
|
}
|
|
|
|
- (void) sizeToFit
|
|
{
|
|
if (_tf.is_horizontally_resizable || _tf.is_vertically_resizable)
|
|
{
|
|
NSSize size;
|
|
|
|
size = [_layoutManager usedRectForTextContainer: _textContainer].size;
|
|
size.width += 2 * _textContainerInset.width;
|
|
size.height += 2 * _textContainerInset.height;
|
|
|
|
[self setConstrainedFrameSize: size];
|
|
}
|
|
}
|
|
|
|
/*
|
|
* [NSText] Spelling
|
|
*/
|
|
- (void) checkSpelling: (id)sender
|
|
{
|
|
NSSpellChecker *sp = [NSSpellChecker sharedSpellChecker];
|
|
NSString *misspelledWord = nil;
|
|
NSRange errorRange;
|
|
int count = 0;
|
|
|
|
errorRange = [sp checkSpellingOfString: [self string]
|
|
startingAt: NSMaxRange (_selected_range)
|
|
language: [sp language]
|
|
wrap: YES
|
|
inSpellDocumentWithTag: [self spellCheckerDocumentTag]
|
|
wordCount: &count];
|
|
|
|
if (errorRange.length)
|
|
{
|
|
[self setSelectedRange: 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]];
|
|
}
|
|
|
|
/*
|
|
* [NSText] Scrolling
|
|
*/
|
|
- (void) scrollRangeToVisible: (NSRange)aRange
|
|
{
|
|
// Don't try scrolling an ancestor clipview if we are field editor.
|
|
// This makes things so much simpler and stabler for now.
|
|
if (_tf.is_field_editor == NO)
|
|
{
|
|
[self scrollRectToVisible: [self rectForCharacterRange: aRange]];
|
|
}
|
|
}
|
|
|
|
/*
|
|
* [NSResponder] Handle enabling/disabling of services menu items.
|
|
*/
|
|
- (id) validRequestorForSendType: (NSString*)sendType
|
|
returnType: (NSString*)returnType
|
|
{
|
|
BOOL sendOK = NO;
|
|
BOOL returnOK = NO;
|
|
|
|
if (sendType == nil)
|
|
{
|
|
sendOK = YES;
|
|
}
|
|
else if (_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];
|
|
}
|
|
|
|
/*
|
|
* NSTextView's specific methods
|
|
*/
|
|
|
|
- (void) _updateMultipleTextViews
|
|
{
|
|
id oldNotifObject = _notifObject;
|
|
|
|
if ([[_layoutManager textContainers] count] > 1)
|
|
{
|
|
_tvf.multiple_textviews = YES;
|
|
_notifObject = [_layoutManager firstTextView];
|
|
}
|
|
else
|
|
{
|
|
_tvf.multiple_textviews = NO;
|
|
_notifObject = self;
|
|
}
|
|
|
|
if ((_delegate != nil) && (oldNotifObject != _notifObject))
|
|
{
|
|
[nc removeObserver: _delegate name: nil object: oldNotifObject];
|
|
|
|
if ([_delegate respondsToSelector:
|
|
@selector(shouldChangeTextInRange:replacementString:)])
|
|
{
|
|
_tvf.delegate_responds_to_should_change = YES;
|
|
}
|
|
else
|
|
{
|
|
_tvf.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);
|
|
}
|
|
}
|
|
|
|
/* This should only be called by [NSTextContainer -setTextView:] */
|
|
- (void) setTextContainer: (NSTextContainer*)aTextContainer
|
|
{
|
|
_textContainer = aTextContainer;
|
|
_layoutManager = [aTextContainer layoutManager];
|
|
_textStorage = [_layoutManager textStorage];
|
|
|
|
[self _updateMultipleTextViews];
|
|
}
|
|
|
|
- (void) replaceTextContainer: (NSTextContainer*)aTextContainer
|
|
{
|
|
/* FIXME/TODO: Tell the layout manager the text container is changed
|
|
keeping all the rest intact */
|
|
|
|
/* Do not retain: text container is owning us. */
|
|
_textContainer = aTextContainer;
|
|
|
|
[self _updateMultipleTextViews];
|
|
}
|
|
|
|
- (NSTextContainer *) textContainer
|
|
{
|
|
return _textContainer;
|
|
}
|
|
|
|
- (void) setTextContainerInset: (NSSize)inset
|
|
{
|
|
_textContainerInset = inset;
|
|
[self invalidateTextContainerOrigin];
|
|
}
|
|
|
|
- (NSSize) textContainerInset
|
|
{
|
|
return _textContainerInset;
|
|
}
|
|
|
|
- (NSPoint) textContainerOrigin
|
|
{
|
|
return _textContainerOrigin;
|
|
}
|
|
|
|
- (void) invalidateTextContainerOrigin
|
|
{
|
|
NSRect usedRect;
|
|
NSSize textContainerSize;
|
|
|
|
usedRect = [_layoutManager usedRectForTextContainer: _textContainer];
|
|
textContainerSize = [_textContainer containerSize];
|
|
|
|
/* The `text container origin' is - I think - the origin of the used
|
|
rect, but relative to our own coordinate system (used rect as
|
|
returned by [NSLayoutManager -usedRectForTextContainer:] is
|
|
instead relative to the text container coordinate system). This
|
|
information is used when we ask the layout manager to draw - the
|
|
base point is precisely this `text container origin', which is
|
|
the origin of the used rect in our own coordinate system. */
|
|
|
|
/* First get the pure text container origin */
|
|
_textContainerOrigin.x = NSMinX (_bounds);
|
|
_textContainerOrigin.x += _textContainerInset.width;
|
|
/* Then move to the used rect origin */
|
|
_textContainerOrigin.x += usedRect.origin.x;
|
|
|
|
/* First get the pure text container origin */
|
|
_textContainerOrigin.y = NSMaxY (_bounds);
|
|
_textContainerOrigin.y -= _textContainerInset.height;
|
|
_textContainerOrigin.y -= textContainerSize.height;
|
|
/* Then move to the used rect origin */
|
|
_textContainerOrigin.y += usedRect.origin.y;
|
|
}
|
|
|
|
- (NSLayoutManager*) layoutManager
|
|
{
|
|
return _layoutManager;
|
|
}
|
|
|
|
- (NSTextStorage*) textStorage
|
|
{
|
|
return _textStorage;
|
|
}
|
|
|
|
- (void) setAllowsUndo: (BOOL)flag
|
|
{
|
|
_tvf.allows_undo = flag;
|
|
}
|
|
|
|
- (BOOL) allowsUndo
|
|
{
|
|
return _tvf.allows_undo;
|
|
}
|
|
|
|
- (void) setNeedsDisplayInRect: (NSRect)aRect
|
|
avoidAdditionalLayout: (BOOL)flag
|
|
{
|
|
/* FIXME: This is here until the layout manager is working */
|
|
/* This is very important */
|
|
[super setNeedsDisplayInRect: aRect];
|
|
}
|
|
|
|
/* We override NSView's setNeedsDisplayInRect: */
|
|
|
|
- (void) setNeedsDisplayInRect: (NSRect)aRect
|
|
{
|
|
[self setNeedsDisplayInRect: aRect avoidAdditionalLayout: NO];
|
|
}
|
|
|
|
- (BOOL) shouldDrawInsertionPoint
|
|
{
|
|
return (_selected_range.length == 0) && _tf.is_editable
|
|
&& [_window isKeyWindow] && ([_window firstResponder] == self);
|
|
}
|
|
|
|
/*
|
|
* 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 = _caret_color;
|
|
}
|
|
|
|
[color set];
|
|
NSRectFill (rect);
|
|
}
|
|
else
|
|
{
|
|
[_background_color set];
|
|
NSRectFill (rect);
|
|
}
|
|
}
|
|
|
|
- (void) setConstrainedFrameSize: (NSSize)desiredSize
|
|
{
|
|
NSSize newSize;
|
|
|
|
if (_tf.is_horizontally_resizable)
|
|
{
|
|
newSize.width = desiredSize.width;
|
|
newSize.width = MAX (newSize.width, _minSize.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, _minSize.height);
|
|
newSize.height = MIN (newSize.height, _maxSize.height);
|
|
}
|
|
else
|
|
{
|
|
newSize.height = _frame.size.height;
|
|
}
|
|
|
|
if (NSEqualSizes (_frame.size, newSize) == NO)
|
|
{
|
|
[self setFrameSize: newSize];
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Code to share settings between multiple textviews
|
|
*
|
|
*/
|
|
|
|
/*
|
|
_syncTextViewsCalling: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;
|
|
int i, count;
|
|
void (*msg)(id, SEL, BOOL);
|
|
|
|
if (IS_SYNCHRONIZING_FLAGS == YES)
|
|
{
|
|
[NSException raise: NSGenericException
|
|
format: @"_syncTextViewsCalling:withFlag: "
|
|
@"called recursively"];
|
|
}
|
|
|
|
array = [_layoutManager textContainers];
|
|
count = [array count];
|
|
|
|
msg = (void (*)(id, SEL, BOOL))[self methodForSelector: action];
|
|
|
|
if (!msg)
|
|
{
|
|
[NSException raise: NSGenericException
|
|
format: @"invalid selector in "
|
|
@"_syncTextViewsCalling:withFlag:"];
|
|
}
|
|
|
|
IS_SYNCHRONIZING_FLAGS = YES;
|
|
|
|
for (i = 0; i < count; i++)
|
|
{
|
|
NSTextView *tv;
|
|
|
|
tv = [(NSTextContainer *)[array objectAtIndex: i] textView];
|
|
(*msg) (tv, action, flag);
|
|
}
|
|
|
|
IS_SYNCHRONIZING_FLAGS = NO;
|
|
}
|
|
|
|
#define NSTEXTVIEW_SYNC(X) \
|
|
if (_tvf.multiple_textviews && (IS_SYNCHRONIZING_FLAGS == NO)) \
|
|
{ [self _syncTextViewsByCalling: X withFlag: flag]; \
|
|
return; }
|
|
|
|
/*
|
|
* NB: 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 textviews - ie, when it is called on
|
|
* one, it will be automatically called on all related textviews.
|
|
* */
|
|
|
|
- (void) setEditable: (BOOL)flag
|
|
{
|
|
NSTEXTVIEW_SYNC (@selector(setEditable:));
|
|
[super setEditable: flag];
|
|
|
|
if ([self shouldDrawInsertionPoint])
|
|
{
|
|
[self updateInsertionPointStateAndRestartTimer: YES];
|
|
}
|
|
else
|
|
{
|
|
if (_insertionPointTimer != nil)
|
|
{
|
|
[_insertionPointTimer invalidate];
|
|
DESTROY (_insertionPointTimer);
|
|
_drawInsertionPointNow = NO;
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void) setFieldEditor: (BOOL)flag
|
|
{
|
|
NSTEXTVIEW_SYNC (@selector(setFieldEditor:));
|
|
[self setHorizontallyResizable: NO];
|
|
[self setVerticallyResizable: NO];
|
|
[super setFieldEditor: flag];
|
|
}
|
|
|
|
- (void) setSelectable: (BOOL)flag
|
|
{
|
|
NSTEXTVIEW_SYNC (@selector(setSelectable:));
|
|
[super setSelectable: flag];
|
|
}
|
|
|
|
- (void) setRichText: (BOOL)flag
|
|
{
|
|
NSTEXTVIEW_SYNC (@selector(setRichText:));
|
|
|
|
[super setRichText: flag];
|
|
[self updateDragTypeRegistration];
|
|
/* FIXME/TODO: Also convert text to plain text or to rich text */
|
|
}
|
|
|
|
- (void) setImportsGraphics: (BOOL)flag
|
|
{
|
|
NSTEXTVIEW_SYNC (@selector(setImportsGraphics:));
|
|
|
|
[super setImportsGraphics: flag];
|
|
[self updateDragTypeRegistration];
|
|
}
|
|
|
|
- (void) setUsesRuler: (BOOL)flag
|
|
{
|
|
NSTEXTVIEW_SYNC (@selector(setUsesRuler:));
|
|
_tf.uses_ruler = flag;
|
|
}
|
|
|
|
- (BOOL) usesRuler
|
|
{
|
|
return _tf.uses_ruler;
|
|
}
|
|
|
|
- (void) setUsesFontPanel: (BOOL)flag
|
|
{
|
|
NSTEXTVIEW_SYNC (@selector(setUsesFontPanel:));
|
|
[super setUsesFontPanel: flag];
|
|
}
|
|
|
|
- (void) setRulerVisible: (BOOL)flag
|
|
{
|
|
NSScrollView *sv;
|
|
|
|
NSTEXTVIEW_SYNC (@selector(setRulerVisible:));
|
|
|
|
sv = [self enclosingScrollView];
|
|
_tf.is_ruler_visible = flag;
|
|
if (sv != nil)
|
|
{
|
|
[sv setRulersVisible: _tf.is_ruler_visible];
|
|
}
|
|
}
|
|
|
|
#undef NSTEXTVIEW_SYNC
|
|
|
|
/* NB: Only NSSelectionAffinityDownstream works */
|
|
- (void) setSelectedRange: (NSRange)range
|
|
affinity: (NSSelectionAffinity)affinity
|
|
stillSelecting: (BOOL)flag
|
|
{
|
|
/* 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 = _selected_range;
|
|
|
|
if (flag == 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 (_original_selected_range.location == NSNotFound)
|
|
{
|
|
_original_selected_range = _selected_range;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
/* Retrieve the original range */
|
|
if (_original_selected_range.location != NSNotFound)
|
|
{
|
|
oldRange = _original_selected_range;
|
|
_original_selected_range.location = NSNotFound;
|
|
}
|
|
else
|
|
{
|
|
oldRange = _selected_range;
|
|
}
|
|
|
|
/* Ask delegate to modify the range */
|
|
if (_tvf.delegate_responds_to_will_change_sel)
|
|
{
|
|
range = [_delegate textView: _notifObject
|
|
willChangeSelectionFromCharacterRange: oldRange
|
|
toCharacterRange: range];
|
|
}
|
|
}
|
|
|
|
/* Set the new selected range */
|
|
_selected_range = range;
|
|
|
|
/* FIXME: when and if to restart timer <and where to stop it before> */
|
|
[self updateInsertionPointStateAndRestartTimer: !flag];
|
|
|
|
if (flag == NO)
|
|
{
|
|
[self updateFontPanel];
|
|
|
|
/* Insertion Point */
|
|
if (range.length)
|
|
{
|
|
if (_insertionPointTimer != nil)
|
|
{
|
|
[_insertionPointTimer invalidate];
|
|
DESTROY (_insertionPointTimer);
|
|
_drawInsertionPointNow = YES;
|
|
}
|
|
}
|
|
else /* no selection, only insertion point */
|
|
{
|
|
if (_tf.is_rich_text && [_textStorage length])
|
|
{
|
|
NSDictionary *dict;
|
|
|
|
if (range.location > 0)
|
|
{
|
|
/* 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: (range.location - 1)
|
|
effectiveRange: NULL];
|
|
}
|
|
else
|
|
{
|
|
/* Unless we are at the beginning of text - we use the
|
|
first valid attributes then */
|
|
dict = [_textStorage attributesAtIndex: range.location
|
|
effectiveRange: NULL];
|
|
}
|
|
[self setTypingAttributes: dict];
|
|
}
|
|
}
|
|
}
|
|
|
|
if (_window != nil)
|
|
{
|
|
NSRange overlap;
|
|
|
|
if (flag == NO)
|
|
{
|
|
/* Make the selected range visible */
|
|
[self scrollRangeToVisible: range];
|
|
}
|
|
|
|
/* Try to optimize for overlapping ranges */
|
|
overlap = NSIntersectionRange (oldRange, range);
|
|
if (overlap.length)
|
|
{
|
|
if (range.location != oldDisplayedRange.location)
|
|
{
|
|
NSRange r;
|
|
r = MakeRangeFromAbs (MIN (range.location,
|
|
oldDisplayedRange.location),
|
|
MAX (range.location,
|
|
oldDisplayedRange.location));
|
|
[self setNeedsDisplayInRect: [self rectForCharacterRange: r]
|
|
avoidAdditionalLayout: YES];
|
|
}
|
|
if (NSMaxRange (range) != NSMaxRange (oldDisplayedRange))
|
|
{
|
|
NSRange r;
|
|
|
|
r = MakeRangeFromAbs (MIN (NSMaxRange (range),
|
|
NSMaxRange (oldDisplayedRange)),
|
|
MAX (NSMaxRange (range),
|
|
NSMaxRange (oldDisplayedRange)));
|
|
[self setNeedsDisplayInRect: [self rectForCharacterRange: r]
|
|
avoidAdditionalLayout: YES];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
[self setNeedsDisplayInRect: [self rectForCharacterRange: range]
|
|
avoidAdditionalLayout: YES];
|
|
[self setNeedsDisplayInRect: [self rectForCharacterRange:
|
|
oldDisplayedRange]
|
|
avoidAdditionalLayout: YES];
|
|
}
|
|
}
|
|
|
|
[self setSelectionGranularity: NSSelectByCharacter];
|
|
|
|
/* TODO: Remove the marking from marked text if the new selection is
|
|
greater than the marked region. */
|
|
|
|
if (flag == NO)
|
|
{
|
|
NSDictionary *userInfo;
|
|
|
|
userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
|
|
[NSValue valueWithBytes: &oldRange
|
|
objCType: @encode(NSRange)],
|
|
NSOldSelectedCharacterRange, nil];
|
|
|
|
[nc postNotificationName: NSTextViewDidChangeSelectionNotification
|
|
object: _notifObject userInfo: userInfo];
|
|
}
|
|
}
|
|
|
|
- (NSSelectionAffinity) selectionAffinity
|
|
{
|
|
return NSSelectionAffinityDownstream;
|
|
}
|
|
|
|
- (void) setSelectionGranularity: (NSSelectionGranularity)granularity
|
|
{
|
|
_selectionGranularity = granularity;
|
|
}
|
|
|
|
- (NSSelectionGranularity) selectionGranularity
|
|
{
|
|
return _selectionGranularity;
|
|
}
|
|
|
|
- (void) setInsertionPointColor: (NSColor*)aColor
|
|
{
|
|
ASSIGN (_caret_color, aColor);
|
|
}
|
|
|
|
- (NSColor*) insertionPointColor
|
|
{
|
|
return _caret_color;
|
|
}
|
|
|
|
- (void) updateInsertionPointStateAndRestartTimer: (BOOL)flag
|
|
{
|
|
/* Update insertion point rect */
|
|
NSRange charRange;
|
|
NSRange glyphRange;
|
|
unsigned glyphIndex;
|
|
NSRect rect;
|
|
|
|
/* Simple case - no insertion point */
|
|
if ((_selected_range.length > 0) || _selected_range.location == NSNotFound)
|
|
{
|
|
if (_insertionPointTimer != nil)
|
|
{
|
|
[_insertionPointTimer invalidate];
|
|
DESTROY (_insertionPointTimer);
|
|
_drawInsertionPointNow = YES;
|
|
}
|
|
|
|
/* FIXME: horizontal position of insertion point */
|
|
_originalInsertPoint = 0;
|
|
return;
|
|
}
|
|
|
|
charRange = NSMakeRange (_selected_range.location, 0);
|
|
glyphRange = [_layoutManager glyphRangeForCharacterRange: charRange
|
|
actualCharacterRange: NULL];
|
|
glyphIndex = glyphRange.location;
|
|
|
|
rect = [_layoutManager lineFragmentRectForGlyphAtIndex: glyphIndex
|
|
effectiveRange: NULL];
|
|
|
|
if ([self selectionAffinity] != NSSelectionAffinityUpstream)
|
|
{
|
|
/* Standard case - draw the insertion point just before the
|
|
associated glyph index */
|
|
NSPoint loc = [_layoutManager locationForGlyphAtIndex: glyphIndex];
|
|
|
|
rect.origin.x += loc.x;
|
|
}
|
|
else /* _affinity == NSSelectionAffinityUpstream - non standard */
|
|
{
|
|
/* FIXME - THIS DOES NOT WORK - as a consequence,
|
|
NSSelectionAffinityUpstream DOES NOT WORK */
|
|
|
|
/* Check - if the previous glyph is on another line */
|
|
|
|
/* FIXME: Don't know how to do this check, this is a hack and
|
|
DOES NOT WORK - clearly this code should be inside the layout
|
|
manager anyway */
|
|
NSRect rect2;
|
|
|
|
rect2 = [_layoutManager lineFragmentRectForGlyphAtIndex: glyphIndex - 1
|
|
effectiveRange: NULL];
|
|
if (NSMinY (rect2) < NSMinY (rect))
|
|
{
|
|
/* Then we need to draw the insertion point just after the
|
|
previous glyph - DOES NOT WORK */
|
|
glyphRange = NSMakeRange (glyphIndex - 1, 1);
|
|
rect = [_layoutManager boundingRectForGlyphRange: glyphRange
|
|
inTextContainer:_textContainer];
|
|
rect.origin.x = NSMaxX (rect) - 1;
|
|
}
|
|
else /* Else, standard case again */
|
|
{
|
|
NSPoint loc = [_layoutManager locationForGlyphAtIndex: glyphIndex];
|
|
rect.origin.x += loc.x;
|
|
}
|
|
}
|
|
|
|
rect.size.width = 1;
|
|
|
|
if (rect.size.height == 0)
|
|
{
|
|
rect.size.height = 12;
|
|
}
|
|
|
|
_insertionPointRect = rect;
|
|
|
|
|
|
/* Remember horizontal position of insertion point */
|
|
_originalInsertPoint = _insertionPointRect.origin.x;
|
|
|
|
if (flag)
|
|
{
|
|
/* Start blinking timer if not yet started */
|
|
if (_insertionPointTimer == nil && [self shouldDrawInsertionPoint])
|
|
{
|
|
_insertionPointTimer = [NSTimer scheduledTimerWithTimeInterval: 0.5
|
|
target: self
|
|
selector: @selector(_blink:)
|
|
userInfo: nil
|
|
repeats: YES];
|
|
RETAIN (_insertionPointTimer);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (_insertionPointTimer != nil)
|
|
{
|
|
[_insertionPointTimer invalidate];
|
|
DESTROY (_insertionPointTimer);
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void) setSelectedTextAttributes: (NSDictionary *)attributes
|
|
{
|
|
ASSIGN (_selectedTextAttributes, attributes);
|
|
}
|
|
|
|
- (NSDictionary *) selectedTextAttributes
|
|
{
|
|
return _selectedTextAttributes;
|
|
}
|
|
|
|
- (NSRange) markedRange
|
|
{
|
|
// calculate
|
|
|
|
return NSMakeRange (NSNotFound, 0);
|
|
}
|
|
|
|
- (void) setMarkedTextAttributes: (NSDictionary*)attributes
|
|
{
|
|
ASSIGN (_markedTextAttributes, attributes);
|
|
}
|
|
|
|
- (NSDictionary*) markedTextAttributes
|
|
{
|
|
return _markedTextAttributes;
|
|
}
|
|
|
|
- (void) alignJustified: (id)sender
|
|
{
|
|
[self setAlignment: NSJustifiedTextAlignment
|
|
range: [self rangeForUserParagraphAttributeChange]];
|
|
}
|
|
|
|
- (void) changeColor: (id)sender
|
|
{
|
|
NSColor *aColor = (NSColor*)[sender color];
|
|
NSRange aRange = [self rangeForUserCharacterAttributeChange];
|
|
|
|
if (aRange.location == NSNotFound)
|
|
return;
|
|
|
|
// sets the color for the selected range.
|
|
[self setTextColor: aColor range: aRange];
|
|
}
|
|
|
|
- (void) setAlignment: (NSTextAlignment)alignment range: (NSRange)aRange
|
|
{
|
|
NSParagraphStyle *style;
|
|
NSMutableParagraphStyle *mstyle;
|
|
|
|
if (aRange.location == NSNotFound)
|
|
return;
|
|
|
|
if (![self shouldChangeTextInRange: aRange replacementString: nil])
|
|
return;
|
|
[_textStorage beginEditing];
|
|
[_textStorage setAlignment: alignment
|
|
range: aRange];
|
|
[_textStorage endEditing];
|
|
[self didChangeText];
|
|
|
|
// Set the typing attributes
|
|
style = [_typingAttributes objectForKey: NSParagraphStyleAttributeName];
|
|
if (style == nil)
|
|
style = [NSParagraphStyle defaultParagraphStyle];
|
|
|
|
mstyle = [style mutableCopy];
|
|
|
|
[mstyle setAlignment: alignment];
|
|
// FIXME: Should use setTypingAttributes
|
|
[_typingAttributes setObject: mstyle forKey: NSParagraphStyleAttributeName];
|
|
RELEASE (mstyle);
|
|
}
|
|
|
|
- (void) insertText: (NSString*) insertString
|
|
{
|
|
NSRange insertRange = [self rangeForUserTextChange];
|
|
|
|
if (insertRange.location == NSNotFound)
|
|
return;
|
|
|
|
if (_tf.is_rich_text)
|
|
{
|
|
[self replaceRange: insertRange
|
|
withAttributedString: AUTORELEASE([[NSAttributedString alloc]
|
|
initWithString: insertString
|
|
attributes: _typingAttributes])];
|
|
}
|
|
else
|
|
{
|
|
[self replaceCharactersInRange: insertRange
|
|
withString: insertString];
|
|
}
|
|
|
|
// move cursor <!> [self selectionRangeForProposedRange: ]
|
|
[self setSelectedRange:
|
|
NSMakeRange (insertRange.location + [insertString length], 0)];
|
|
}
|
|
|
|
- (void) setTypingAttributes: (NSDictionary*)dict
|
|
{
|
|
if (dict == nil)
|
|
{
|
|
dict = [isa defaultTypingAttributes];
|
|
}
|
|
|
|
if ([dict isKindOfClass: [NSMutableDictionary class]] == NO)
|
|
{
|
|
RELEASE (_typingAttributes);
|
|
_typingAttributes = [[NSMutableDictionary alloc]
|
|
initWithDictionary: dict];
|
|
}
|
|
else
|
|
{
|
|
ASSIGN (_typingAttributes, (NSMutableDictionary*)dict);
|
|
}
|
|
[self updateFontPanel];
|
|
[self updateRuler];
|
|
}
|
|
|
|
- (NSDictionary*) typingAttributes
|
|
{
|
|
return [NSDictionary dictionaryWithDictionary: _typingAttributes];
|
|
}
|
|
|
|
- (void) useStandardKerning: (id)sender
|
|
{
|
|
// rekern for selected range if rich text, else rekern entire document.
|
|
NSRange aRange = [self rangeForUserCharacterAttributeChange];
|
|
|
|
if (aRange.location == NSNotFound)
|
|
return;
|
|
|
|
if (![self shouldChangeTextInRange: aRange
|
|
replacementString: nil])
|
|
return;
|
|
[_textStorage beginEditing];
|
|
[_textStorage removeAttribute: NSKernAttributeName
|
|
range: aRange];
|
|
[_textStorage endEditing];
|
|
[self didChangeText];
|
|
}
|
|
|
|
- (void) lowerBaseline: (id)sender
|
|
{
|
|
id value;
|
|
float sValue;
|
|
NSRange effRange;
|
|
NSRange aRange = [self rangeForUserCharacterAttributeChange];
|
|
|
|
if (aRange.location == NSNotFound)
|
|
return;
|
|
|
|
if (![self shouldChangeTextInRange: aRange
|
|
replacementString: nil])
|
|
return;
|
|
[_textStorage beginEditing];
|
|
// We take the value form the first character and use it for the whole range
|
|
value = [_textStorage attribute: NSBaselineOffsetAttributeName
|
|
atIndex: aRange.location
|
|
effectiveRange: &effRange];
|
|
|
|
if (value != nil)
|
|
sValue = [value floatValue] + 1.0;
|
|
else
|
|
sValue = 1.0;
|
|
|
|
[_textStorage addAttribute: NSBaselineOffsetAttributeName
|
|
value: [NSNumber numberWithFloat: sValue]
|
|
range: aRange];
|
|
}
|
|
|
|
- (void) raiseBaseline: (id)sender
|
|
{
|
|
id value;
|
|
float sValue;
|
|
NSRange effRange;
|
|
NSRange aRange = [self rangeForUserCharacterAttributeChange];
|
|
|
|
if (aRange.location == NSNotFound)
|
|
return;
|
|
|
|
if (![self shouldChangeTextInRange: aRange
|
|
replacementString: nil])
|
|
return;
|
|
[_textStorage beginEditing];
|
|
// We take the value form the first character and use it for the whole range
|
|
value = [_textStorage attribute: NSBaselineOffsetAttributeName
|
|
atIndex: aRange.location
|
|
effectiveRange: &effRange];
|
|
|
|
if (value != nil)
|
|
sValue = [value floatValue] - 1.0;
|
|
else
|
|
sValue = -1.0;
|
|
|
|
[_textStorage addAttribute: NSBaselineOffsetAttributeName
|
|
value: [NSNumber numberWithFloat: sValue]
|
|
range: aRange];
|
|
[_textStorage endEditing];
|
|
[self didChangeText];
|
|
}
|
|
|
|
- (void) turnOffKerning: (id)sender
|
|
{
|
|
NSRange aRange = [self rangeForUserCharacterAttributeChange];
|
|
|
|
if (aRange.location == NSNotFound)
|
|
return;
|
|
|
|
if (![self shouldChangeTextInRange: aRange
|
|
replacementString: nil])
|
|
return;
|
|
[_textStorage beginEditing];
|
|
[_textStorage addAttribute: NSKernAttributeName
|
|
value: [NSNumber numberWithFloat: 0.0]
|
|
range: aRange];
|
|
[_textStorage endEditing];
|
|
[self didChangeText];
|
|
}
|
|
|
|
- (void) loosenKerning: (id)sender
|
|
{
|
|
NSRange aRange = [self rangeForUserCharacterAttributeChange];
|
|
|
|
if (aRange.location == NSNotFound)
|
|
return;
|
|
|
|
if (![self shouldChangeTextInRange: aRange
|
|
replacementString: nil])
|
|
return;
|
|
[_textStorage beginEditing];
|
|
// FIXME: Should use the current kerning and work relative to point size
|
|
[_textStorage addAttribute: NSKernAttributeName
|
|
value: [NSNumber numberWithFloat: 1.0]
|
|
range: aRange];
|
|
[_textStorage endEditing];
|
|
[self didChangeText];
|
|
}
|
|
|
|
- (void) tightenKerning: (id)sender
|
|
{
|
|
NSRange aRange = [self rangeForUserCharacterAttributeChange];
|
|
|
|
if (aRange.location == NSNotFound)
|
|
return;
|
|
|
|
if (![self shouldChangeTextInRange: aRange
|
|
replacementString: nil])
|
|
return;
|
|
[_textStorage beginEditing];
|
|
// FIXME: Should use the current kerning and work relative to point size
|
|
[_textStorage addAttribute: NSKernAttributeName
|
|
value: [NSNumber numberWithFloat: -1.0]
|
|
range: aRange];
|
|
[_textStorage endEditing];
|
|
[self didChangeText];
|
|
}
|
|
|
|
- (void) useStandardLigatures: (id)sender
|
|
{
|
|
NSRange aRange = [self rangeForUserCharacterAttributeChange];
|
|
|
|
if (aRange.location == NSNotFound)
|
|
return;
|
|
|
|
if (![self shouldChangeTextInRange: aRange
|
|
replacementString: nil])
|
|
return;
|
|
[_textStorage beginEditing];
|
|
[_textStorage addAttribute: NSLigatureAttributeName
|
|
value: [NSNumber numberWithInt: 1]
|
|
range: aRange];
|
|
[_textStorage endEditing];
|
|
[self didChangeText];
|
|
}
|
|
|
|
- (void) turnOffLigatures: (id)sender
|
|
{
|
|
NSRange aRange = [self rangeForUserCharacterAttributeChange];
|
|
|
|
if (aRange.location == NSNotFound)
|
|
return;
|
|
|
|
if (![self shouldChangeTextInRange: aRange
|
|
replacementString: nil])
|
|
return;
|
|
[_textStorage beginEditing];
|
|
[_textStorage addAttribute: NSLigatureAttributeName
|
|
value: [NSNumber numberWithInt: 0]
|
|
range: aRange];
|
|
[_textStorage endEditing];
|
|
[self didChangeText];
|
|
}
|
|
|
|
- (void) useAllLigatures: (id)sender
|
|
{
|
|
NSRange aRange = [self rangeForUserCharacterAttributeChange];
|
|
|
|
if (aRange.location == NSNotFound)
|
|
return;
|
|
|
|
if (![self shouldChangeTextInRange: aRange
|
|
replacementString: nil])
|
|
return;
|
|
[_textStorage beginEditing];
|
|
[_textStorage addAttribute: NSLigatureAttributeName
|
|
value: [NSNumber numberWithInt: 2]
|
|
range: aRange];
|
|
[_textStorage endEditing];
|
|
[self didChangeText];
|
|
}
|
|
|
|
- (void) toggleTraditionalCharacterShape: (id)sender
|
|
{
|
|
// TODO
|
|
NSLog(@"Method %s is not implemented for class %s",
|
|
"toggleTraditionalCharacterShape:", "NSTextView");
|
|
}
|
|
|
|
- (void) clickedOnLink: (id)link
|
|
atIndex: (unsigned int)charIndex
|
|
{
|
|
if (_delegate != nil)
|
|
{
|
|
SEL selector = @selector(textView:clickedOnLink:atIndex:);
|
|
|
|
if ([_delegate respondsToSelector: selector])
|
|
{
|
|
[_delegate textView: self clickedOnLink: link atIndex: charIndex];
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
The text is inserted at the insertion point if there is one, otherwise
|
|
replacing the selection.
|
|
*/
|
|
|
|
- (void) pasteAsPlainText: (id)sender
|
|
{
|
|
[self readSelectionFromPasteboard: [NSPasteboard generalPasteboard]
|
|
type: NSStringPboardType];
|
|
}
|
|
|
|
- (void) pasteAsRichText: (id)sender
|
|
{
|
|
[self readSelectionFromPasteboard: [NSPasteboard generalPasteboard]
|
|
type: NSRTFPboardType];
|
|
}
|
|
|
|
- (void) updateFontPanel
|
|
{
|
|
/* Update fontPanel only if told so */
|
|
if (_tf.uses_font_panel)
|
|
{
|
|
NSRange longestRange;
|
|
NSFontManager *fm = [NSFontManager sharedFontManager];
|
|
NSFont *currentFont;
|
|
|
|
if (_selected_range.length > 0) /* Multiple chars selection */
|
|
{
|
|
currentFont = [_textStorage attribute: NSFontAttributeName
|
|
atIndex: _selected_range.location
|
|
longestEffectiveRange: &longestRange
|
|
inRange: _selected_range];
|
|
[fm setSelectedFont: currentFont
|
|
isMultiple: !NSEqualRanges (longestRange, _selected_range)];
|
|
}
|
|
else /* Just Insertion Point. */
|
|
{
|
|
currentFont = [_typingAttributes objectForKey: NSFontAttributeName];
|
|
[fm setSelectedFont: currentFont isMultiple: NO];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void) updateRuler
|
|
{
|
|
/* Update ruler view only if told so */
|
|
if (_tf.uses_ruler && _tf.is_ruler_visible)
|
|
{
|
|
NSParagraphStyle *paraStyle;
|
|
NSScrollView *sv = [self enclosingScrollView];
|
|
NSRulerView *rv = [sv horizontalRulerView];
|
|
NSArray *makers;
|
|
|
|
if (_selected_range.length > 0) /* Multiple chars selection */
|
|
{
|
|
paraStyle = [_textStorage attribute: NSParagraphStyleAttributeName
|
|
atIndex: _selected_range.location
|
|
effectiveRange: NULL];
|
|
}
|
|
else
|
|
{
|
|
paraStyle = [_typingAttributes objectForKey:
|
|
NSParagraphStyleAttributeName];
|
|
}
|
|
|
|
makers = [_layoutManager rulerMarkersForTextView: self
|
|
paragraphStyle: paraStyle
|
|
ruler: rv];
|
|
[rv setMarkers: makers];
|
|
}
|
|
}
|
|
|
|
- (NSArray*) acceptableDragTypes
|
|
{
|
|
return [self readablePasteboardTypes];
|
|
}
|
|
|
|
- (void) updateDragTypeRegistration
|
|
{
|
|
// FIXME: Should change registration for all our text views
|
|
if (_tf.is_editable && _tf.is_rich_text)
|
|
[self registerForDraggedTypes: [self acceptableDragTypes]];
|
|
else
|
|
[self unregisterDraggedTypes];
|
|
}
|
|
|
|
- (BOOL) shouldChangeTextInRange: (NSRange)affectedCharRange
|
|
replacementString: (NSString*)replacementString
|
|
{
|
|
if (BEGAN_EDITING == NO)
|
|
{
|
|
if (([_delegate respondsToSelector: @selector(textShouldBeginEditing:)])
|
|
&& ([_delegate textShouldBeginEditing: _notifObject] == NO))
|
|
return NO;
|
|
|
|
SET_BEGAN_EDITING (YES);
|
|
|
|
[nc postNotificationName: NSTextDidBeginEditingNotification
|
|
object: _notifObject];
|
|
}
|
|
|
|
if (_tvf.delegate_responds_to_should_change)
|
|
{
|
|
return [_delegate shouldChangeTextInRange: affectedCharRange
|
|
replacementString: replacementString];
|
|
}
|
|
|
|
return YES;
|
|
}
|
|
|
|
- (void) didChangeText
|
|
{
|
|
[nc postNotificationName: NSTextDidChangeNotification
|
|
object: _notifObject];
|
|
}
|
|
|
|
- (void) setSmartInsertDeleteEnabled: (BOOL)flag
|
|
{
|
|
_tvf.smart_insert_delete = flag;
|
|
}
|
|
|
|
- (BOOL) smartInsertDeleteEnabled
|
|
{
|
|
return _tvf.smart_insert_delete;
|
|
}
|
|
|
|
- (NSRange) smartDeleteRangeForProposedRange: (NSRange)proposedCharRange
|
|
{
|
|
// FIXME.
|
|
return proposedCharRange;
|
|
}
|
|
|
|
- (NSString *)smartInsertAfterStringForString: (NSString *)aString
|
|
replacingRange: (NSRange)charRange
|
|
{
|
|
// FIXME.
|
|
return nil;
|
|
}
|
|
|
|
- (NSString *)smartInsertBeforeStringForString: (NSString *)aString
|
|
replacingRange: (NSRange)charRange
|
|
{
|
|
// FIXME.
|
|
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];
|
|
}
|
|
|
|
/*
|
|
* Handling Events
|
|
*/
|
|
- (void) mouseDown: (NSEvent*)theEvent
|
|
{
|
|
NSSelectionAffinity affinity = [self selectionAffinity];
|
|
NSSelectionGranularity granularity = NSSelectByCharacter;
|
|
NSRange chosenRange, proposedRange;
|
|
NSPoint point, startPoint;
|
|
NSEvent *currentEvent;
|
|
unsigned startIndex;
|
|
unsigned mask;
|
|
|
|
/* If non selectable then ignore the mouse down. */
|
|
if (_tf.is_selectable == NO)
|
|
{
|
|
return;
|
|
}
|
|
|
|
/* Otherwise, NSWindow has already made us first responder (if
|
|
possible) */
|
|
|
|
startPoint = [self convertPoint: [theEvent locationInWindow] fromView: nil];
|
|
startIndex = [self characterIndexForPoint: startPoint];
|
|
|
|
if ([theEvent modifierFlags] & NSShiftKeyMask)
|
|
{
|
|
/* Shift-click is for extending an existing selection using
|
|
the existing granularity */
|
|
granularity = _selectionGranularity;
|
|
/* Compute the new selection */
|
|
proposedRange = NSMakeRange (startIndex, 0);
|
|
proposedRange = NSUnionRange (_selected_range, proposedRange);
|
|
proposedRange = [self selectionRangeForProposedRange: proposedRange
|
|
granularity: granularity];
|
|
/* Merge it with the old one */
|
|
proposedRange = NSUnionRange (_selected_range, proposedRange);
|
|
/* Now decide what happens if the user shift-drags. The range
|
|
will be based in startIndex, so we need to adjust it. */
|
|
if (startIndex <= _selected_range.location)
|
|
{
|
|
startIndex = NSMaxRange (proposedRange);
|
|
}
|
|
else
|
|
{
|
|
startIndex = proposedRange.location;
|
|
}
|
|
}
|
|
else /* No shift */
|
|
{
|
|
switch ([theEvent clickCount])
|
|
{
|
|
case 1: granularity = NSSelectByCharacter;
|
|
break;
|
|
case 2: granularity = NSSelectByWord;
|
|
break;
|
|
case 3: granularity = NSSelectByParagraph;
|
|
break;
|
|
}
|
|
|
|
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)
|
|
{
|
|
if ([_textStorage containsAttachments])
|
|
{
|
|
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)
|
|
{
|
|
/* FIXME: Where to get the cellFrame? */
|
|
NSRect cellFrame = NSMakeRect(0, 0, 0, 0);
|
|
|
|
/* FIXME: 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.*/
|
|
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];
|
|
[self setSelectedRange: chosenRange affinity: affinity
|
|
stillSelecting: YES];
|
|
|
|
/* Do an immediate redisplay for visual feedback */
|
|
[self displayIfNeeded];
|
|
|
|
/* Enter modal loop tracking the mouse */
|
|
|
|
mask = NSLeftMouseDraggedMask | NSLeftMouseUpMask;
|
|
|
|
for (currentEvent = [_window nextEventMatchingMask: mask];
|
|
[currentEvent type] != NSLeftMouseUp;
|
|
currentEvent = [_window nextEventMatchingMask: mask])
|
|
{
|
|
BOOL didScroll = [self autoscroll: currentEvent];
|
|
|
|
point = [self convertPoint: [currentEvent locationInWindow]
|
|
fromView: nil];
|
|
proposedRange = MakeRangeFromAbs ([self characterIndexForPoint: point],
|
|
startIndex);
|
|
chosenRange = [self selectionRangeForProposedRange: proposedRange
|
|
granularity: granularity];
|
|
[self setSelectedRange: chosenRange affinity: affinity
|
|
stillSelecting: YES];
|
|
|
|
if (didScroll)
|
|
{
|
|
/* FIXME: Only redisplay where needed, and avoid relayout */
|
|
[self setNeedsDisplay: YES];
|
|
}
|
|
|
|
/* Do an immediate redisplay for visual feedback */
|
|
[self displayIfNeeded];
|
|
}
|
|
|
|
NSDebugLog(@"chosenRange. location = %d, length = %d\n",
|
|
(int)chosenRange.location, (int)chosenRange.length);
|
|
|
|
[self setSelectedRange: chosenRange affinity: affinity
|
|
stillSelecting: NO];
|
|
|
|
/* Ahm - this shouldn't really be needed but... */
|
|
[self displayIfNeeded];
|
|
|
|
/* Remember granularity till a new selection destroys the memory */
|
|
[self setSelectionGranularity: granularity];
|
|
}
|
|
|
|
- (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]];
|
|
}
|
|
}
|
|
|
|
- (void) insertNewline: (id)sender
|
|
{
|
|
if (_tf.is_field_editor)
|
|
{
|
|
[self _illegalMovement: NSReturnTextMovement];
|
|
return;
|
|
}
|
|
|
|
[self insertText: @"\n"];
|
|
}
|
|
|
|
- (void) insertTab: (id)sender
|
|
{
|
|
if (_tf.is_field_editor)
|
|
{
|
|
[self _illegalMovement: NSTabTextMovement];
|
|
return;
|
|
}
|
|
|
|
[self insertText: @"\t"];
|
|
}
|
|
|
|
- (void) insertBacktab: (id)sender
|
|
{
|
|
if (_tf.is_field_editor)
|
|
{
|
|
[self _illegalMovement: NSBacktabTextMovement];
|
|
return;
|
|
}
|
|
|
|
//[self insertText: @"\t"];
|
|
}
|
|
|
|
- (void) deleteForward: (id)sender
|
|
{
|
|
NSRange range = [self rangeForUserTextChange];
|
|
|
|
if (range.location == NSNotFound)
|
|
{
|
|
return;
|
|
}
|
|
|
|
/* Manage case of insertion point - implicitly means to delete following
|
|
character */
|
|
if (range.length == 0)
|
|
{
|
|
if (range.location != [_textStorage length])
|
|
{
|
|
/* Not at the end of text -- delete following character */
|
|
range.length = 1;
|
|
}
|
|
else
|
|
{
|
|
/* At the end of text - TODO: Make beeping or not beeping
|
|
configurable vie User Defaults */
|
|
NSBeep ();
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (![self shouldChangeTextInRange: range replacementString: @""])
|
|
{
|
|
return;
|
|
}
|
|
|
|
[_textStorage beginEditing];
|
|
[_textStorage deleteCharactersInRange: range];
|
|
[_textStorage endEditing];
|
|
[self didChangeText];
|
|
|
|
/* The new selected range is just the insertion point at the beginning
|
|
of deleted range */
|
|
[self setSelectedRange: NSMakeRange (range.location, 0)];
|
|
}
|
|
|
|
- (void) deleteBackward: (id)sender
|
|
{
|
|
NSRange range = [self rangeForUserTextChange];
|
|
|
|
if (range.location == NSNotFound)
|
|
{
|
|
return;
|
|
}
|
|
|
|
/* Manage case of insertion point - implicitly means to delete
|
|
previous character */
|
|
if (range.length == 0)
|
|
{
|
|
if (range.location != 0)
|
|
{
|
|
/* Not at the beginning of text -- delete previous character */
|
|
range.location -= 1;
|
|
range.length = 1;
|
|
}
|
|
else
|
|
{
|
|
/* At the beginning of text - TODO: Make beeping or not
|
|
beeping configurable via User Defaults */
|
|
NSBeep ();
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (![self shouldChangeTextInRange: range replacementString: @""])
|
|
{
|
|
return;
|
|
}
|
|
|
|
[_textStorage beginEditing];
|
|
[_textStorage deleteCharactersInRange: range];
|
|
[_textStorage endEditing];
|
|
[self didChangeText];
|
|
|
|
/* The new selected range is just the insertion point at the beginning
|
|
of deleted range */
|
|
[self setSelectedRange: NSMakeRange (range.location, 0)];
|
|
}
|
|
|
|
- (void) moveUp: (id)sender
|
|
{
|
|
float originalInsertionPoint;
|
|
float startingY;
|
|
unsigned newLocation;
|
|
|
|
if (_tf.is_field_editor)
|
|
return;
|
|
|
|
/* Do nothing if we are at beginning of text */
|
|
if (_selected_range.location == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
/* Read from memory the horizontal position we aim to move the cursor
|
|
at on the next line */
|
|
originalInsertionPoint = _originalInsertPoint;
|
|
|
|
/* Ask the layout manager to compute where to go */
|
|
startingY = NSMidY (_insertionPointRect);
|
|
newLocation = [_layoutManager
|
|
_charIndexForInsertionPointMovingFromY: startingY
|
|
bestX: originalInsertionPoint
|
|
up: YES
|
|
textContainer: _textContainer];
|
|
|
|
/* Move the insertion point */
|
|
[self setSelectedRange: NSMakeRange (newLocation, 0)];
|
|
|
|
/* Restore the _originalInsertPoint (which was changed
|
|
by setSelectedRange:) because we don't want it to change between
|
|
moveUp:/moveDown: operations. */
|
|
_originalInsertPoint = originalInsertionPoint;
|
|
}
|
|
|
|
- (void) moveDown: (id)sender
|
|
{
|
|
float originalInsertionPoint;
|
|
float startingY;
|
|
unsigned newLocation;
|
|
|
|
if (_tf.is_field_editor)
|
|
return;
|
|
|
|
/* Do nothing if we are at end of text */
|
|
if (_selected_range.location == [_textStorage length])
|
|
{
|
|
return;
|
|
}
|
|
|
|
/* Read from memory the horizontal position we aim to move the cursor
|
|
at on the next line */
|
|
originalInsertionPoint = _originalInsertPoint;
|
|
|
|
/* Ask the layout manager to compute where to go */
|
|
startingY = NSMidY (_insertionPointRect);
|
|
newLocation = [_layoutManager
|
|
_charIndexForInsertionPointMovingFromY: startingY
|
|
bestX: originalInsertionPoint
|
|
up: NO
|
|
textContainer: _textContainer];
|
|
|
|
/* Move the insertion point */
|
|
[self setSelectedRange: NSMakeRange (newLocation, 0)];
|
|
|
|
/* Restore the _originalInsertPoint (which was changed
|
|
by setSelectedRange:) because we don't want it to change between
|
|
moveUp:/moveDown: operations. */
|
|
_originalInsertPoint = originalInsertionPoint;
|
|
}
|
|
|
|
- (void) moveLeft: (id)sender
|
|
{
|
|
unsigned newLocation;
|
|
|
|
/* Do nothing if we are at beginning of text with no selection */
|
|
if (_selected_range.location == 0 && _selected_range.length == 0)
|
|
return;
|
|
|
|
if (_selected_range.location == 0)
|
|
{
|
|
newLocation = 0;
|
|
}
|
|
else
|
|
{
|
|
newLocation = _selected_range.location - 1;
|
|
}
|
|
|
|
[self setSelectedRange: NSMakeRange (newLocation, 0)];
|
|
}
|
|
|
|
- (void) moveRight: (id)sender
|
|
{
|
|
unsigned int length = [_textStorage length];
|
|
unsigned newLocation;
|
|
|
|
/* Do nothing if we are at end of text */
|
|
if (_selected_range.location == length)
|
|
return;
|
|
|
|
newLocation = MIN (NSMaxRange (_selected_range) + 1, length);
|
|
|
|
[self setSelectedRange: NSMakeRange (newLocation, 0)];
|
|
}
|
|
|
|
- (void) moveBackwardAndModifySelection: (id)sender
|
|
{
|
|
NSRange newRange;
|
|
|
|
/* Do nothing if we are at beginning of text. */
|
|
if (_selected_range.location == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
/* Turn to select by character. */
|
|
[self setSelectionGranularity: NSSelectByCharacter];
|
|
|
|
/* Extend the selection on the left. */
|
|
newRange = NSMakeRange (_selected_range.location - 1,
|
|
_selected_range.length + 1);
|
|
|
|
newRange = [self selectionRangeForProposedRange: newRange
|
|
granularity: NSSelectByCharacter];
|
|
|
|
[self setSelectedRange: newRange];
|
|
}
|
|
|
|
- (void) moveForwardAndModifySelection: (id)sender
|
|
{
|
|
unsigned int length = [_textStorage length];
|
|
NSRange newRange;
|
|
|
|
/* Do nothing if we are at end of text */
|
|
if (_selected_range.location == length)
|
|
return;
|
|
|
|
/* Turn to select by character. */
|
|
[self setSelectionGranularity: NSSelectByCharacter];
|
|
|
|
/* Extend the selection on the right. */
|
|
newRange = NSMakeRange (_selected_range.location,
|
|
_selected_range.length + 1);
|
|
|
|
newRange = [self selectionRangeForProposedRange: newRange
|
|
granularity: NSSelectByCharacter];
|
|
|
|
[self setSelectedRange: newRange];
|
|
}
|
|
|
|
- (void) moveWordBackward: (id)sender
|
|
{
|
|
unsigned newLocation;
|
|
|
|
newLocation = [_textStorage nextWordFromIndex: _selected_range.location
|
|
forward: NO];
|
|
|
|
[self setSelectedRange: NSMakeRange (newLocation, 0)];
|
|
}
|
|
|
|
- (void) moveWordForward: (id)sender
|
|
{
|
|
unsigned newLocation;
|
|
|
|
newLocation = [_textStorage nextWordFromIndex: _selected_range.location
|
|
forward: YES];
|
|
|
|
[self setSelectedRange: NSMakeRange (newLocation, 0)];
|
|
}
|
|
|
|
- (void) moveWordBackwardAndModifySelection: (id)sender
|
|
{
|
|
unsigned newLocation;
|
|
NSRange newRange;
|
|
|
|
[self setSelectionGranularity: NSSelectByWord];
|
|
|
|
newLocation = [_textStorage nextWordFromIndex: _selected_range.location
|
|
forward: NO];
|
|
|
|
newRange = NSMakeRange (newLocation,
|
|
NSMaxRange (_selected_range) - newLocation);
|
|
|
|
newRange = [self selectionRangeForProposedRange: newRange
|
|
granularity: NSSelectByCharacter];
|
|
|
|
[self setSelectedRange: newRange];
|
|
}
|
|
|
|
- (void) moveWordForwardAndModifySelection: (id)sender
|
|
{
|
|
unsigned newMaxRange;
|
|
NSRange newRange;
|
|
|
|
[self setSelectionGranularity: NSSelectByWord];
|
|
|
|
newMaxRange = [_textStorage nextWordFromIndex: NSMaxRange (_selected_range)
|
|
forward: YES];
|
|
|
|
newRange = NSMakeRange (_selected_range.location,
|
|
newMaxRange - _selected_range.location);
|
|
|
|
newRange = [self selectionRangeForProposedRange: newRange
|
|
granularity: NSSelectByCharacter];
|
|
|
|
[self setSelectedRange: newRange];
|
|
}
|
|
|
|
- (void) moveToBeginningOfDocument: (id)sender
|
|
{
|
|
[self setSelectedRange: NSMakeRange (0, 0)];
|
|
}
|
|
|
|
- (void) moveToBeginningOfLine: (id)sender
|
|
{
|
|
NSRange aRange;
|
|
NSRect ignored;
|
|
|
|
/* We do nothing if we are at the beginning of the text. */
|
|
if (_selected_range.location == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ignored = [_layoutManager lineFragmentRectForGlyphAtIndex:
|
|
_selected_range.location
|
|
effectiveRange: &aRange];
|
|
|
|
[self setSelectedRange: NSMakeRange (aRange.location, 0)];
|
|
}
|
|
|
|
- (void) moveToEndOfDocument: (id)sender
|
|
{
|
|
unsigned length = [_textStorage length];
|
|
|
|
if (length > 0)
|
|
{
|
|
[self setSelectedRange: NSMakeRange (length, 0)];
|
|
}
|
|
else
|
|
{
|
|
[self setSelectedRange: NSMakeRange (0, 0)];
|
|
}
|
|
}
|
|
|
|
- (void) moveToEndOfLine: (id)sender
|
|
{
|
|
NSRect ignored;
|
|
NSRange line, glyphs;
|
|
unsigned newLocation;
|
|
unsigned maxRange;
|
|
|
|
/* We do nothing if we are at the end of the text. */
|
|
if (_selected_range.location == [_textStorage length])
|
|
{
|
|
return;
|
|
}
|
|
|
|
ignored = [_layoutManager lineFragmentRectForGlyphAtIndex:
|
|
_selected_range.location
|
|
effectiveRange: &glyphs];
|
|
|
|
line = [_layoutManager characterRangeForGlyphRange: glyphs
|
|
actualGlyphRange: NULL];
|
|
|
|
maxRange = NSMaxRange (line);
|
|
|
|
if (maxRange == [_textStorage length])
|
|
{
|
|
/* End of text is special - we want the insertion point to
|
|
appear *after* the last character, which means as if before
|
|
the next (virtual) character after the end of text. */
|
|
newLocation = maxRange;
|
|
}
|
|
else if (maxRange == 0)
|
|
{
|
|
/* Beginning of text is special only for technical reasons -
|
|
since maxRange is an unsigned, we can't safely subtract 1
|
|
from it if it is 0. */
|
|
newLocation = maxRange;
|
|
}
|
|
else
|
|
{
|
|
/* Else, we want the insertion point to appear before the last
|
|
character in the line range. Normally the last character in
|
|
the line range is a space or a newline. */
|
|
newLocation = maxRange - 1;
|
|
|
|
if (newLocation < line.location)
|
|
{
|
|
newLocation = line.location;
|
|
}
|
|
}
|
|
|
|
[self setSelectedRange: NSMakeRange (newLocation, 0) ];
|
|
}
|
|
|
|
- (void) pageDown: (id)sender
|
|
{
|
|
// TODO
|
|
NSLog(@"Method %s is not implemented for class %s",
|
|
"pageDown:", "NSTextView");
|
|
}
|
|
|
|
- (void) pageUp: (id)sender
|
|
{
|
|
// TODO
|
|
NSLog(@"Method %s is not implemented for class %s",
|
|
"pageUp:", "NSTextView");
|
|
}
|
|
|
|
- (void) scrollLineDown: (id)sender
|
|
{
|
|
// TODO
|
|
NSLog(@"Method %s is not implemented for class %s",
|
|
"scrollLineDown:", "NSTextView");
|
|
}
|
|
|
|
- (void) scrollLineUp: (id)sender
|
|
{
|
|
// TODO
|
|
NSLog(@"Method %s is not implemented for class %s",
|
|
"scrollLineUp:", "NSTextView");
|
|
}
|
|
|
|
- (void) scrollPageDown: (id)sender
|
|
{
|
|
// TODO
|
|
NSLog(@"Method %s is not implemented for class %s",
|
|
"scrollPageDown:", "NSTextView");
|
|
}
|
|
|
|
- (void) scrollPageUp: (id)sender
|
|
{
|
|
// TODO
|
|
NSLog(@"Method %s is not implemented for class %s",
|
|
"scrollPageUp:", "NSTextView");
|
|
}
|
|
|
|
|
|
/* -selectAll: inherited from NSText */
|
|
|
|
- (void) selectLine: (id)sender
|
|
{
|
|
if ([_textStorage length] > 0)
|
|
{
|
|
NSRange aRange;
|
|
NSRect ignored;
|
|
|
|
ignored = [_layoutManager lineFragmentRectForGlyphAtIndex:
|
|
_selected_range.location
|
|
effectiveRange: &aRange];
|
|
|
|
[self setSelectedRange: aRange];
|
|
}
|
|
}
|
|
|
|
/* The following method is bound to 'Control-t', and must work like
|
|
* pressing 'Control-t' inside Emacs. For example, say that I type
|
|
* 'Nicoal' in a NSTextView. Then, I press 'Control-t'. This should
|
|
* swap the last two characters which were inserted, thus swapping the
|
|
* 'a' and the 'l', and changing the text to read 'Nicola'. */
|
|
- (void) transpose: (id)sender
|
|
{
|
|
NSRange range;
|
|
NSString *string;
|
|
NSString *replacementString;
|
|
unichar chars[2];
|
|
unichar tmp;
|
|
|
|
/* Do nothing if we are at beginning of text. */
|
|
if (_selected_range.location < 2)
|
|
{
|
|
return;
|
|
}
|
|
|
|
range = NSMakeRange (_selected_range.location - 2, 2);
|
|
|
|
/* Get the two chars. */
|
|
string = [_textStorage string];
|
|
chars[0] = [string characterAtIndex: (_selected_range.location - 2)];
|
|
chars[1] = [string characterAtIndex: (_selected_range.location - 1)];
|
|
|
|
/* Swap them. */
|
|
tmp = chars[0];
|
|
chars[0] = chars[1];
|
|
chars[1] = tmp;
|
|
|
|
/* Replace the original chars with the swapped ones. */
|
|
replacementString = [NSString stringWithCharacters: chars length: 2];
|
|
[self replaceCharactersInRange: range withString: replacementString];
|
|
}
|
|
|
|
|
|
- (BOOL) acceptsFirstResponder
|
|
{
|
|
if (_tf.is_selectable)
|
|
{
|
|
return YES;
|
|
}
|
|
else
|
|
{
|
|
return NO;
|
|
}
|
|
}
|
|
|
|
- (BOOL) resignFirstResponder
|
|
{
|
|
if (_insertionPointTimer != nil)
|
|
{
|
|
[_insertionPointTimer invalidate];
|
|
DESTROY (_insertionPointTimer);
|
|
_drawInsertionPointNow = NO;
|
|
}
|
|
|
|
if (_tvf.multiple_textviews == YES)
|
|
{
|
|
id futureFirstResponder;
|
|
NSArray *textContainers;
|
|
int 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 */
|
|
|
|
if ([self shouldDrawInsertionPoint])
|
|
{
|
|
[self updateInsertionPointStateAndRestartTimer: NO];
|
|
}
|
|
|
|
SET_BEGAN_EDITING (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. */
|
|
[nc postNotificationName: NSTextDidEndEditingNotification
|
|
object: _notifObject];
|
|
|
|
return YES;
|
|
}
|
|
|
|
- (BOOL) becomeFirstResponder
|
|
{
|
|
if (_tf.is_selectable == NO)
|
|
{
|
|
return NO;
|
|
}
|
|
|
|
/* NB: Notifications (NSTextBeginEditingNotification etc) are managed
|
|
on the first time the user tries to edit us. */
|
|
|
|
/* Draw selection, update insertion point */
|
|
|
|
if ([self shouldDrawInsertionPoint])
|
|
{
|
|
[self updateInsertionPointStateAndRestartTimer: YES];
|
|
}
|
|
|
|
return YES;
|
|
}
|
|
|
|
- (void) drawRect: (NSRect)rect
|
|
{
|
|
/* TODO: Only do relayout if needed */
|
|
NSRange drawnRange = [_layoutManager glyphRangeForBoundingRect: rect
|
|
inTextContainer: _textContainer];
|
|
if (_tf.draws_background)
|
|
{
|
|
[_layoutManager drawBackgroundForGlyphRange: drawnRange
|
|
atPoint: _textContainerOrigin];
|
|
}
|
|
|
|
[_layoutManager drawGlyphsForGlyphRange: drawnRange
|
|
atPoint: _textContainerOrigin];
|
|
|
|
if ([self shouldDrawInsertionPoint])
|
|
{
|
|
unsigned location = _selected_range.location;
|
|
|
|
if (NSLocationInRange (location, drawnRange)
|
|
|| location == NSMaxRange (drawnRange))
|
|
{
|
|
if (_drawInsertionPointNow)
|
|
{
|
|
[self drawInsertionPointInRect: _insertionPointRect
|
|
color: _caret_color
|
|
turnedOn: YES];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Ruler Views
|
|
*/
|
|
- (void) rulerView: (NSRulerView*)aRulerView
|
|
didMoveMarker: (NSRulerMarker*)aMarker
|
|
{
|
|
NSTextTab *old_tab = [aMarker representedObject];
|
|
NSTextTab *new_tab = [[NSTextTab alloc] initWithType: [old_tab tabStopType]
|
|
location: [aMarker makerLocation]];
|
|
NSRange range = [self rangeForUserParagraphAttributeChange];
|
|
unsigned loc = range.location;
|
|
NSParagraphStyle *style;
|
|
NSMutableParagraphStyle *mstyle;
|
|
|
|
[_textStorage beginEditing];
|
|
while (loc < NSMaxRange(range))
|
|
{
|
|
id value;
|
|
BOOL copiedStyle = NO;
|
|
NSRange effRange;
|
|
NSRange newRange;
|
|
|
|
value = [_textStorage attribute: NSParagraphStyleAttributeName
|
|
atIndex: loc
|
|
effectiveRange: &effRange];
|
|
newRange = NSIntersectionRange (effRange, range);
|
|
|
|
if (value == nil)
|
|
{
|
|
value = [NSMutableParagraphStyle defaultParagraphStyle];
|
|
}
|
|
else
|
|
{
|
|
value = [value mutableCopy];
|
|
copiedStyle = YES;
|
|
}
|
|
|
|
[value removeTabStop: old_tab];
|
|
[value addTabStop: new_tab];
|
|
|
|
[_textStorage addAttribute: NSParagraphStyleAttributeName
|
|
value: value
|
|
range: newRange];
|
|
if (copiedStyle == YES)
|
|
{
|
|
RELEASE(value);
|
|
}
|
|
loc = NSMaxRange (effRange);
|
|
}
|
|
[_textStorage endEditing];
|
|
[self didChangeText];
|
|
|
|
// Set the typing attributes
|
|
style = [_typingAttributes objectForKey: NSParagraphStyleAttributeName];
|
|
if (style == nil)
|
|
style = [NSParagraphStyle defaultParagraphStyle];
|
|
|
|
mstyle = [style mutableCopy];
|
|
|
|
[mstyle removeTabStop: old_tab];
|
|
[mstyle addTabStop: new_tab];
|
|
// FIXME: Should use setTypingAttributes
|
|
[_typingAttributes setObject: mstyle forKey: NSParagraphStyleAttributeName];
|
|
RELEASE (mstyle);
|
|
|
|
[aMarker setRepresentedObject: new_tab];
|
|
RELEASE(new_tab);
|
|
}
|
|
|
|
- (void) rulerView: (NSRulerView*)aRulerView
|
|
didRemoveMarker: (NSRulerMarker*)aMarker
|
|
{
|
|
NSTextTab *tab = [aMarker representedObject];
|
|
NSRange range = [self rangeForUserParagraphAttributeChange];
|
|
unsigned loc = range.location;
|
|
NSParagraphStyle *style;
|
|
NSMutableParagraphStyle *mstyle;
|
|
|
|
[_textStorage beginEditing];
|
|
while (loc < NSMaxRange(range))
|
|
{
|
|
id value;
|
|
BOOL copiedStyle = NO;
|
|
NSRange effRange;
|
|
NSRange newRange;
|
|
|
|
value = [_textStorage attribute: NSParagraphStyleAttributeName
|
|
atIndex: loc
|
|
effectiveRange: &effRange];
|
|
newRange = NSIntersectionRange (effRange, range);
|
|
|
|
if (value == nil)
|
|
{
|
|
value = [NSMutableParagraphStyle defaultParagraphStyle];
|
|
}
|
|
else
|
|
{
|
|
value = [value mutableCopy];
|
|
copiedStyle = YES;
|
|
}
|
|
|
|
[value removeTabStop: tab];
|
|
|
|
[_textStorage addAttribute: NSParagraphStyleAttributeName
|
|
value: value
|
|
range: newRange];
|
|
if (copiedStyle == YES)
|
|
{
|
|
RELEASE(value);
|
|
}
|
|
loc = NSMaxRange (effRange);
|
|
}
|
|
[_textStorage endEditing];
|
|
[self didChangeText];
|
|
|
|
// Set the typing attributes
|
|
style = [_typingAttributes objectForKey: NSParagraphStyleAttributeName];
|
|
if (style == nil)
|
|
style = [NSParagraphStyle defaultParagraphStyle];
|
|
|
|
mstyle = [style mutableCopy];
|
|
|
|
[mstyle removeTabStop: tab];
|
|
// FIXME: Should use setTypingAttributes
|
|
[_typingAttributes setObject: mstyle forKey: NSParagraphStyleAttributeName];
|
|
RELEASE (mstyle);
|
|
}
|
|
|
|
- (void)rulerView:(NSRulerView *)ruler
|
|
didAddMarker:(NSRulerMarker *)marker
|
|
{
|
|
NSTextTab *tab = [marker representedObject];
|
|
NSRange range = [self rangeForUserParagraphAttributeChange];
|
|
unsigned loc = range.location;
|
|
NSParagraphStyle *style;
|
|
NSMutableParagraphStyle *mstyle;
|
|
|
|
[_textStorage beginEditing];
|
|
while (loc < NSMaxRange(range))
|
|
{
|
|
id value;
|
|
BOOL copiedStyle = NO;
|
|
NSRange effRange;
|
|
NSRange newRange;
|
|
|
|
value = [_textStorage attribute: NSParagraphStyleAttributeName
|
|
atIndex: loc
|
|
effectiveRange: &effRange];
|
|
newRange = NSIntersectionRange (effRange, range);
|
|
|
|
if (value == nil)
|
|
{
|
|
value = [NSMutableParagraphStyle defaultParagraphStyle];
|
|
}
|
|
else
|
|
{
|
|
value = [value mutableCopy];
|
|
copiedStyle = YES;
|
|
}
|
|
|
|
[value addTabStop: tab];
|
|
|
|
[_textStorage addAttribute: NSParagraphStyleAttributeName
|
|
value: value
|
|
range: newRange];
|
|
if (copiedStyle == YES)
|
|
{
|
|
RELEASE(value);
|
|
}
|
|
loc = NSMaxRange (effRange);
|
|
}
|
|
[_textStorage endEditing];
|
|
[self didChangeText];
|
|
|
|
// Set the typing attributes
|
|
style = [_typingAttributes objectForKey: NSParagraphStyleAttributeName];
|
|
if (style == nil)
|
|
style = [NSParagraphStyle defaultParagraphStyle];
|
|
|
|
mstyle = [style mutableCopy];
|
|
|
|
[mstyle addTabStop: tab];
|
|
// FIXME: Should use setTypingAttributes
|
|
[_typingAttributes setObject: mstyle forKey: NSParagraphStyleAttributeName];
|
|
RELEASE (mstyle);
|
|
}
|
|
|
|
- (void) rulerView: (NSRulerView*)aRulerView
|
|
handleMouseDown: (NSEvent*)theEvent
|
|
{
|
|
NSPoint point = [aRulerView convertPoint: [theEvent locationInWindow]
|
|
fromView: nil];
|
|
float location = point.x;
|
|
// FIXME This image does not exist.
|
|
NSRulerMarker *marker = [[NSRulerMarker alloc]
|
|
initWithRulerView: aRulerView
|
|
markerLocation: location
|
|
image: [NSImage imageNamed: @"common_LeftTabStop"]
|
|
imageOrigin: NSMakePoint(0, 0)];
|
|
NSTextTab *tab = [[NSTextTab alloc] initWithType: NSLeftTabStopType
|
|
location: location];
|
|
|
|
[marker setRepresentedObject: tab];
|
|
[aRulerView addMarker: marker];
|
|
RELEASE(marker);
|
|
AUTORELEASE(tab);
|
|
}
|
|
|
|
- (BOOL) rulerView: (NSRulerView*)aRulerView
|
|
shouldAddMarker: (NSRulerMarker*)aMarker
|
|
{
|
|
return [self shouldChangeTextInRange: [self rangeForUserParagraphAttributeChange]
|
|
replacementString: nil];
|
|
}
|
|
|
|
- (BOOL) rulerView: (NSRulerView*)aRulerView
|
|
shouldMoveMarker: (NSRulerMarker*)aMarker
|
|
{
|
|
return [self shouldChangeTextInRange: [self rangeForUserParagraphAttributeChange]
|
|
replacementString: nil];
|
|
}
|
|
|
|
- (BOOL) rulerView: (NSRulerView*)aRulerView
|
|
shouldRemoveMarker: (NSRulerMarker*)aMarker
|
|
{
|
|
return [(id)[aMarker representedObject] isKindOfClass: [NSTextTab class]];
|
|
}
|
|
|
|
- (float) rulerView: (NSRulerView*)aRulerView
|
|
willAddMarker: (NSRulerMarker*)aMarker
|
|
atLocation: (float)location
|
|
{
|
|
NSSize size = [_textContainer containerSize];
|
|
|
|
if (location < 0.0)
|
|
return 0.0;
|
|
|
|
if (location > size.width)
|
|
return size.width;
|
|
|
|
return location;
|
|
}
|
|
|
|
- (float) rulerView: (NSRulerView*)aRulerView
|
|
willMoveMarker: (NSRulerMarker*)aMarker
|
|
toLocation: (float)location
|
|
{
|
|
NSSize size = [_textContainer containerSize];
|
|
|
|
if (location < 0.0)
|
|
return 0.0;
|
|
|
|
if (location > size.width)
|
|
return size.width;
|
|
|
|
return location;
|
|
}
|
|
|
|
- (void) setDelegate: (id)anObject
|
|
{
|
|
SEL selector;
|
|
|
|
/* Code to allow sharing the delegate */
|
|
if (_tvf.multiple_textviews && (IS_SYNCHRONIZING_DELEGATES == NO))
|
|
{
|
|
/* Invoke setDelegate: on all the textviews which share this
|
|
delegate. */
|
|
NSArray *array;
|
|
int 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)
|
|
{
|
|
[nc removeObserver: _delegate name: nil object: _notifObject];
|
|
}
|
|
|
|
[super setDelegate: anObject];
|
|
|
|
selector = @selector(shouldChangeTextInRange:replacementString:);
|
|
if ([_delegate respondsToSelector: selector])
|
|
{
|
|
_tvf.delegate_responds_to_should_change = YES;
|
|
}
|
|
else
|
|
{
|
|
_tvf.delegate_responds_to_should_change = NO;
|
|
}
|
|
|
|
selector = @selector(textView:willChangeSelectionFromCharacterRange:toCharacterRange:);
|
|
if ([_delegate respondsToSelector: selector])
|
|
{
|
|
_tvf.delegate_responds_to_will_change_sel = YES;
|
|
}
|
|
else
|
|
{
|
|
_tvf.delegate_responds_to_will_change_sel = 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);
|
|
}
|
|
|
|
- (int) spellCheckerDocumentTag
|
|
{
|
|
if (!_spellCheckerDocumentTag)
|
|
_spellCheckerDocumentTag = [NSSpellChecker uniqueSpellDocumentTag];
|
|
|
|
return _spellCheckerDocumentTag;
|
|
}
|
|
|
|
- (BOOL) isContinuousSpellCheckingEnabled
|
|
{
|
|
// TODO
|
|
NSLog(@"Method %s is not implemented for class %s",
|
|
"isContinuousSpellCheckingEnabled", "NSTextView");
|
|
return NO;
|
|
}
|
|
|
|
- (void) setContinuousSpellCheckingEnabled: (BOOL)flag
|
|
{
|
|
// TODO
|
|
NSLog(@"Method %s is not implemented for class %s",
|
|
"setContinuousSpellCheckingEnabled:", "NSTextView");
|
|
}
|
|
|
|
- (void) toggleContinuousSpellChecking: (id)sender
|
|
{
|
|
[self setContinuousSpellCheckingEnabled:
|
|
![self isContinuousSpellCheckingEnabled]];
|
|
}
|
|
|
|
- (NSRange) selectionRangeForProposedRange: (NSRange)proposedCharRange
|
|
granularity: (NSSelectionGranularity)granul
|
|
{
|
|
unsigned index;
|
|
NSRange aRange;
|
|
NSRange newRange;
|
|
NSString *string = [self string];
|
|
unsigned length = [string length];
|
|
|
|
if (proposedCharRange.location >= length)
|
|
{
|
|
proposedCharRange.location = length;
|
|
proposedCharRange.length = 0;
|
|
return proposedCharRange;
|
|
}
|
|
|
|
if (NSMaxRange (proposedCharRange) > length)
|
|
{
|
|
proposedCharRange.length = length - proposedCharRange.location;
|
|
}
|
|
|
|
if (length == 0)
|
|
{
|
|
return proposedCharRange;
|
|
}
|
|
|
|
switch (granul)
|
|
{
|
|
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;
|
|
}
|
|
}
|
|
|
|
- (NSRange) rangeForUserCharacterAttributeChange
|
|
{
|
|
if (!_tf.is_editable || !_tf.uses_font_panel)
|
|
{
|
|
return NSMakeRange (NSNotFound, 0);
|
|
}
|
|
|
|
if (_tf.is_rich_text)
|
|
{
|
|
// This expects the selection to be already corrected to characters
|
|
return _selected_range;
|
|
}
|
|
else
|
|
{
|
|
return NSMakeRange (0, [_textStorage length]);
|
|
}
|
|
}
|
|
|
|
- (NSRange) rangeForUserParagraphAttributeChange
|
|
{
|
|
if (!_tf.is_editable || !_tf.uses_ruler)
|
|
{
|
|
return NSMakeRange (NSNotFound, 0);
|
|
}
|
|
|
|
if (_tf.is_rich_text)
|
|
{
|
|
return [self selectionRangeForProposedRange: _selected_range
|
|
granularity: NSSelectByParagraph];
|
|
}
|
|
else
|
|
{
|
|
return NSMakeRange (0, [_textStorage length]);
|
|
}
|
|
}
|
|
|
|
- (NSRange) rangeForUserTextChange
|
|
{
|
|
if (!_tf.is_editable)
|
|
{
|
|
return NSMakeRange (NSNotFound, 0);
|
|
}
|
|
|
|
// This expects the selection to be already corrected to characters
|
|
return _selected_range;
|
|
}
|
|
|
|
- (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];
|
|
}
|
|
|
|
- (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. */
|
|
|
|
if ([type isEqualToString: NSStringPboardType])
|
|
{
|
|
[self insertText: [pboard stringForType: NSStringPboardType]];
|
|
return YES;
|
|
}
|
|
|
|
if (_tf.is_rich_text)
|
|
{
|
|
if ([type isEqualToString: NSRTFPboardType])
|
|
{
|
|
[self replaceCharactersInRange: [self rangeForUserTextChange]
|
|
withRTF: [pboard dataForType: NSRTFPboardType]];
|
|
return YES;
|
|
}
|
|
}
|
|
|
|
if (_tf.imports_graphics)
|
|
{
|
|
if ([type isEqualToString: NSRTFDPboardType])
|
|
{
|
|
[self replaceCharactersInRange: [self rangeForUserTextChange]
|
|
withRTFD: [pboard dataForType: NSRTFDPboardType]];
|
|
return YES;
|
|
}
|
|
// FIXME: Should also support: NSTIFFPboardType
|
|
if ([type isEqualToString: NSFileContentsPboardType])
|
|
{
|
|
NSTextAttachment *attachment = [[NSTextAttachment alloc]
|
|
initWithFileWrapper:
|
|
[pboard readFileWrapper]];
|
|
|
|
[self replaceRange: [self rangeForUserTextChange]
|
|
withAttributedString:
|
|
[NSAttributedString attributedStringWithAttachment: attachment]];
|
|
RELEASE(attachment);
|
|
return YES;
|
|
}
|
|
}
|
|
|
|
// color accepting
|
|
if ([type isEqualToString: NSColorPboardType])
|
|
{
|
|
NSColor *color = [NSColor colorFromPasteboard: pboard];
|
|
NSRange aRange = [self rangeForUserCharacterAttributeChange];
|
|
NSMutableDictionary *d = [[self typingAttributes] mutableCopy];
|
|
|
|
[d setObject: color forKey: NSForegroundColorAttributeName];
|
|
[self setTypingAttributes: d];
|
|
RELEASE(d);
|
|
|
|
if (aRange.location != NSNotFound)
|
|
{
|
|
[self setTextColor: color range: aRange];
|
|
}
|
|
|
|
return YES;
|
|
}
|
|
|
|
// font pasting
|
|
if ([type isEqualToString: NSFontPboardType])
|
|
{
|
|
NSData *data = [pboard dataForType: NSFontPboardType];
|
|
NSDictionary *dict = [NSUnarchiver unarchiveObjectWithData: data];
|
|
|
|
if (dict != nil)
|
|
{
|
|
NSRange aRange = [self rangeForUserCharacterAttributeChange];
|
|
NSMutableDictionary *d;
|
|
|
|
if (aRange.location != NSNotFound)
|
|
{
|
|
[self setAttributes: dict range: aRange];
|
|
}
|
|
d = [[self typingAttributes] mutableCopy];
|
|
[d addEntriesFromDictionary: dict];
|
|
[self setTypingAttributes: d];
|
|
RELEASE(d);
|
|
return YES;
|
|
}
|
|
return NO;
|
|
}
|
|
|
|
// ruler pasting
|
|
if ([type isEqualToString: NSRulerPboardType])
|
|
{
|
|
NSData *data = [pboard dataForType: NSRulerPboardType];
|
|
NSDictionary *dict = [NSUnarchiver unarchiveObjectWithData: data];
|
|
|
|
if (dict != nil)
|
|
{
|
|
NSRange aRange = [self rangeForUserParagraphAttributeChange];
|
|
NSMutableDictionary *d;
|
|
|
|
if (aRange.location != NSNotFound)
|
|
{
|
|
[self setAttributes: dict range: aRange];
|
|
}
|
|
d = [[self typingAttributes] mutableCopy];
|
|
[d addEntriesFromDictionary: dict];
|
|
[self setTypingAttributes: d];
|
|
RELEASE(d);
|
|
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.
|
|
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.
|
|
*/
|
|
|
|
return [self writeSelectionToPasteboard: pboard
|
|
types: [NSArray arrayWithObject: type]];
|
|
}
|
|
|
|
- (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 (_selected_range.location == NSNotFound)
|
|
{
|
|
return NO;
|
|
}
|
|
|
|
[pboard declareTypes: types owner: self];
|
|
|
|
enumerator = [types objectEnumerator];
|
|
while ((type = [enumerator nextObject]) != nil)
|
|
{
|
|
if ([type isEqualToString: NSStringPboardType])
|
|
{
|
|
ret = [pboard setString: [[self string] substringWithRange: _selected_range]
|
|
forType: NSStringPboardType] || ret;
|
|
}
|
|
|
|
if ([type isEqualToString: NSRTFPboardType])
|
|
{
|
|
ret = [pboard setData: [self RTFFromRange: _selected_range]
|
|
forType: NSRTFPboardType] || ret;
|
|
}
|
|
|
|
if ([type isEqualToString: NSRTFDPboardType])
|
|
{
|
|
ret = [pboard setData: [self RTFDFromRange: _selected_range]
|
|
forType: NSRTFDPboardType] || ret;
|
|
}
|
|
|
|
if ([type isEqualToString: NSColorPboardType])
|
|
{
|
|
NSColor *color;
|
|
|
|
color = [_textStorage attribute: NSForegroundColorAttributeName
|
|
atIndex: _selected_range.location
|
|
effectiveRange: 0];
|
|
if (color != nil)
|
|
{
|
|
[color writeToPasteboard: pboard];
|
|
ret = YES;
|
|
}
|
|
}
|
|
|
|
if ([type isEqualToString: NSFontPboardType])
|
|
{
|
|
NSDictionary *dict;
|
|
|
|
dict = [_textStorage fontAttributesInRange: _selected_range];
|
|
if (dict != nil)
|
|
{
|
|
[pboard setData: [NSArchiver archivedDataWithRootObject: dict]
|
|
forType: NSFontPboardType];
|
|
ret = YES;
|
|
}
|
|
}
|
|
|
|
if ([type isEqualToString: NSRulerPboardType])
|
|
{
|
|
NSDictionary *dict;
|
|
|
|
dict = [_textStorage rulerAttributesInRange: _selected_range];
|
|
if (dict != nil)
|
|
{
|
|
[pboard setData: [NSArchiver archivedDataWithRootObject: dict]
|
|
forType: NSRulerPboardType];
|
|
ret = YES;
|
|
}
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
// dragging of text, colors and files
|
|
- (unsigned int) draggingEntered: (id <NSDraggingInfo>)sender
|
|
{
|
|
NSPasteboard *pboard = [sender draggingPasteboard];
|
|
NSString *type = [self preferredPasteboardTypeFromArray: [pboard types]
|
|
restrictedToTypesFromArray: [self readablePasteboardTypes]];
|
|
|
|
return [self dragOperationForDraggingInfo: sender
|
|
type: type];
|
|
}
|
|
|
|
- (unsigned int) draggingUpdated: (id <NSDraggingInfo>)sender
|
|
{
|
|
NSPasteboard *pboard = [sender draggingPasteboard];
|
|
NSString *type = [self preferredPasteboardTypeFromArray: [pboard types]
|
|
restrictedToTypesFromArray: [self readablePasteboardTypes]];
|
|
|
|
return [self dragOperationForDraggingInfo: sender
|
|
type: type];
|
|
}
|
|
|
|
- (void) draggingExited: (id <NSDraggingInfo>)sender
|
|
{
|
|
}
|
|
|
|
- (BOOL) prepareForDragOperation: (id <NSDraggingInfo>)sender
|
|
{
|
|
return YES;
|
|
}
|
|
|
|
- (BOOL) performDragOperation: (id <NSDraggingInfo>)sender
|
|
{
|
|
return [self readSelectionFromPasteboard: [sender draggingPasteboard]];
|
|
}
|
|
|
|
- (void) concludeDragOperation: (id <NSDraggingInfo>)sender
|
|
{
|
|
}
|
|
|
|
- (void) cleanUpAfterDragOperation
|
|
{
|
|
// release drag information
|
|
}
|
|
|
|
- (unsigned int) dragOperationForDraggingInfo: (id <NSDraggingInfo>)dragInfo
|
|
type: (NSString *)type
|
|
{
|
|
//FIXME
|
|
return NSDragOperationCopy | NSDragOperationGeneric;
|
|
}
|
|
|
|
- (NSImage *) dragImageForSelectionWithEvent: (NSEvent *)event
|
|
origin: (NSPoint *)origin
|
|
{
|
|
if (origin)
|
|
*origin = NSMakePoint(0, 0);
|
|
|
|
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];
|
|
|
|
[self writeSelectionToPasteboard: pboard types: types];
|
|
|
|
[self dragImage: image at: location offset: mouseOffset event: event
|
|
pasteboard: pboard source: self slideBack: slideBack];
|
|
|
|
return YES;
|
|
}
|
|
|
|
// This are all the NSTextInput methods that are not implemented on NSTextView
|
|
|
|
- (NSAttributedString *) attributedSubstringFromRange: (NSRange)theRange
|
|
{
|
|
return [_textStorage attributedSubstringFromRange: theRange];
|
|
}
|
|
|
|
- (unsigned) characterIndexForPoint: (NSPoint)point
|
|
{
|
|
unsigned index;
|
|
float fraction;
|
|
|
|
index = [_layoutManager glyphIndexForPoint: point
|
|
inTextContainer: _textContainer
|
|
fractionOfDistanceThroughGlyph: &fraction];
|
|
|
|
index = [_layoutManager characterIndexForGlyphAtIndex: index];
|
|
if (fraction > 0.5)
|
|
{
|
|
index++;
|
|
}
|
|
return index;
|
|
}
|
|
|
|
- (void) setMarkedText:(NSString *)aString selectedRange:(NSRange)selRange
|
|
{
|
|
}
|
|
|
|
- (BOOL) hasMarkedText
|
|
{
|
|
return NO;
|
|
}
|
|
|
|
- (void) unmarkText
|
|
{
|
|
}
|
|
|
|
- (NSArray*) validAttributesForMarkedText
|
|
{
|
|
return nil;
|
|
}
|
|
|
|
- (long) conversationIdentifier
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
- (NSRect) firstRectForCharacterRange: (NSRange)theRange
|
|
{
|
|
unsigned rectCount;
|
|
NSRect *rects = [_layoutManager
|
|
rectArrayForCharacterRange: theRange
|
|
withinSelectedCharacterRange: NSMakeRange(NSNotFound, 0)
|
|
inTextContainer: _textContainer
|
|
rectCount: &rectCount];
|
|
|
|
if (rectCount)
|
|
return rects[0];
|
|
else
|
|
return NSZeroRect;
|
|
}
|
|
|
|
// This are here to keep compiler quite.
|
|
// Somehow gcc does not recognize when a super class already implements
|
|
// a method needed by a protocol.
|
|
- (void) doCommandBySelector: (SEL)aSelector
|
|
{
|
|
[super doCommandBySelector: aSelector];
|
|
}
|
|
|
|
@end
|
|
|
|
@implementation NSTextView (GNUstepExtensions)
|
|
|
|
- (void) replaceRange: (NSRange)aRange
|
|
withAttributedString: (NSAttributedString*)attrString
|
|
{
|
|
if (aRange.location == NSNotFound)
|
|
return;
|
|
|
|
if (![self shouldChangeTextInRange: aRange
|
|
replacementString: [attrString string]])
|
|
return;
|
|
|
|
[_textStorage beginEditing];
|
|
|
|
if (_tf.is_rich_text)
|
|
{
|
|
[_textStorage replaceCharactersInRange: aRange
|
|
withAttributedString: attrString];
|
|
}
|
|
else
|
|
{
|
|
[_textStorage replaceCharactersInRange: aRange
|
|
withString: [attrString string]];
|
|
}
|
|
|
|
[_textStorage endEditing];
|
|
[self didChangeText];
|
|
}
|
|
|
|
- (unsigned) textLength
|
|
{
|
|
return [_textStorage length];
|
|
}
|
|
|
|
@end
|
|
|
|
@implementation NSTextView (GNUstepPrivate)
|
|
|
|
- (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];
|
|
}
|
|
|
|
+ (NSDictionary*) defaultTypingAttributes
|
|
{
|
|
return [NSDictionary dictionaryWithObjectsAndKeys:
|
|
[NSParagraphStyle defaultParagraphStyle],
|
|
NSParagraphStyleAttributeName,
|
|
[NSFont userFontOfSize: 0], NSFontAttributeName,
|
|
[NSColor textColor], NSForegroundColorAttributeName,
|
|
nil];
|
|
}
|
|
|
|
- (void) setAttributes: (NSDictionary*)attributes range: (NSRange)aRange
|
|
{
|
|
NSString *type;
|
|
id val;
|
|
NSEnumerator *enumerator = [attributes keyEnumerator];
|
|
|
|
if (aRange.location == NSNotFound)
|
|
return;
|
|
if (![self shouldChangeTextInRange: aRange
|
|
replacementString: nil])
|
|
return;
|
|
|
|
[_textStorage beginEditing];
|
|
while ((type = [enumerator nextObject]) != nil)
|
|
{
|
|
val = [attributes objectForKey: type];
|
|
[_textStorage addAttribute: type value: val range: aRange];
|
|
}
|
|
[_textStorage endEditing];
|
|
[self didChangeText];
|
|
}
|
|
|
|
- (void) _illegalMovement: (int)textMovement
|
|
{
|
|
// This is similar to [self resignFirstResponder], with the
|
|
// difference that in the notification we need to put the
|
|
// NSTextMovement, which resignFirstResponder does not. Also, if we
|
|
// are ending editing, we are going to be removed, so it's useless
|
|
// to update any drawing.
|
|
NSNumber *number;
|
|
NSDictionary *uiDictionary;
|
|
|
|
if ((_tf.is_editable)
|
|
&& ([_delegate respondsToSelector:
|
|
@selector(textShouldEndEditing:)])
|
|
&& ([_delegate textShouldEndEditing: self] == NO))
|
|
return;
|
|
|
|
/* FIXME */
|
|
if (_insertionPointTimer != nil)
|
|
{
|
|
[_insertionPointTimer invalidate];
|
|
DESTROY (_insertionPointTimer);
|
|
}
|
|
|
|
number = [NSNumber numberWithInt: textMovement];
|
|
uiDictionary = [NSDictionary dictionaryWithObject: number
|
|
forKey: @"NSTextMovement"];
|
|
[nc postNotificationName: NSTextDidEndEditingNotification
|
|
object: self userInfo: uiDictionary];
|
|
return;
|
|
}
|
|
|
|
|
|
- (NSRect) rectForCharacterRange: (NSRange)aRange
|
|
{
|
|
NSRange glyphRange;
|
|
|
|
glyphRange = [_layoutManager glyphRangeForCharacterRange: aRange
|
|
actualCharacterRange: NULL];
|
|
|
|
return [_layoutManager boundingRectForGlyphRange: glyphRange
|
|
inTextContainer: _textContainer];
|
|
}
|
|
@end
|
|
|
|
|