mirror of
https://github.com/gnustep/libs-gui.git
synced 2025-04-22 17:52:42 +00:00
NSTextView continuous spell checking
git-svn-id: svn+ssh://svn.gna.org/svn/gnustep/libs/gui/trunk@32534 72102866-910b-0410-8b05-ffd578937521
This commit is contained in:
parent
b216539303
commit
b460081f8c
6 changed files with 353 additions and 7 deletions
|
@ -1,3 +1,11 @@
|
|||
2011-03-11 Eric Wasylishen <ewasylishen@gmail.com>
|
||||
|
||||
* Source/NSLayoutManager.m:
|
||||
* Source/externs.m:
|
||||
* Source/NSTextView.m:
|
||||
* Headers/AppKit/NSAttributedString.h:
|
||||
* Headers/AppKit/NSTextView.h: Implement continuous spell checking
|
||||
|
||||
2011-03-11 Fred Kiefer <FredKiefer@gmx.de>
|
||||
|
||||
* ColorPickers/GSStandardColorPicker.m
|
||||
|
|
|
@ -142,6 +142,9 @@ APPKIT_EXPORT NSString *NSWebResourceLoadDelegateDocumentOption;
|
|||
|
||||
APPKIT_EXPORT NSString *NSCharacterShapeAttributeName;
|
||||
APPKIT_EXPORT const unsigned NSUnderlineByWordMask;
|
||||
APPKIT_EXPORT NSString *NSSpellingStateAttributeName;
|
||||
APPKIT_EXPORT const unsigned NSSpellingStateSpellingFlag;
|
||||
APPKIT_EXPORT const unsigned NSSpellingStateGrammarFlag;
|
||||
|
||||
// readFrom... attributes
|
||||
|
||||
|
|
|
@ -111,6 +111,7 @@ therefore be stored in the NSLayoutManager to avoid problems.
|
|||
would be very awkward if they weren't. */
|
||||
unsigned allows_undo:1;
|
||||
unsigned smart_insert_delete:1;
|
||||
unsigned continuous_spell_checking:1;
|
||||
/* End of shared attributes. */
|
||||
|
||||
|
||||
|
@ -243,6 +244,10 @@ therefore be stored in the NSLayoutManager to avoid problems.
|
|||
NSParagraphStyle *_defaultParagraphStyle;
|
||||
NSDictionary *_linkTextAttributes;
|
||||
NSRange _markedRange;
|
||||
|
||||
// Text checking (spelling/grammar)
|
||||
NSTimer *_textCheckingTimer;
|
||||
NSRect _lastCheckedRect;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -107,6 +107,7 @@ first. Remaining cases, highest priority first:
|
|||
|
||||
#import <Foundation/NSEnumerator.h>
|
||||
#import <Foundation/NSException.h>
|
||||
#import <Foundation/NSValue.h>
|
||||
#import "AppKit/NSAttributedString.h"
|
||||
#import "AppKit/NSColor.h"
|
||||
#import "AppKit/NSImage.h"
|
||||
|
@ -122,6 +123,16 @@ first. Remaining cases, highest priority first:
|
|||
#import "GNUstepGUI/GSLayoutManager_internal.h"
|
||||
|
||||
|
||||
@interface NSLayoutManager (spelling)
|
||||
|
||||
-(void) _drawSpellingState: (NSInteger)spellingState
|
||||
forGylphRange: (NSRange)range
|
||||
lineFragmentRect: (NSRect)fragmentRect
|
||||
lineFragmentGlyphRange: (NSRange)fragmentGlyphRange
|
||||
containerOrigin: (NSPoint)containerOrigin;
|
||||
|
||||
@end
|
||||
|
||||
@interface NSLayoutManager (LayoutHelpers)
|
||||
-(void) _doLayoutToContainer: (int)cindex point: (NSPoint)p;
|
||||
@end
|
||||
|
@ -1718,6 +1729,41 @@ for (i = 0; i < gbuf_len; i++) printf(" %3i : %04x\n", i, gbuf[i]); */
|
|||
}
|
||||
i += underlinedCharacterRange.length;
|
||||
}
|
||||
|
||||
// Draw spelling state (i.e. red underline for misspelled words)
|
||||
|
||||
for (i=characterRange.location; i<NSMaxRange(characterRange); )
|
||||
{
|
||||
NSRange underlinedCharacterRange;
|
||||
id underlineValue = [self temporaryAttribute: NSSpellingStateAttributeName
|
||||
atCharacterIndex: i
|
||||
longestEffectiveRange: &underlinedCharacterRange
|
||||
inRange: characterRange];
|
||||
if (underlineValue != nil && [underlineValue integerValue] != 0)
|
||||
{
|
||||
const NSRange underlinedGylphRange = [self glyphRangeForCharacterRange: underlinedCharacterRange
|
||||
actualCharacterRange: NULL];
|
||||
|
||||
// we have a range of glpyhs that need underlining, which might span
|
||||
// multiple line fragments, so we need to iterate though the line fragments
|
||||
for (j=underlinedGylphRange.location; j<NSMaxRange(underlinedGylphRange); )
|
||||
{
|
||||
NSRange lineFragmentGlyphRange;
|
||||
const NSRect lineFragmentRect = [self lineFragmentRectForGlyphAtIndex: j
|
||||
effectiveRange: &lineFragmentGlyphRange];
|
||||
const NSRange rangeToUnderline = NSIntersectionRange(underlinedGylphRange, lineFragmentGlyphRange);
|
||||
|
||||
[self _drawSpellingState: [underlineValue integerValue]
|
||||
forGylphRange: rangeToUnderline
|
||||
lineFragmentRect: lineFragmentRect
|
||||
lineFragmentGlyphRange: lineFragmentGlyphRange
|
||||
containerOrigin: containerOrigin];
|
||||
|
||||
j = NSMaxRange(rangeToUnderline);
|
||||
}
|
||||
}
|
||||
i += underlinedCharacterRange.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1902,6 +1948,55 @@ static void GSDrawPatternLine(NSPoint start, NSPoint end, NSInteger pattern, CGF
|
|||
|
||||
@end
|
||||
|
||||
@implementation NSLayoutManager (spelling)
|
||||
|
||||
-(void) _drawSpellingState: (NSInteger)spellingState
|
||||
forGylphRange: (NSRange)range
|
||||
lineFragmentRect: (NSRect)fragmentRect
|
||||
lineFragmentGlyphRange: (NSRange)fragmentGlyphRange
|
||||
containerOrigin: (NSPoint)containerOrigin
|
||||
{
|
||||
NSBezierPath *path;
|
||||
const float pattern[2] = {2.5, 1.0};
|
||||
NSFont *largestFont = [self effectiveFontForGlyphAtIndex: range.location // NOTE: GS private method
|
||||
range: NULL];
|
||||
NSPoint start = [self locationForGlyphAtIndex: range.location];
|
||||
NSPoint end = [self locationForGlyphAtIndex: NSMaxRange(range) - 1]; //FIXME: check length > 0
|
||||
|
||||
if (spellingState == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// FIXME: calculate the underline position correctly, using the font on both the start and end glyph
|
||||
start.y += [largestFont pointSize] * 0.07;
|
||||
end.y += [largestFont pointSize] * 0.07;
|
||||
|
||||
end.x += [largestFont advancementForGlyph: [self glyphAtIndex: (NSMaxRange(range) - 1)]].width;
|
||||
|
||||
start = NSMakePoint(start.x + containerOrigin.x + fragmentRect.origin.x, start.y + containerOrigin.y + fragmentRect.origin.y);
|
||||
end = NSMakePoint(end.x + containerOrigin.x + fragmentRect.origin.x, end.y + containerOrigin.y + fragmentRect.origin.y);
|
||||
|
||||
path = [NSBezierPath bezierPath];
|
||||
[path setLineDash: pattern count: 2 phase: 0];
|
||||
[path setLineWidth: 1.5];
|
||||
[path moveToPoint: start];
|
||||
[path lineToPoint: end];
|
||||
|
||||
if ((spellingState & NSSpellingStateGrammarFlag) != 0)
|
||||
{
|
||||
[[NSColor greenColor] set];
|
||||
}
|
||||
else
|
||||
{
|
||||
[[NSColor redColor] set];
|
||||
}
|
||||
|
||||
[path stroke];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation NSLayoutManager
|
||||
|
||||
|
|
|
@ -148,6 +148,14 @@ Interface for a bunch of internal methods that need to be cleaned up.
|
|||
//
|
||||
- (void) copySelection;
|
||||
- (void) pasteSelection;
|
||||
|
||||
/*
|
||||
* Text checking
|
||||
*/
|
||||
- (void) _scheduleTextCheckingInVisibleRectIfNeeded;
|
||||
- (void) _textDidChange: (NSNotification*)notif;
|
||||
- (void) _textCheckingTimerFired: (NSTimer *)t;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
|
@ -187,6 +195,7 @@ Interface for a bunch of internal methods that need to be cleaned up.
|
|||
([tv isFieldEditor]?0x10:0) |
|
||||
([tv usesFontPanel]?0x20:0) |
|
||||
([tv isRulerVisible]?0x40:0) |
|
||||
([tv isContinuousSpellCheckingEnabled]?0x80:0) |
|
||||
([tv usesRuler]?0x100:0) |
|
||||
([tv smartInsertDeleteEnabled]?0x200:0) |
|
||||
([tv allowsUndo]?0x400:0) |
|
||||
|
@ -738,6 +747,10 @@ If a text view is added to an empty text network, it keeps its attributes.
|
|||
name: NSViewFrameDidChangeNotification
|
||||
object: self];
|
||||
|
||||
[notificationCenter addObserver: self
|
||||
selector: @selector(_textDidChange:)
|
||||
name: NSTextDidChangeNotification
|
||||
object: self];
|
||||
return self;
|
||||
}
|
||||
|
||||
|
@ -903,6 +916,7 @@ that makes decoding and encoding compatible with the old code.
|
|||
_tf.is_field_editor = ((0x10 & flags) > 0);
|
||||
_tf.uses_font_panel = ((0x20 & flags) > 0);
|
||||
_tf.is_ruler_visible = ((0x40 & flags) > 0);
|
||||
_tf.continuous_spell_checking = ((0x80 & flags) > 0);
|
||||
_tf.uses_ruler = ((0x100 & flags) > 0);
|
||||
_tf.smart_insert_delete = ((0x200 & flags) > 0);
|
||||
_tf.allows_undo = ((0x400 & flags) > 0);
|
||||
|
@ -1019,7 +1033,10 @@ that makes decoding and encoding compatible with the old code.
|
|||
selector: @selector(_updateState:)
|
||||
name: NSViewFrameDidChangeNotification
|
||||
object: self];
|
||||
|
||||
[notificationCenter addObserver: self
|
||||
selector: @selector(_textDidChange:)
|
||||
name: NSTextDidChangeNotification
|
||||
object: self];
|
||||
return self;
|
||||
}
|
||||
|
||||
|
@ -1051,6 +1068,11 @@ that makes decoding and encoding compatible with the old code.
|
|||
[notificationCenter removeObserver: self
|
||||
name: NSViewFrameDidChangeNotification
|
||||
object: self];
|
||||
[notificationCenter removeObserver: self
|
||||
name: NSTextDidChangeNotification
|
||||
object: self];
|
||||
[_textCheckingTimer invalidate];
|
||||
|
||||
[[NSRunLoop currentRunLoop] cancelPerformSelector: @selector(_updateState:)
|
||||
target: self
|
||||
argument: nil];
|
||||
|
@ -1471,18 +1493,35 @@ to make sure syncing is handled properly in all cases.
|
|||
|
||||
/* Continuous spell checking */
|
||||
|
||||
/* TODO */
|
||||
- (BOOL) isContinuousSpellCheckingEnabled
|
||||
{
|
||||
NSLog(@"Method %s is not implemented for class %s",
|
||||
__PRETTY_FUNCTION__, "NSTextView");
|
||||
return NO;
|
||||
return _tf.continuous_spell_checking;
|
||||
}
|
||||
|
||||
- (void) setContinuousSpellCheckingEnabled: (BOOL)flag
|
||||
{
|
||||
NSLog(@"Method %s is not implemented for class %s",
|
||||
__PRETTY_FUNCTION__, "NSTextView");
|
||||
NSTEXTVIEW_SYNC;
|
||||
|
||||
if (_tf.continuous_spell_checking && !flag)
|
||||
{
|
||||
_tf.continuous_spell_checking = 0;
|
||||
|
||||
const NSRange allRange = NSMakeRange(0, [[self string] length]);
|
||||
[_layoutManager removeTemporaryAttribute: @"NSTextChecked"
|
||||
forCharacterRange: allRange];
|
||||
[_layoutManager removeTemporaryAttribute: NSSpellingStateAttributeName
|
||||
forCharacterRange: allRange];
|
||||
_lastCheckedRect = NSZeroRect;
|
||||
|
||||
[_textCheckingTimer invalidate];
|
||||
_textCheckingTimer = nil;
|
||||
}
|
||||
else if (!_tf.continuous_spell_checking && flag)
|
||||
{
|
||||
_tf.continuous_spell_checking = 1;
|
||||
|
||||
[self _scheduleTextCheckingInVisibleRectIfNeeded];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -3813,6 +3852,8 @@ Figure out how the additional layout stuff is supposed to work.
|
|||
drawnRange = NSMakeRange(0, 0);
|
||||
}
|
||||
|
||||
[self _scheduleTextCheckingInVisibleRectIfNeeded];
|
||||
|
||||
/* FIXME: We should only draw inside of rect. This code is necessary
|
||||
* to remove markings of old glyphs. These would not be removed
|
||||
* by the following call to the layout manager because that only
|
||||
|
@ -5740,6 +5781,195 @@ or add guards
|
|||
type: NSStringPboardType];
|
||||
}
|
||||
|
||||
/**
|
||||
* Text checking
|
||||
*/
|
||||
|
||||
- (void) _checkTextInRange: (NSRange)aRange
|
||||
{
|
||||
NSRange longestRange;
|
||||
id value = [_layoutManager temporaryAttribute: @"NSTextChecked"
|
||||
atCharacterIndex: aRange.location
|
||||
longestEffectiveRange: &longestRange
|
||||
inRange: aRange];
|
||||
longestRange = NSIntersectionRange(longestRange, aRange);
|
||||
|
||||
if ([value boolValue] && NSEqualRanges(longestRange, aRange))
|
||||
{
|
||||
//NSLog(@"No need to check in range %@", NSStringFromRange(aRange));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check all of aRange
|
||||
|
||||
[_layoutManager removeTemporaryAttribute: NSSpellingStateAttributeName
|
||||
forCharacterRange: aRange];
|
||||
|
||||
{
|
||||
NSSpellChecker *sp = [NSSpellChecker sharedSpellChecker];
|
||||
NSInteger start = 0;
|
||||
int count;
|
||||
// FIXME: doing the spellcheck on this substring could create false-positives
|
||||
// if a word is split in half.. so the range should be expanded to the
|
||||
// nearest word boundary
|
||||
NSString *substring = [[self string] substringWithRange: aRange];
|
||||
|
||||
do {
|
||||
NSRange errorRange = [sp checkSpellingOfString: substring
|
||||
startingAt: start
|
||||
language: [sp language]
|
||||
wrap: YES
|
||||
inSpellDocumentWithTag: [self spellCheckerDocumentTag]
|
||||
wordCount: &count];
|
||||
|
||||
if (errorRange.location < start)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (errorRange.length > 0)
|
||||
{
|
||||
start = NSMaxRange(errorRange);
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
errorRange = NSMakeRange(aRange.location + errorRange.location, errorRange.length);
|
||||
|
||||
//NSLog(@"highlighting mistake: %@", [[self string] substringWithRange: errorRange]);
|
||||
|
||||
[_layoutManager addTemporaryAttribute: NSSpellingStateAttributeName
|
||||
value: [NSNumber numberWithInteger: NSSpellingStateSpellingFlag]
|
||||
forCharacterRange: errorRange];
|
||||
|
||||
} while (1);
|
||||
|
||||
[_layoutManager addTemporaryAttribute: @"NSTextChecked"
|
||||
value: [NSNumber numberWithBool: YES]
|
||||
forCharacterRange: aRange];
|
||||
}
|
||||
}
|
||||
|
||||
- (void) _textCheckingTimerFired: (NSTimer *)t
|
||||
{
|
||||
_textCheckingTimer = nil;
|
||||
|
||||
if (nil == _layoutManager)
|
||||
return;
|
||||
|
||||
{
|
||||
const NSRect visibleRect = [self visibleRect];
|
||||
|
||||
NSRange visibleGlyphRange = [_layoutManager glyphRangeForBoundingRect: visibleRect
|
||||
inTextContainer: _textContainer];
|
||||
|
||||
NSRange visibleRange = [_layoutManager characterRangeForGlyphRange: visibleGlyphRange
|
||||
actualGlyphRange: NULL];
|
||||
|
||||
[self _checkTextInRange: visibleRange];
|
||||
|
||||
_lastCheckedRect = visibleRect;
|
||||
}
|
||||
}
|
||||
|
||||
- (void) _scheduleTextCheckingTimer
|
||||
{
|
||||
[_textCheckingTimer invalidate];
|
||||
_textCheckingTimer = [NSTimer scheduledTimerWithTimeInterval: 0.5
|
||||
target: self
|
||||
selector: @selector(_textCheckingTimerFired:)
|
||||
userInfo: [NSValue valueWithRect: [self visibleRect]]
|
||||
repeats: NO];
|
||||
|
||||
}
|
||||
|
||||
- (void) _scheduleTextCheckingInVisibleRectIfNeeded
|
||||
{
|
||||
if (_tf.continuous_spell_checking)
|
||||
{
|
||||
const NSRect visibleRect = [self visibleRect];
|
||||
if (!NSEqualRects(visibleRect, _lastCheckedRect))
|
||||
{
|
||||
if (NSEqualRects(visibleRect, [[_textCheckingTimer userInfo] rectValue]))
|
||||
{
|
||||
return;
|
||||
}
|
||||
[self _scheduleTextCheckingTimer];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If selected has a nonzero length, return it unmodified.
|
||||
* If selected is touching a word, expand it to cover the entire word
|
||||
* Otherwise return selected unmodified
|
||||
*/
|
||||
- (NSRange) _rangeToInvalidateSpellingForSelectionRange: (NSRange)selected
|
||||
{
|
||||
if (selected.length > 0)
|
||||
{
|
||||
return selected;
|
||||
}
|
||||
else
|
||||
{
|
||||
NSCharacterSet *boundary = [[NSCharacterSet letterCharacterSet] invertedSet];
|
||||
|
||||
if (selected.location == [[self string] length] && selected.location > 0)
|
||||
{
|
||||
selected.location--;
|
||||
}
|
||||
|
||||
NSRange prevNonCharacter = [[self string] rangeOfCharacterFromSet: boundary options: NSBackwardsSearch range: NSMakeRange(0, selected.location)];
|
||||
NSRange nextNonCharacter = [[self string] rangeOfCharacterFromSet: boundary options: 0 range: NSMakeRange(NSMaxRange(selected), [[self string] length] - NSMaxRange(selected))];
|
||||
NSRange range;
|
||||
|
||||
if (prevNonCharacter.length == 0)
|
||||
{
|
||||
range.location = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
range.location = prevNonCharacter.location + 1;
|
||||
}
|
||||
|
||||
if (nextNonCharacter.length == 0)
|
||||
{
|
||||
range.length = [[self string] length] - range.location;
|
||||
}
|
||||
else
|
||||
{
|
||||
range.length = nextNonCharacter.location - range.location;
|
||||
}
|
||||
|
||||
return range;
|
||||
}
|
||||
}
|
||||
|
||||
- (void) _textDidChange: (NSNotification*)notif
|
||||
{
|
||||
if (_tf.continuous_spell_checking)
|
||||
{
|
||||
// FIXME: This uses the caret position to guess what change caused
|
||||
// the NSTextDidChangeNotification and mark that range of the text as requiring re-checking.
|
||||
//
|
||||
// It would be better to use accurate information to decide what range
|
||||
// need its spelling state invalidated.
|
||||
|
||||
NSRange range = [self _rangeToInvalidateSpellingForSelectionRange: [self selectedRange]];
|
||||
if (range.length > 0)
|
||||
{
|
||||
[_layoutManager removeTemporaryAttribute: @"NSTextChecked"
|
||||
forCharacterRange: range];
|
||||
[_layoutManager removeTemporaryAttribute: NSSpellingStateAttributeName
|
||||
forCharacterRange: range];
|
||||
}
|
||||
|
||||
[self _scheduleTextCheckingTimer];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation NSTextViewUndoObject
|
||||
|
|
|
@ -547,6 +547,11 @@ NSString *NSModificationTimeDocumentAttribute = @"ModificationTime";
|
|||
|
||||
const unsigned NSUnderlineByWordMask = 0x01;
|
||||
|
||||
NSString *NSSpellingStateAttributeName = @"NSSpellingState";
|
||||
const unsigned NSSpellingStateSpellingFlag = 1;
|
||||
const unsigned NSSpellingStateGrammarFlag = 2;
|
||||
|
||||
|
||||
NSString *NSPlainTextDocumentType = @"PlainText";
|
||||
NSString *NSRTFTextDocumentType = @"RTF";
|
||||
NSString *NSRTFDTextDocumentType = @"RTFD";
|
||||
|
|
Loading…
Reference in a new issue