mirror of
https://github.com/gnustep/libs-gui.git
synced 2025-04-22 19:01:15 +00:00
514 lines
13 KiB
Objective-C
514 lines
13 KiB
Objective-C
/*
|
|
Copyright (C) 2013-2021 Free Software Foundation, Inc.
|
|
|
|
Author: German A. Arias <german@xelalug.org>
|
|
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 <http://www.gnu.org/licenses/> or write to the
|
|
Free Software Foundation, 51 Franklin Street, Fifth Floor,
|
|
Boston, MA 02110-1301, USA.
|
|
*/
|
|
|
|
#import <Foundation/NSAutoreleasePool.h>
|
|
#import <Foundation/NSRunLoop.h>
|
|
#import <Foundation/NSNotification.h>
|
|
#import "AppKit/NSApplication.h"
|
|
#import "AppKit/NSBox.h"
|
|
#import "AppKit/NSEvent.h"
|
|
#import "AppKit/NSScreen.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;
|
|
NSInterfaceStyle style;
|
|
|
|
style = NSInterfaceStyleForKey(@"NSScrollViewInterfaceStyle", nil);
|
|
|
|
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.
|
|
if (style == NSMacintoshInterfaceStyle
|
|
|| style == NSWindows95InterfaceStyle)
|
|
{
|
|
rect.origin.x = point.x - 4;
|
|
}
|
|
else
|
|
{
|
|
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) )
|
|
{
|
|
DESTROY(_originalWord);
|
|
DESTROY(_words);
|
|
}
|
|
}
|
|
|
|
// 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
|
|
- (NSInteger) numberOfRowsInTableView: (NSTableView *)aTableView
|
|
{
|
|
return [_words count];
|
|
}
|
|
|
|
- (id) tableView: (NSTableView *)aTableView
|
|
objectValueForTableColumn: (NSTableColumn *)aTableColumn
|
|
row: (NSInteger)rowIndex
|
|
{
|
|
return [[_words objectAtIndex: rowIndex] description];
|
|
}
|
|
@end
|
|
|