mirror of
https://github.com/gnustep/libs-gui.git
synced 2025-04-25 06:30:55 +00:00
git-svn-id: svn+ssh://svn.gna.org/svn/gnustep/libs/gui/trunk@22019 72102866-910b-0410-8b05-ffd578937521
1190 lines
28 KiB
Objective-C
1190 lines
28 KiB
Objective-C
/** <title>NSTextView</title>
|
|
|
|
<abstract>Categories which add user actions to NSTextView</abstract>
|
|
|
|
Copyright (C) 1996, 1998, 2000, 2001, 2002, 2003 Free Software Foundation, Inc.
|
|
|
|
Originally moved here from NSTextView.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
|
|
|
|
Author: Pierre-Yves Rivaille <pyrivail@ens-lyon.fr>
|
|
Date: September 2002
|
|
|
|
Extensive reworking: Alexander Malmberg <alexander@malmberg.org>
|
|
Date: December 2002 - February 2003
|
|
|
|
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,
|
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
*/
|
|
|
|
#include <Foundation/NSNotification.h>
|
|
#include <Foundation/NSValue.h>
|
|
#include "AppKit/NSGraphics.h"
|
|
#include "AppKit/NSLayoutManager.h"
|
|
#include "AppKit/NSScrollView.h"
|
|
#include "AppKit/NSTextStorage.h"
|
|
#include "AppKit/NSTextView.h"
|
|
|
|
/*
|
|
These methods are for user actions, ie. they are normally called from
|
|
-doCommandBySelector: (which is called by the input manager) in response
|
|
to some key press or other user event.
|
|
|
|
User actions that modify the text must check that a modification is allowed
|
|
and make sure all necessary notifications are sent. This is done by sending
|
|
-shouldChangeTextInRange:replacementString: before making any changes, and
|
|
(if the change is allowed) -didChangeText after the changes have been made.
|
|
|
|
All actions from NSResponder that make sense for a text view should be
|
|
implemented here, but this is _not_ the place to add new actions.
|
|
|
|
When changing attributes, the range returned by
|
|
rangeForUserCharacterAttributeChange or rangeForUserParagraphAttributeChange
|
|
should be used. If the location is NSNotFound, nothing should be done (in
|
|
particular, the typing attributes should _not_ be changed). Otherwise,
|
|
-shouldChangeTextInRange:replacementString: should be called, and if it
|
|
returns YES, the attributes of the range and the typing attributes should be
|
|
changed, and -didChangeText should be called.
|
|
|
|
In a non-rich-text text view, the typing attributes _must_always_ hold the
|
|
attributes of the text. Thus, the typing attributes must always be changed
|
|
in the same way that the attributes of the text are changed.
|
|
|
|
TODO: can the selected range's location be NSNotFound? when?
|
|
|
|
Not all user actions are here. Exceptions:
|
|
|
|
-copy:
|
|
-copyFont:
|
|
-copyRuler:
|
|
-paste:
|
|
-pasteFont:
|
|
-pasteRuler:
|
|
-pasteAsPlainText:
|
|
-pasteAsRichText:
|
|
|
|
-checkSpelling:
|
|
-showGuessPanel:
|
|
|
|
-selectAll: (implemented in NSText)
|
|
|
|
Not all methods that handle user-induced text modifications are here.
|
|
Exceptions:
|
|
(TODO)
|
|
|
|
-insertText:
|
|
-changeColor:
|
|
-changeFont: (action method?)
|
|
drag&drop handling methods
|
|
(others?)
|
|
|
|
All other methods that modify text are for programmatic changes and do not
|
|
send -shouldChangeTextInRange:replacementString: or -didChangeText.
|
|
|
|
*/
|
|
|
|
|
|
/** First some helpers **/
|
|
|
|
@interface NSTextView (user_action_helpers)
|
|
|
|
-(void) _illegalMovement: (int)textMovement;
|
|
|
|
-(void) _changeAttribute: (NSString *)name
|
|
inRange: (NSRange)r
|
|
using: (id (*)(id))func;
|
|
|
|
@end
|
|
|
|
|
|
@implementation NSTextView (user_action_helpers)
|
|
|
|
- (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. Please note that this ends up calling
|
|
resignFirstResponder anyway. */
|
|
NSNumber *number;
|
|
NSDictionary *uiDictionary;
|
|
|
|
if ((_tf.is_editable)
|
|
&& ([_delegate respondsToSelector:
|
|
@selector(textShouldEndEditing:)])
|
|
&& ([_delegate textShouldEndEditing: self] == NO))
|
|
return;
|
|
|
|
/* TODO: insertion point.
|
|
doesn't the -resignFirstResponder take care of that?
|
|
*/
|
|
|
|
number = [NSNumber numberWithInt: textMovement];
|
|
uiDictionary = [NSDictionary dictionaryWithObject: number
|
|
forKey: @"NSTextMovement"];
|
|
[[NSNotificationCenter defaultCenter]
|
|
postNotificationName: NSTextDidEndEditingNotification
|
|
object: self
|
|
userInfo: uiDictionary];
|
|
/* The TextField will get the notification, and drop our first responder
|
|
* status if it's the case ... in that case, our -resignFirstResponder will
|
|
* be called! */
|
|
return;
|
|
}
|
|
|
|
|
|
- (void) _changeAttribute: (NSString *)name
|
|
inRange: (NSRange)r
|
|
using: (id (*)(id))func
|
|
{
|
|
unsigned int i;
|
|
NSRange e, r2;
|
|
id current, new;
|
|
|
|
if (![self shouldChangeTextInRange: r replacementString: nil])
|
|
return;
|
|
|
|
[_textStorage beginEditing];
|
|
for (i = r.location; i < NSMaxRange(r);)
|
|
{
|
|
current = [_textStorage attribute: name
|
|
atIndex: i
|
|
effectiveRange: &e];
|
|
|
|
r2 = NSMakeRange(i, NSMaxRange(e) - i);
|
|
r2 = NSIntersectionRange(r2, r);
|
|
i = NSMaxRange(e);
|
|
|
|
new = func(current);
|
|
if (new != current)
|
|
{
|
|
if (!new)
|
|
{
|
|
[_textStorage removeAttribute: name
|
|
range: r2];
|
|
}
|
|
else
|
|
{
|
|
[_textStorage addAttribute: name
|
|
value: new
|
|
range: r2];
|
|
}
|
|
}
|
|
}
|
|
[_textStorage endEditing];
|
|
|
|
current = [_layoutManager->_typingAttributes objectForKey: name];
|
|
new = func(current);
|
|
if (new != current)
|
|
{
|
|
if (!new)
|
|
{
|
|
[_layoutManager->_typingAttributes removeObjectForKey: name];
|
|
}
|
|
else
|
|
{
|
|
[_layoutManager->_typingAttributes setObject: new forKey: name];
|
|
}
|
|
}
|
|
|
|
[self didChangeText];
|
|
}
|
|
|
|
@end
|
|
|
|
|
|
@implementation NSTextView (user_actions)
|
|
|
|
/* Helpers used with _changeAttribute:inRange:using:. */
|
|
static NSNumber *int_minus_one(NSNumber *cur)
|
|
{
|
|
int value;
|
|
|
|
if (cur)
|
|
value = [cur intValue] - 1;
|
|
else
|
|
value = -1;
|
|
|
|
if (value)
|
|
return [NSNumber numberWithInt: value];
|
|
else
|
|
return nil;
|
|
}
|
|
|
|
static NSNumber *int_plus_one(NSNumber *cur)
|
|
{
|
|
int value;
|
|
|
|
if (cur)
|
|
value = [cur intValue] + 1;
|
|
else
|
|
value = 1;
|
|
|
|
if (value)
|
|
return [NSNumber numberWithInt: value];
|
|
else
|
|
return nil;
|
|
}
|
|
|
|
static NSNumber *float_minus_one(NSNumber *cur)
|
|
{
|
|
float value;
|
|
|
|
if (cur)
|
|
value = [cur floatValue] - 1;
|
|
else
|
|
value = -1;
|
|
|
|
if (value)
|
|
return [NSNumber numberWithFloat: value];
|
|
else
|
|
return nil;
|
|
}
|
|
|
|
static NSNumber *float_plus_one(NSNumber *cur)
|
|
{
|
|
int value;
|
|
|
|
if (cur)
|
|
value = [cur floatValue] + 1;
|
|
else
|
|
value = 1;
|
|
|
|
if (value)
|
|
return [NSNumber numberWithFloat: value];
|
|
else
|
|
return nil;
|
|
}
|
|
|
|
|
|
- (void) subscript: (id)sender
|
|
{
|
|
NSRange r = [self rangeForUserCharacterAttributeChange];
|
|
|
|
if (r.location == NSNotFound)
|
|
return;
|
|
|
|
[self _changeAttribute: NSSuperscriptAttributeName
|
|
inRange: r
|
|
using: int_minus_one];
|
|
}
|
|
|
|
- (void) superscript: (id)sender
|
|
{
|
|
NSRange r = [self rangeForUserCharacterAttributeChange];
|
|
|
|
if (r.location == NSNotFound)
|
|
return;
|
|
|
|
[self _changeAttribute: NSSuperscriptAttributeName
|
|
inRange: r
|
|
using: int_plus_one];
|
|
}
|
|
|
|
- (void) lowerBaseline: (id)sender
|
|
{
|
|
NSRange r = [self rangeForUserCharacterAttributeChange];
|
|
|
|
if (r.location == NSNotFound)
|
|
return;
|
|
|
|
[self _changeAttribute: NSBaselineOffsetAttributeName
|
|
inRange: r
|
|
using: float_plus_one];
|
|
}
|
|
|
|
- (void) raiseBaseline: (id)sender
|
|
{
|
|
NSRange r = [self rangeForUserCharacterAttributeChange];
|
|
|
|
if (r.location == NSNotFound)
|
|
return;
|
|
|
|
[self _changeAttribute: NSBaselineOffsetAttributeName
|
|
inRange: r
|
|
using: float_minus_one];
|
|
}
|
|
|
|
- (void) unscript: (id)sender
|
|
{
|
|
NSRange aRange = [self rangeForUserCharacterAttributeChange];
|
|
|
|
if (aRange.location == NSNotFound)
|
|
return;
|
|
|
|
if (![self shouldChangeTextInRange: aRange
|
|
replacementString: nil])
|
|
return;
|
|
|
|
if (aRange.length)
|
|
{
|
|
[_textStorage beginEditing];
|
|
[_textStorage removeAttribute: NSSuperscriptAttributeName
|
|
range: aRange];
|
|
[_textStorage removeAttribute: NSBaselineOffsetAttributeName
|
|
range: aRange];
|
|
[_textStorage endEditing];
|
|
}
|
|
|
|
[_layoutManager->_typingAttributes removeObjectForKey: NSSuperscriptAttributeName];
|
|
[_layoutManager->_typingAttributes removeObjectForKey: NSBaselineOffsetAttributeName];
|
|
[self didChangeText];
|
|
}
|
|
|
|
|
|
- (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];
|
|
}
|
|
|
|
[_layoutManager->_typingAttributes
|
|
setObject: [NSNumber numberWithInt: doUnderline]
|
|
forKey: NSUnderlineStyleAttributeName];
|
|
}
|
|
|
|
|
|
- (void) useStandardKerning: (id)sender
|
|
{
|
|
NSRange aRange = [self rangeForUserCharacterAttributeChange];
|
|
|
|
if (aRange.location == NSNotFound)
|
|
return;
|
|
if (![self shouldChangeTextInRange: aRange
|
|
replacementString: nil])
|
|
return;
|
|
|
|
[_textStorage removeAttribute: NSKernAttributeName
|
|
range: aRange];
|
|
[_layoutManager->_typingAttributes removeObjectForKey: NSKernAttributeName];
|
|
[self didChangeText];
|
|
}
|
|
|
|
- (void) turnOffKerning: (id)sender
|
|
{
|
|
NSRange aRange = [self rangeForUserCharacterAttributeChange];
|
|
|
|
if (aRange.location == NSNotFound)
|
|
return;
|
|
if (![self shouldChangeTextInRange: aRange
|
|
replacementString: nil])
|
|
return;
|
|
|
|
[_textStorage addAttribute: NSKernAttributeName
|
|
value: [NSNumber numberWithFloat: 0.0]
|
|
range: aRange];
|
|
[_layoutManager->_typingAttributes setObject: [NSNumber numberWithFloat: 0.0]
|
|
forKey: NSKernAttributeName];
|
|
[self didChangeText];
|
|
}
|
|
|
|
- (void) loosenKerning: (id)sender
|
|
{
|
|
NSRange r = [self rangeForUserCharacterAttributeChange];
|
|
|
|
if (r.location == NSNotFound)
|
|
return;
|
|
|
|
[self _changeAttribute: NSKernAttributeName
|
|
inRange: r
|
|
using: float_plus_one];
|
|
}
|
|
|
|
- (void) tightenKerning: (id)sender
|
|
{
|
|
NSRange r = [self rangeForUserCharacterAttributeChange];
|
|
|
|
if (r.location == NSNotFound)
|
|
return;
|
|
|
|
[self _changeAttribute: NSKernAttributeName
|
|
inRange: r
|
|
using: float_minus_one];
|
|
}
|
|
|
|
- (void) turnOffLigatures: (id)sender
|
|
{
|
|
NSRange aRange = [self rangeForUserCharacterAttributeChange];
|
|
|
|
if (aRange.location == NSNotFound)
|
|
return;
|
|
|
|
if (![self shouldChangeTextInRange: aRange
|
|
replacementString: nil])
|
|
return;
|
|
[_textStorage addAttribute: NSLigatureAttributeName
|
|
value: [NSNumber numberWithInt: 0]
|
|
range: aRange];
|
|
[_layoutManager->_typingAttributes setObject: [NSNumber numberWithInt: 0]
|
|
forKey: NSLigatureAttributeName];
|
|
[self didChangeText];
|
|
}
|
|
|
|
- (void) useStandardLigatures: (id)sender
|
|
{
|
|
NSRange aRange = [self rangeForUserCharacterAttributeChange];
|
|
|
|
if (aRange.location == NSNotFound)
|
|
return;
|
|
|
|
if (![self shouldChangeTextInRange: aRange
|
|
replacementString: nil])
|
|
return;
|
|
|
|
[_textStorage removeAttribute: NSLigatureAttributeName
|
|
range: aRange];
|
|
[_layoutManager->_typingAttributes removeObjectForKey: NSLigatureAttributeName];
|
|
[self didChangeText];
|
|
}
|
|
|
|
- (void) useAllLigatures: (id)sender
|
|
{
|
|
NSRange aRange = [self rangeForUserCharacterAttributeChange];
|
|
|
|
if (aRange.location == NSNotFound)
|
|
return;
|
|
|
|
if (![self shouldChangeTextInRange: aRange
|
|
replacementString: nil])
|
|
return;
|
|
[_textStorage addAttribute: NSLigatureAttributeName
|
|
value: [NSNumber numberWithInt: 2]
|
|
range: aRange];
|
|
[_layoutManager->_typingAttributes setObject: [NSNumber numberWithInt: 2]
|
|
forKey: NSLigatureAttributeName];
|
|
[self didChangeText];
|
|
}
|
|
|
|
- (void) toggleTraditionalCharacterShape: (id)sender
|
|
{
|
|
// TODO
|
|
NSLog(@"Method %s is not implemented for class %s",
|
|
"toggleTraditionalCharacterShape:", "NSTextView");
|
|
}
|
|
|
|
|
|
- (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;
|
|
}
|
|
|
|
/* TODO */
|
|
//[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];
|
|
}
|
|
|
|
- (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];
|
|
}
|
|
|
|
|
|
/*
|
|
TODO: find out what affinity is supposed to mean
|
|
|
|
My current assumption:
|
|
|
|
Affinity deals with which direction we are selecting in, ie. which end of
|
|
the selected range is the moving end, and which is the anchor.
|
|
|
|
NSSelectionAffinityUpstream means that the minimum index of the selected
|
|
range is moving (ie. _selected_range.location).
|
|
|
|
NSSelectionAffinityDownstream means that the maximum index of the selected
|
|
range is moving (ie. _selected_range.location+_selected_range.length).
|
|
|
|
Thus, when moving and selecting, we use the affinity to find out which end
|
|
of the selected range to move, and after moving, we compare the character
|
|
index we moved to with the anchor and set the range and affinity.
|
|
|
|
|
|
The affinity is important when making keyboard selection have sensible
|
|
behavior. Example:
|
|
|
|
If, in the string "abcd", the insertion point is between the "c" and the "d"
|
|
(selected range is (3,0)), and the user hits shift-left twice, we select
|
|
the "c" and "b" (1,2) and set the affinity to NSSelectionAffinityUpstream.
|
|
If the user hits shift-right, only the "c" will be selected (2,1).
|
|
|
|
If the insertion point is between the "a" and the "b" (1,0) and the user hits
|
|
shift-right twice, we again select the "b" and "c" (1,2), but the affinity
|
|
is NSSelectionAffinityDownstream. If the user hits shift-right, the "d" is
|
|
added to the selection (1,3).
|
|
|
|
*/
|
|
|
|
- (unsigned int) _movementOrigin
|
|
{
|
|
NSRange range = [self selectedRange];
|
|
|
|
if ([self selectionAffinity] == NSSelectionAffinityUpstream)
|
|
return range.location;
|
|
else
|
|
return NSMaxRange(range);
|
|
}
|
|
|
|
- (unsigned int) _movementEnd
|
|
{
|
|
NSRange range = [self selectedRange];
|
|
|
|
if ([self selectionAffinity] == NSSelectionAffinityDownstream)
|
|
return range.location;
|
|
else
|
|
return NSMaxRange(range);
|
|
}
|
|
|
|
- (void) _moveTo: (unsigned int)cindex
|
|
select: (BOOL)select
|
|
{
|
|
if (select)
|
|
{
|
|
unsigned int anchor = [self _movementEnd];
|
|
|
|
if (anchor < cindex)
|
|
{
|
|
[self setSelectedRange: NSMakeRange(anchor, cindex - anchor)
|
|
affinity: NSSelectionAffinityDownstream
|
|
stillSelecting: NO];
|
|
[self scrollRangeToVisible: NSMakeRange(anchor, cindex - anchor)];
|
|
}
|
|
else
|
|
{
|
|
[self setSelectedRange: NSMakeRange(cindex, anchor - cindex)
|
|
affinity: NSSelectionAffinityUpstream
|
|
stillSelecting: NO];
|
|
[self scrollRangeToVisible: NSMakeRange(cindex, anchor - cindex)];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
[self setSelectedRange: NSMakeRange(cindex, 0)];
|
|
[self scrollRangeToVisible: NSMakeRange(cindex, 0)];
|
|
}
|
|
}
|
|
|
|
- (void) _move: (GSInsertionPointMovementDirection)direction
|
|
distance: (float)distance
|
|
select: (BOOL)select
|
|
{
|
|
unsigned int cindex;
|
|
int new_direction;
|
|
|
|
if (direction == GSInsertionPointMoveUp ||
|
|
direction == GSInsertionPointMoveDown)
|
|
{
|
|
new_direction = 2;
|
|
}
|
|
else if (direction == GSInsertionPointMoveLeft ||
|
|
direction == GSInsertionPointMoveRight)
|
|
{
|
|
new_direction = 1;
|
|
}
|
|
else
|
|
{
|
|
new_direction = 0;
|
|
}
|
|
|
|
cindex = [self _movementOrigin];
|
|
if (new_direction != _currentInsertionPointMovementDirection ||
|
|
!new_direction)
|
|
{
|
|
_originalInsertionPointCharacterIndex = cindex;
|
|
}
|
|
|
|
cindex = [_layoutManager characterIndexMoving: direction
|
|
fromCharacterIndex: cindex
|
|
originalCharacterIndex: _originalInsertionPointCharacterIndex
|
|
distance: distance];
|
|
[self _moveTo: cindex
|
|
select: select];
|
|
/* Setting the selected range will clear out the current direction, but
|
|
not the index. Thus, we always set the direction here. */
|
|
_currentInsertionPointMovementDirection = new_direction;
|
|
}
|
|
|
|
|
|
/*
|
|
Insertion point movement actions.
|
|
|
|
TODO: some of these used to do nothing if self is a field editor. should
|
|
check if there was a reason for that.
|
|
*/
|
|
|
|
- (void) moveUp: (id)sender
|
|
{
|
|
[self _move: GSInsertionPointMoveUp
|
|
distance: 0.0
|
|
select: NO];
|
|
}
|
|
|
|
- (void) moveUpAndModifySelection: (id)sender
|
|
{
|
|
[self _move: GSInsertionPointMoveUp
|
|
distance: 0.0
|
|
select: YES];
|
|
}
|
|
|
|
- (void) moveDown: (id)sender
|
|
{
|
|
[self _move: GSInsertionPointMoveDown
|
|
distance: 0.0
|
|
select: NO];
|
|
}
|
|
|
|
- (void) moveDownAndModifySelection: (id)sender
|
|
{
|
|
[self _move: GSInsertionPointMoveDown
|
|
distance: 0.0
|
|
select: YES];
|
|
}
|
|
|
|
- (void) moveLeft: (id)sender
|
|
{
|
|
[self _move: GSInsertionPointMoveLeft
|
|
distance: 0.0
|
|
select: NO];
|
|
}
|
|
|
|
- (void) moveRight: (id)sender
|
|
{
|
|
[self _move: GSInsertionPointMoveRight
|
|
distance: 0.0
|
|
select: NO];
|
|
}
|
|
|
|
|
|
- (void) moveBackward: (id)sender
|
|
{
|
|
unsigned int to = [self _movementOrigin];
|
|
|
|
if (to == 0)
|
|
return;
|
|
to--;
|
|
[self _moveTo: to
|
|
select: NO];
|
|
}
|
|
|
|
- (void) moveBackwardAndModifySelection: (id)sender
|
|
{
|
|
unsigned int to = [self _movementOrigin];
|
|
|
|
if (to == 0)
|
|
return;
|
|
to--;
|
|
[self _moveTo: to
|
|
select: YES];
|
|
}
|
|
|
|
- (void) moveForward: (id)sender
|
|
{
|
|
unsigned int to = [self _movementOrigin];
|
|
|
|
if (to == [_textStorage length])
|
|
return;
|
|
to++;
|
|
[self _moveTo: to
|
|
select: NO];
|
|
}
|
|
- (void) moveForwardAndModifySelection: (id)sender
|
|
{
|
|
unsigned int to = [self _movementOrigin];
|
|
|
|
if (to == [_textStorage length])
|
|
return;
|
|
to++;
|
|
[self _moveTo: to
|
|
select: YES];
|
|
}
|
|
|
|
- (void) moveWordBackward: (id)sender
|
|
{
|
|
unsigned int newLocation;
|
|
|
|
newLocation = [_textStorage nextWordFromIndex: [self _movementOrigin]
|
|
forward: NO];
|
|
[self _moveTo: newLocation
|
|
select: NO];
|
|
}
|
|
|
|
- (void) moveWordBackwardAndModifySelection: (id)sender
|
|
{
|
|
unsigned int newLocation;
|
|
|
|
newLocation = [_textStorage nextWordFromIndex: [self _movementOrigin]
|
|
forward: NO];
|
|
[self _moveTo: newLocation
|
|
select: YES];
|
|
}
|
|
|
|
- (void) moveWordForward: (id)sender
|
|
{
|
|
unsigned newLocation;
|
|
|
|
newLocation = [_textStorage nextWordFromIndex: [self _movementOrigin]
|
|
forward: YES];
|
|
[self _moveTo: newLocation
|
|
select: NO];
|
|
}
|
|
|
|
- (void) moveWordForwardAndModifySelection: (id)sender
|
|
{
|
|
unsigned newLocation;
|
|
|
|
newLocation = [_textStorage nextWordFromIndex: [self _movementOrigin]
|
|
forward: YES];
|
|
[self _moveTo: newLocation
|
|
select: YES];
|
|
}
|
|
|
|
- (void) moveToBeginningOfDocument: (id)sender
|
|
{
|
|
[self _moveTo: 0
|
|
select: NO];
|
|
}
|
|
|
|
- (void) moveToEndOfDocument: (id)sender
|
|
{
|
|
[self _moveTo: [_textStorage length]
|
|
select: NO];
|
|
}
|
|
|
|
- (void) moveToBeginningOfParagraph: (id)sender
|
|
{
|
|
NSRange aRange;
|
|
|
|
aRange = [[_textStorage string] lineRangeForRange:
|
|
NSMakeRange([self _movementOrigin], 0)];
|
|
[self _moveTo: aRange.location
|
|
select: NO];
|
|
}
|
|
|
|
- (void) moveToEndOfParagraph: (id)sender
|
|
{
|
|
NSRange aRange;
|
|
unsigned newLocation;
|
|
unsigned maxRange;
|
|
|
|
aRange = [[_textStorage string] lineRangeForRange:
|
|
NSMakeRange([self _movementOrigin], 0)];
|
|
maxRange = NSMaxRange (aRange);
|
|
|
|
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 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 ... unless
|
|
the last character is a newline, and we are trying to go to
|
|
the end of the line which is displayed as the
|
|
one-before-the-last. Please note (maxRange - 1) is a valid
|
|
char since the maxRange == 0 case has already been
|
|
eliminated. */
|
|
unichar u = [[_textStorage string] characterAtIndex: (maxRange - 1)];
|
|
if (u == '\n' || u == '\r')
|
|
{
|
|
newLocation = maxRange - 1;
|
|
}
|
|
else
|
|
{
|
|
newLocation = maxRange;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
/* Else, we want the insertion point to appear before the last
|
|
character in the paragraph range. Normally the last
|
|
character in the paragraph range is a newline. */
|
|
newLocation = maxRange - 1;
|
|
}
|
|
|
|
if (newLocation < aRange.location)
|
|
{
|
|
newLocation = aRange.location;
|
|
}
|
|
|
|
[self _moveTo: newLocation
|
|
select: NO];
|
|
}
|
|
|
|
|
|
/* TODO: this is only the beginning and end of lines if lines are horizontal
|
|
and layout is left-to-right */
|
|
- (void) moveToBeginningOfLine: (id)sender
|
|
{
|
|
[self _move: GSInsertionPointMoveLeft
|
|
distance: 1e8
|
|
select: NO];
|
|
}
|
|
- (void) moveToEndOfLine: (id)sender
|
|
{
|
|
[self _move: GSInsertionPointMoveRight
|
|
distance: 1e8
|
|
select: NO];
|
|
}
|
|
|
|
/**
|
|
* Tries to move the selection/insertion point down one page of the
|
|
* visible rect in the receiver while trying to maintain the
|
|
* horizontal position of the last vertical movement.
|
|
* If the receiver is a field editor, this method returns immediatly.
|
|
*/
|
|
- (void) pageDown: (id)sender
|
|
{
|
|
float scrollDelta;
|
|
float oldOriginY;
|
|
float newOriginY;
|
|
|
|
/*
|
|
* Scroll; also determine how far to move the insertion point.
|
|
*/
|
|
oldOriginY = NSMinY([self visibleRect]);
|
|
[[self enclosingScrollView] pageDown: sender];
|
|
newOriginY = NSMinY([self visibleRect]);
|
|
scrollDelta = newOriginY - oldOriginY;
|
|
|
|
if (scrollDelta == 0)
|
|
{
|
|
/* TODO/FIXME: If no scroll was done, it means we are in the
|
|
* last page of the document already - should we move the
|
|
* insertion point to the last line when the user clicks
|
|
* 'PageDown' in that case ?
|
|
*/
|
|
return;
|
|
}
|
|
|
|
[self _move: GSInsertionPointMoveDown
|
|
distance: scrollDelta
|
|
select: NO];
|
|
}
|
|
|
|
/**
|
|
* Tries to move the selection/insertion point up one page of the
|
|
* visible rect in the receiver while trying to maintain the
|
|
* horizontal position of the last vertical movement.
|
|
* If the receiver is a field editor, this method returns immediatly.
|
|
*/
|
|
- (void) pageUp: (id)sender
|
|
{
|
|
float scrollDelta;
|
|
float oldOriginY;
|
|
float newOriginY;
|
|
|
|
/*
|
|
* Scroll; also determine how far to move the insertion point.
|
|
*/
|
|
oldOriginY = NSMinY([self visibleRect]);
|
|
[[self enclosingScrollView] pageUp: sender];
|
|
newOriginY = NSMinY([self visibleRect]);
|
|
scrollDelta = newOriginY - oldOriginY;
|
|
|
|
if (scrollDelta == 0)
|
|
{
|
|
/* TODO/FIXME: If no scroll was done, it means we are in the
|
|
* first page of the document already - should we move the
|
|
* insertion point to the first line when the user clicks
|
|
* 'PageUp' in that case ?
|
|
*/
|
|
return;
|
|
}
|
|
|
|
|
|
[self _move: GSInsertionPointMoveUp
|
|
distance: -scrollDelta
|
|
select: NO];
|
|
}
|
|
|
|
- (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
|
|
{
|
|
unsigned int start, end, cindex;
|
|
|
|
cindex = [self _movementOrigin];
|
|
start = [_layoutManager characterIndexMoving: GSInsertionPointMoveLeft
|
|
fromCharacterIndex: cindex
|
|
originalCharacterIndex: cindex
|
|
distance: 1e8];
|
|
end = [_layoutManager characterIndexMoving: GSInsertionPointMoveRight
|
|
fromCharacterIndex: cindex
|
|
originalCharacterIndex: cindex
|
|
distance: 1e8];
|
|
[self setSelectedRange: NSMakeRange(start, end - start)];
|
|
}
|
|
|
|
|
|
/* 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'. */
|
|
/*
|
|
TODO: description incorrect. should swap characters on either side of the
|
|
insertion point. (see also: miswart)
|
|
*/
|
|
- (void) transpose: (id)sender
|
|
{
|
|
NSRange range = [self selectedRange];
|
|
NSString *string;
|
|
NSString *replacementString;
|
|
unichar chars[2];
|
|
|
|
/* Do nothing if we are at beginning of text. */
|
|
if (range.location < 2)
|
|
{
|
|
return;
|
|
}
|
|
|
|
range = NSMakeRange(range.location - 2, 2);
|
|
|
|
/* Get the two chars and swap them. */
|
|
string = [_textStorage string];
|
|
chars[1] = [string characterAtIndex: range.location];
|
|
chars[0] = [string characterAtIndex: (range.location + 1)];
|
|
|
|
/* Replace the original chars with the swapped ones. */
|
|
replacementString = [NSString stringWithCharacters: chars length: 2];
|
|
|
|
if ([self shouldChangeTextInRange: range
|
|
replacementString: replacementString])
|
|
{
|
|
[self replaceCharactersInRange: range
|
|
withString: replacementString];
|
|
[self didChangeText];
|
|
}
|
|
}
|
|
|
|
- (void) delete: (id)sender
|
|
{
|
|
[self deleteForward: sender];
|
|
}
|
|
|
|
|
|
/* Helper for -align*: */
|
|
- (void) _alignUser: (NSTextAlignment)alignment
|
|
{
|
|
NSRange r = [self rangeForUserParagraphAttributeChange];
|
|
|
|
if (r.location == NSNotFound)
|
|
return;
|
|
if (![self shouldChangeTextInRange: r
|
|
replacementString: nil])
|
|
return;
|
|
|
|
[self setAlignment: alignment
|
|
range: r];
|
|
[self didChangeText];
|
|
}
|
|
|
|
- (void) alignCenter: (id)sender
|
|
{
|
|
[self _alignUser: NSCenterTextAlignment];
|
|
}
|
|
|
|
- (void) alignLeft: (id)sender
|
|
{
|
|
[self _alignUser: NSLeftTextAlignment];
|
|
}
|
|
|
|
- (void) alignRight: (id)sender
|
|
{
|
|
[self _alignUser: NSRightTextAlignment];
|
|
}
|
|
|
|
- (void) alignJustified: (id)sender
|
|
{
|
|
[self _alignUser: NSJustifiedTextAlignment];
|
|
}
|
|
|
|
- (void) toggleContinuousSpellChecking: (id)sender
|
|
{
|
|
[self setContinuousSpellCheckingEnabled:
|
|
![self isContinuousSpellCheckingEnabled]];
|
|
}
|
|
|
|
- (void) toggleRuler: (id)sender
|
|
{
|
|
[self setRulerVisible: !_tf.is_ruler_visible];
|
|
}
|
|
|
|
@end
|