diff --git a/ChangeLog b/ChangeLog index 096408064..fb8a99a8f 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,13 @@ +2013-06-19 German A. Arias + + * Source/GSAutocompleteWindow.h: + * Source/GSAutocompleteWindow.m: Add. + * Source/GNUmakefile: Add new files. + * Source/NSTextView.m (-rangeForUserCompletion, -complete:, + -completionsForPartialWordRange:indexOfSelectedItem:, + -insertCompletion:forPartialWordRange:movement:isFinal:): + Implement methods for autocomplete. + 2013-06-02 German A. Arias * Headers/AppKit/NSText.h: Add NSCancelTextMovement and diff --git a/Source/GNUmakefile b/Source/GNUmakefile index be55fe39c..b6975a2ce 100644 --- a/Source/GNUmakefile +++ b/Source/GNUmakefile @@ -194,6 +194,7 @@ NSWindow.m \ NSWindowController.m \ NSWorkspace.m \ GSAnimator.m \ +GSAutocompleteWindow.m \ GSDisplayServer.m \ GSHelpManagerPanel.m \ GSInfoPanel.m \ @@ -419,6 +420,7 @@ GSVersion.h \ GMAppKit.h \ GMArchiver.h \ GSAnimator.h \ +GSAutocompleteWindow.h \ GSTheme.h \ GSFontInfo.h \ GSMemoryPanel.h \ diff --git a/Source/GSAutocompleteWindow.h b/Source/GSAutocompleteWindow.h new file mode 100644 index 000000000..6be0eb84e --- /dev/null +++ b/Source/GSAutocompleteWindow.h @@ -0,0 +1,72 @@ +/* + Copyright (C) 2013 Free Software Foundation, Inc. + + Author: German A. Arias + Date: 2013 + + This file is part of the GNUstep GUI Library. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; see the file COPYING.LIB. + If not, see or write to the + Free Software Foundation, 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#import + +@class GSAutocompleteView; +@class NSArray; +@class NSString; +@class NSNotification; +@class NSNotificationCenter; +@class NSTableColumn; +@class NSTableView; +@class NSTextView; + +@interface GSAutocompleteWindow : NSPanel +{ + BOOL _stopped; + NSRange _range; + NSTextView *_textView; + GSAutocompleteView *_tableView; + + //Retained + NSString *_originalWord; + NSArray *_words; +} + ++ (GSAutocompleteWindow *) defaultWindow; + +- (void) layout; +- (void) computePosition; +- (void) displayForTextView: (NSTextView *)textView; +- (NSArray *) words; + +- (void) runModalWindow; +- (void) runLoop; +- (void) onWindowEdited: (NSNotification *)notification; + +- (void) reloadData; +- (void) updateTextViewWithMovement: (NSInteger)movement isFinal: (BOOL)flag; + +- (void) clickItem: (id)sender; +- (void) moveUpSelection; +- (void) moveDownSelection; + +// Delegate +- (int) numberOfRowsInTableView: (NSTableView*)aTableView; +- (id) tableView: (NSTableView*)aTableView + objectValueForTableColumn: (NSTableColumn*)aTableColumn + row: (int)rowIndex; +@end diff --git a/Source/GSAutocompleteWindow.m b/Source/GSAutocompleteWindow.m new file mode 100644 index 000000000..16c132acd --- /dev/null +++ b/Source/GSAutocompleteWindow.m @@ -0,0 +1,502 @@ +/* + Copyright (C) 2013 Free Software Foundation, Inc. + + Author: German A. Arias + Date: 2013 + + This file is part of the GNUstep GUI Library. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; see the file COPYING.LIB. + If not, see or write to the + Free Software Foundation, 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#import +#import +#import +#import "AppKit/NSApplication.h" +#import "AppKit/NSBox.h" +#import "AppKit/NSEvent.h" +#import "AppKit/NSScrollView.h" +#import "AppKit/NSTableView.h" +#import "AppKit/NSTableColumn.h" +#import "AppKit/NSText.h" +#import "AppKit/NSTextView.h" +#import "GNUstepGUI/GSTheme.h" +#import "GSAutocompleteWindow.h" + +static GSAutocompleteWindow *gsWindow = nil; + +@interface NSTextView (Additions) +- (NSRect) rectForCharacterRange: (NSRange)aRange; +@end + +@interface GSAutocompleteView : NSTableView +{ +} +@end + +@implementation GSAutocompleteView +- (BOOL) acceptsFirstMouse: (NSEvent *)event +{ + return YES; +} +@end + +@implementation GSAutocompleteWindow + ++ (GSAutocompleteWindow *) defaultWindow +{ + if (gsWindow == nil) + gsWindow = [[self alloc] initWithContentRect: NSMakeRect(0,0,200,200) + styleMask: NSBorderlessWindowMask + backing: NSBackingStoreNonretained + defer: YES]; + + return gsWindow; +} + +- (id) initWithContentRect: (NSRect)contentRect + styleMask: (NSUInteger)aStyle + backing: (NSBackingStoreType)bufferingType + defer: (BOOL)flag +{ + NSBox *box; + NSScrollView *scrollView; + NSTableColumn *column; + NSCell *cell; + + self = [super initWithContentRect: contentRect + styleMask: aStyle + backing: bufferingType + defer: flag]; + if (nil == self) + return self; + + // Init vars + _words = nil; + _originalWord = nil; + + [self setLevel: NSPopUpMenuWindowLevel]; + [self setBecomesKeyOnlyIfNeeded: YES]; + + box = [[NSBox alloc] initWithFrame: contentRect]; + [box setAutoresizingMask: NSViewWidthSizable | NSViewHeightSizable]; + [box setBorderType: NSLineBorder]; + [box setTitlePosition: NSNoTitle]; + [box setContentViewMargins: NSMakeSize(0, 0)]; + [self setContentView: box]; + [box release]; + + _tableView = [[GSAutocompleteView alloc] + initWithFrame: NSMakeRect(0, 0, 100, 100)]; + [_tableView setAutoresizingMask: NSViewWidthSizable | NSViewHeightSizable]; + [_tableView setDrawsGrid: NO]; + [_tableView setAllowsEmptySelection: YES]; + [_tableView setAllowsMultipleSelection: NO]; + [_tableView setAutoresizesAllColumnsToFit: YES]; + [_tableView setHeaderView: nil]; + [_tableView setCornerView: nil]; + + column = [[NSTableColumn alloc] initWithIdentifier: @"content"]; + cell = [[NSCell alloc] initTextCell: @""]; + [column setDataCell: cell]; + [cell release]; + [_tableView addTableColumn: column]; + [column release]; + + [_tableView setDataSource: self]; + [_tableView setDelegate: self]; + [_tableView setAction: @selector(clickItem:)]; + [_tableView setTarget: self]; + + scrollView = [[NSScrollView alloc] initWithFrame: + NSMakeRect(contentRect.origin.x, + contentRect.origin.y, + contentRect.size.width, + contentRect.size.height)]; + [scrollView setHasVerticalScroller: YES]; + [scrollView setDocumentView: _tableView]; + [_tableView release]; + [box setContentView: scrollView]; + [scrollView release]; + + return self; +} + +- (void) dealloc +{ + /* Don't release _words and _originalWord, since these are + * released when the autocomplete is final or canceled. + */ + [super dealloc]; +} + +- (BOOL) canBecomeKeyWindow +{ + return YES; +} + +- (void) layout +{ + NSSize bsize = [[GSTheme theme] sizeForBorderType: NSLineBorder]; + CGFloat windowWidth, windowHeight; + NSInteger num = [_words count]; + NSUInteger widest = 0; + NSCell *cell; + NSString *word, *widestWord = nil; + NSEnumerator *enumerator; + + /* If the suggested words are more than 10, + * we limit the window to show 10. + */ + if (num > 10) + { + num = 10; + } + + /* Lookup the widest word to calculate the width + * of the window. + */ + enumerator = [_words objectEnumerator]; + while ((word = [enumerator nextObject])) + { + if ([word length] > widest) + { + widest = [word length]; + widestWord = word; + } + } + + // Width + cell = [[_tableView tableColumnWithIdentifier: @"content"] dataCell]; + windowWidth = 1.1*[cell _sizeText: widestWord].width + + [NSScroller scrollerWidth] + 2*bsize.width; + + //Height + windowHeight = 2*bsize.height + [_tableView rowHeight]*num + + [_tableView intercellSpacing].height; + + [self setFrame: NSMakeRect(0, 0, windowWidth, windowHeight) display: NO]; +} + +- (void) computePosition +{ + NSRect screenFrame; + NSRect rect; + NSRect stringRect; + NSPoint point; + + rect = [self frame]; + screenFrame = [[[_textView window] screen] frame]; + + // Get the rectangle to draw the current word. + stringRect = [_textView rectForCharacterRange: _range]; + + // Convert the origin point to screen coordinates. + point = [[_textView window] convertBaseToScreen: + [_textView convertRect: stringRect toView: nil].origin]; + + // Calculate the origin point to the window. + rect.origin.x = point.x - [NSScroller scrollerWidth] - 4; + rect.origin.y = point.y - rect.size.height; + + // If part of the window is off screen, change the origin point. + if (screenFrame.size.width < (rect.origin.x + rect.size.width)) + { + rect.origin.x = screenFrame.size.width - rect.size.width; + } + else if (rect.origin.x < 0) + { + rect.origin.x = 0; + } + + // If no space under the string, we display the window over this. + if (rect.origin.y < 0) + { + rect.origin.y = point.y + stringRect.size.height; + } + + [self setFrame: rect display: NO]; +} + +- (NSArray *) words +{ + return _words; +} + +- (void) displayForTextView: (NSTextView *)textView +{ + _textView = textView; + _range = [_textView rangeForUserCompletion]; + [self reloadData]; + + if ([_words count] > 0) + { + [self runModalWindow]; + } + else + { + [self close]; + } +} + +- (void) runModalWindow +{ + NSWindow *onWindow; + NSNotificationCenter *notificationCenter; + + onWindow = [_textView window]; + notificationCenter = [NSNotificationCenter defaultCenter]; + + // Get the appropriate notifications to cancel the autocomplete. + + [notificationCenter addObserver: self selector: @selector(onWindowEdited:) + name: NSWindowWillCloseNotification object: onWindow]; + [notificationCenter addObserver: self selector: @selector(onWindowEdited:) + name: NSWindowWillMiniaturizeNotification object: onWindow]; + // The notification below don't seems to work. + [notificationCenter addObserver: self selector: @selector(onWindowEdited:) + name: NSWindowWillMoveNotification object: onWindow]; + + // FIX ME: The notification below doesn't exist currently + // [nc addObserver: self selector: @selector(onWindowEdited:) + // name: NSWindowWillResizeNotification object: onWindow]; + + // FIXME: The code below must be removed when the notifications over will work + [notificationCenter addObserver: self selector: @selector(onWindowEdited:) + name: NSWindowDidMoveNotification object: onWindow]; + [notificationCenter addObserver: self selector: @selector(onWindowEdited:) + name: NSWindowDidResizeNotification object: onWindow]; + // End of the code to remove + + [self orderFront: self]; + [self makeFirstResponder: _tableView]; + + [self runLoop]; + + [notificationCenter removeObserver: self name: nil object: onWindow]; + [self close]; + [onWindow makeFirstResponder: _textView]; +} + +- (void) runLoop +{ + NSEvent *event; + NSDate *limit = [NSDate distantFuture]; + unichar key; + CREATE_AUTORELEASE_POOL (pool); + + _stopped = NO; + + while (YES) + { + event = [NSApp nextEventMatchingMask: NSAnyEventMask + untilDate: limit + inMode: NSDefaultRunLoopMode + dequeue: YES]; + + if ([event type] == NSLeftMouseDown + || [event type] == NSRightMouseDown) + { + if ([event window] != self) + { + [self updateTextViewWithMovement: NSCancelTextMovement + isFinal: NO]; + break; + } + else + { + [NSApp sendEvent: event]; + } + } + else if ([event type] == NSKeyDown) + { + key = [[event characters] characterAtIndex: 0]; + + if (key == NSUpArrowFunctionKey) + { + [self moveUpSelection]; + [self updateTextViewWithMovement: NSUpTextMovement + isFinal: NO]; + } + else if (key == NSDownArrowFunctionKey) + { + [self moveDownSelection]; + [self updateTextViewWithMovement: NSDownTextMovement + isFinal: NO]; + } + else if (key == NSEnterCharacter || + key == NSCarriageReturnCharacter || + key == NSNewlineCharacter) + { + [self clickItem: self]; + break; + } + else if (key == 0x001b || + key == NSRightArrowFunctionKey || + key == NSLeftArrowFunctionKey) + { + [self updateTextViewWithMovement: NSCancelTextMovement + isFinal: NO]; + break; + } + else + { + // First remove the selected text. + [_textView replaceCharactersInRange: [_textView selectedRange] + withString: @""]; + // Send the even to update the text container. + [NSApp sendEvent: event]; + // Reload data. + [self reloadData]; + } + } + else + { + [NSApp sendEvent: event]; + } + + if (_stopped) + break; + } + + [pool drain]; +} + +- (void) onWindowEdited: (NSNotification *)notification +{ + _stopped = YES; + [self updateTextViewWithMovement: NSCancelTextMovement + isFinal: NO]; +} + +- (void) reloadData +{ + _range = [_textView rangeForUserCompletion]; + + if (_range.location == NSNotFound || _range.length == 0) + { + _stopped = YES; + } + else + { + NSInteger index = 0; + NSString *word; + NSArray *newWords; + + word = [[_textView string] substringWithRange: _range]; + ASSIGN(_originalWord, word); + + newWords = [_textView completionsForPartialWordRange: _range + indexOfSelectedItem: &index]; + + if ([newWords count] > 0) + { + ASSIGN(_words, newWords); + [_tableView reloadData]; + [self layout]; + [self computePosition]; + [_tableView selectRow: index byExtendingSelection: NO]; + [_tableView scrollRowToVisible: index]; + [self updateTextViewWithMovement: NSOtherTextMovement + isFinal: NO]; + } + else + { + [_tableView reloadData]; + _stopped = YES; + [self updateTextViewWithMovement: NSCancelTextMovement + isFinal: NO]; + } + } +} + +- (void) updateTextViewWithMovement: (NSInteger)movement + isFinal: (BOOL)flag +{ + NSString *word; + + if (movement != NSCancelTextMovement) + { + NSInteger rowIndex = [_tableView selectedRow]; + word = [[_words objectAtIndex: rowIndex] description]; + } + else + { + word = _originalWord; + } + + [_textView insertCompletion: word + forPartialWordRange: _range + movement: movement + isFinal: flag]; + + // Release _words and _originalWords if + // autocomplete is final or canceled. + if ( (flag) || + (movement == NSCancelTextMovement) ) + { + ASSIGN(_originalWord, nil); + ASSIGN(_words, nil); + } +} + +// Action method +- (void) clickItem: (id)sender +{ + [self updateTextViewWithMovement: NSOtherTextMovement + isFinal: YES]; + + _stopped = YES; +} + +// Key actions methods +- (void) moveUpSelection +{ + NSInteger index = [_tableView selectedRow] - 1; + + if (index > -1 && index < [_tableView numberOfRows]) + { + [_tableView selectRow: index byExtendingSelection: NO]; + [_tableView scrollRowToVisible: index]; + } +} + +- (void) moveDownSelection +{ + NSInteger index = [_tableView selectedRow] + 1; + + if (index > -1 && index < [_tableView numberOfRows]) + { + [_tableView selectRow: index byExtendingSelection: NO]; + [_tableView scrollRowToVisible: index]; + } +} + +// Delegate +- (int) numberOfRowsInTableView: (NSTableView *)aTableView +{ + return [_words count]; +} + +- (id) tableView: (NSTableView *)aTableView + objectValueForTableColumn: (NSTableColumn *)aTableColumn + row: (int)rowIndex +{ + return [[_words objectAtIndex: rowIndex] description]; +} +@end + diff --git a/Source/NSTextView.m b/Source/NSTextView.m index 0095a4e55..a48d43acb 100644 --- a/Source/NSTextView.m +++ b/Source/NSTextView.m @@ -92,6 +92,7 @@ #import "GSTextFinder.h" #import "GSToolTips.h" #import "GSFastEnumeration.h" +#import "GSAutocompleteWindow.h" /* @@ -2857,8 +2858,49 @@ Returns the ranges to which various kinds of user changes should apply. - (NSRange) rangeForUserCompletion { - // FIXME - return NSMakeRange(NSNotFound, 0); + NSUInteger length, location; + NSRange range, space; + + // Get the current location. + location = [self selectedRange].location; + + // Find the first space starting from current location, backwards. + space = [[self string] rangeOfCharacterFromSet: + [NSCharacterSet whitespaceAndNewlineCharacterSet] + options: NSBackwardsSearch + range: NSMakeRange(0, location)]; + + if (space.location == NSNotFound) + { + // No space was found. + if (location > 0) + { + // Return the range of the whole substring. + range = NSMakeRange(0, location); + } + else + { + // There isn't word. + range = NSMakeRange(NSNotFound, 0); + } + } + else + { + length = location - space.location - 1; + + if (length > 0) + { + // Return the range of the last word. + range = NSMakeRange(space.location + 1, length); + } + else + { + // There isn't word at the end. + range = NSMakeRange(NSNotFound, 0); + } + } + + return range; } - (NSArray *) rangesForUserCharacterAttributeChange @@ -5965,13 +6007,28 @@ configuation! */ - (void) complete: (id)sender { - // FIXME + NSRange range = [self rangeForUserCompletion]; + + if ([self isEditable] && + range.location != NSNotFound && range.length != 0) + { + GSAutocompleteWindow *window = [GSAutocompleteWindow defaultWindow]; + [window displayForTextView: self]; + } } - (NSArray *) completionsForPartialWordRange: (NSRange)range indexOfSelectedItem: (NSInteger *)index { - // FIXME + if ([_delegate respondsToSelector: + @selector(textView:completions:forPartialWordRange:indexOfSelectedItem:)]) + { + return [_delegate textView: self + completions: [[GSAutocompleteWindow defaultWindow] words] + forPartialWordRange: range + indexOfSelectedItem: index]; + } + return nil; } @@ -5980,7 +6037,27 @@ configuation! */ movement: (NSInteger)movement isFinal: (BOOL)flag { - // FIXME + NSString *complete; + NSString *partial = [word substringToIndex: range.length]; + + if (![self shouldChangeTextInRange: range replacementString: partial]) + return; + + complete = [word stringByDeletingPrefix: partial]; + + [_textStorage beginEditing]; + [self replaceCharactersInRange: range withString: partial]; + [_textStorage endEditing]; + [self didChangeText]; + + [self insertText: complete]; + [self didChangeText]; + + if (!flag && ([self selectedRange].length == 0) ) + { + [self setSelectedRange: + NSMakeRange(NSMaxRange(range), [complete length])]; + } } - (void) orderFrontLinkPanel: (id)sender