1/*
2**  AutoCompletingTextField.m
3**
4**  Copyright (c) 2003 Ken Ferry
5**  Copyright (C) 2014-2015 GNUstep Team
6**
7**  Author: Ken Ferry <kenferry@mac.com>
8**
9**  This program is free software; you can redistribute it and/or modify
10**  it under the terms of the GNU General Public License as published by
11**  the Free Software Foundation; either version 2 of the License, or
12**  (at your option) any later version.
13**
14**  This program is distributed in the hope that it will be useful,
15**  but WITHOUT ANY WARRANTY; without even the implied warranty of
16**  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17**  GNU General Public License for more details.
18**
19** You should have received a copy of the GNU General Public License
20** along with this program.  If not, see <http://www.gnu.org/licenses/>.
21
22*/
23
24#import "AutoCompletingTextField.h"
25#import "Constants.h"
26
27static NSWindow *_sharedDropDown = nil;
28static NSScrollView *_sharedDropDownScrollView = nil;
29static NSTableView *_sharedDropDownTableView = nil;
30
31@interface AutoCompletingTextField (Private)
32- (void)_setupAutoCompletingTextField;
33- (NSRange)_defaultCurrentComponentRange;
34- (NSRange)_commaDelimitedCurrentComponentRange;
35@end
36
37
38@implementation AutoCompletingTextField
39
40+ (void)initialize
41{
42  NSTableColumn *aTableColumn;
43
44  aTableColumn = AUTORELEASE([[NSTableColumn alloc] init]);
45  [aTableColumn setResizable: YES];
46  [aTableColumn setDataCell: AUTORELEASE([[NSTextFieldCell alloc] init])];
47
48  _sharedDropDownTableView = AUTORELEASE([[NSTableView alloc] init]);
49  [_sharedDropDownTableView addTableColumn: aTableColumn];
50  [_sharedDropDownTableView setAutoresizingMask: NSViewWidthSizable|NSViewHeightSizable];
51  [_sharedDropDownTableView setHeaderView: nil];
52  [_sharedDropDownTableView setCornerView: nil];
53  [_sharedDropDownTableView setDrawsGrid: NO];
54  [_sharedDropDownTableView sizeLastColumnToFit];
55
56  _sharedDropDownScrollView = AUTORELEASE([[NSScrollView alloc] init]);
57  [_sharedDropDownScrollView setDocumentView: _sharedDropDownTableView];
58  [_sharedDropDownScrollView setHasVerticalScroller: YES];
59  [_sharedDropDownScrollView setBorderType: NSBezelBorder];
60  [_sharedDropDownScrollView setAutoresizingMask: NSViewWidthSizable|NSViewHeightSizable];
61
62  _sharedDropDown = [[NSWindow alloc] initWithContentRect: NSMakeRect(1000000,1000000,0,0)
63				      styleMask: NSBorderlessWindowMask
64				      backing: NSBackingStoreBuffered
65				      defer: YES];
66
67  [_sharedDropDown setContentView: _sharedDropDownScrollView];
68  [_sharedDropDown setHasShadow: YES];
69  [_sharedDropDown setAlphaValue: .88];
70  [_sharedDropDown useOptimizedDrawing: YES];
71}
72
73- (id)initWithFrame:(NSRect)frameRect
74{
75  if ((self = [super initWithFrame:frameRect]))
76    {
77      [self _setupAutoCompletingTextField];
78    }
79  return self;
80}
81
82- (id)initWithCoder:(NSCoder *)decoder
83{
84  if ((self = [super initWithCoder:decoder]))
85    {
86      [self _setupAutoCompletingTextField];
87    }
88  return self;
89}
90
91- (void)_setupAutoCompletingTextField
92{
93  [self setCompletionDelay: .2];
94  [self setMaximumDropDownRows: 10];
95  _justDeleted = NO;
96  _shouldShowDropDown = YES;
97}
98
99- (void)dealloc
100{
101  [_cachedCompletions release];
102  [self setDataSource:nil];
103  [[NSNotificationCenter defaultCenter] removeObserver:self]; // at least NSWindowWillMoveNotification
104  [super dealloc];
105}
106
107- (void)textDidBeginEditing:(NSNotification *)aNotification
108{
109  [super textDidBeginEditing:aNotification];
110  [_sharedDropDownTableView setDelegate:self];
111  [_sharedDropDownTableView setDataSource:self];
112  [self setDropDownIsDown:NO];
113}
114
115- (void)textDidEndEditing:(NSNotification *)aNotification
116{
117  [super textDidEndEditing:aNotification];
118  [_sharedDropDownTableView setDelegate:nil];
119  [_sharedDropDownTableView setDataSource:nil];
120  [_sharedDropDownTableView reloadData];
121  [self setDropDownIsDown:NO];
122}
123
124- (void)textDidChange:(NSNotification *)aNotification
125{
126  [super textDidChange:aNotification];
127
128  if (_justDeleted)
129    {
130      _justDeleted = NO;
131      _shouldShowDropDown = NO;
132    }
133  else
134    {
135      _shouldShowDropDown = YES;
136    }
137
138  [NSObject cancelPreviousPerformRequestsWithTarget:self
139	    selector :@selector(complete:)
140	    object :nil];
141  [self performSelector:@selector(complete:)
142	withObject :nil
143	afterDelay :_completionDelay];
144}
145
146- (void)complete:(id)sender
147{
148  id fieldEditor;
149  NSRange selectedRange;
150  BOOL shouldShowDropDown, shouldComplete;
151  NSUInteger numTableRows;
152
153  fieldEditor = [[self window] fieldEditor:YES forObject:self];
154
155  _componentRange = [self currentComponentRange];
156  selectedRange = [fieldEditor selectedRange];
157
158  shouldShowDropDown = (_shouldShowDropDown &&
159			NSMaxRange(selectedRange) == NSMaxRange(_componentRange) &&
160			NSEqualRanges(NSUnionRange(_componentRange, selectedRange), _componentRange));
161  shouldComplete = (shouldShowDropDown && selectedRange.length == 0);
162  _shouldShowDropDown = YES;
163
164  if (shouldComplete)
165    {
166      NSString *prefix, *newComponent;
167
168      AUTORELEASE(_cachedCompletions);
169      _prefixRange = _componentRange;
170
171      prefix = [[self stringValue] substringWithRange:_prefixRange];
172      newComponent = [_dataSource completionForPrefix:prefix];
173
174      if (newComponent)
175        {
176	  id insertedText;
177
178	  _componentRange.length = [newComponent length];
179	  selectedRange.length = _componentRange.length - _prefixRange.length;
180	  insertedText = [newComponent substringWithRange:NSMakeRange(_prefixRange.length, selectedRange.length)];
181
182	  [fieldEditor insertText:insertedText];
183	  [fieldEditor setSelectedRange:selectedRange];
184
185	  _cachedCompletions = [[_dataSource allCompletionsForPrefix:prefix] retain];
186        }
187      else
188        {
189	  _cachedCompletions = nil;
190        }
191    }
192
193  numTableRows = [_cachedCompletions count];
194  shouldShowDropDown = shouldShowDropDown && (numTableRows > 1);
195
196  if (shouldShowDropDown && shouldComplete)
197    {
198      NSString *component;
199      int selectedRow;
200
201      component = [[self stringValue] substringWithRange:_componentRange];
202      selectedRow = [_cachedCompletions indexOfObject:component];
203      [_sharedDropDownTableView reloadData];
204      if (selectedRow == -1 || selectedRow >= [_sharedDropDownTableView numberOfRows])
205        {
206	  [_sharedDropDownTableView deselectAll: nil];
207        }
208      else
209        {
210	  [_sharedDropDownTableView selectRow: selectedRow
211				    byExtendingSelection: NO];
212        }
213    }
214
215  [self setDropDownIsDown: shouldShowDropDown];
216}
217
218- (BOOL)dropDownIsDown
219{
220  return _dropDownIsDown;
221}
222
223- (void)setDropDownIsDown:(BOOL)flag
224{
225  if (flag)
226    {
227      NSInteger numTableRows, numVisibleTableRows;
228      NSUInteger selectedRow;
229      float visibleTableHeight;
230      NSSize dropDownSize;
231      NSPoint dropDownTopLeft;
232
233      numTableRows = [_cachedCompletions count];
234      selectedRow = [_sharedDropDownTableView selectedRow];
235
236      numVisibleTableRows = numTableRows < _maximumDropDownRows ? numTableRows : _maximumDropDownRows;
237
238      // this is not quite what you'd expect it to be, but seems to be correct on Mac OS X
239      visibleTableHeight = numVisibleTableRows * ([_sharedDropDownTableView rowHeight] +
240						  [_sharedDropDownTableView intercellSpacing].height);
241
242#ifndef MACOSX
243      // We set the table column min/max width.
244      [[[_sharedDropDownTableView tableColumns] objectAtIndex: 0] setMinWidth: [self frame].size.width];
245      [[[_sharedDropDownTableView tableColumns] objectAtIndex: 0] setMaxWidth: [self frame].size.width];
246#endif
247
248      dropDownSize = [NSScrollView frameSizeForContentSize:NSMakeSize(0, visibleTableHeight)
249				   hasHorizontalScroller:NO
250				   hasVerticalScroller:NO
251				   borderType:NSBezelBorder];
252      dropDownSize.width = [self frame].size.width;
253      dropDownTopLeft = [self convertPoint:NSMakePoint(0,[self frame].size.height) toView:nil];
254      dropDownTopLeft = [[self window] convertBaseToScreen:dropDownTopLeft];
255
256      [[[_sharedDropDownTableView tableColumns] objectAtIndex: 0] setWidth: dropDownSize.width];
257
258      [_sharedDropDown setFrame:NSMakeRect(dropDownTopLeft.x,
259
260#ifdef MACOSX
261					   dropDownTopLeft.y - dropDownSize.height,
262#else
263					   dropDownTopLeft.y - dropDownSize.height - [self frame].size.height,
264#endif
265					   dropDownSize.width,
266					   dropDownSize.height)
267		       display: YES];
268
269
270      [_sharedDropDownScrollView setHasVerticalScroller:(numVisibleTableRows != numTableRows)];
271      if (selectedRow != -1)
272        {
273	  [_sharedDropDownTableView scrollRowToVisible:selectedRow];
274        }
275      [_sharedDropDown orderWindow:NSWindowAbove relativeTo:[[self window] windowNumber]];
276    }
277  else // get rid of the drop down
278    {
279      [_sharedDropDown orderOut:nil];
280    }
281  _dropDownIsDown = flag;
282}
283
284- (NSRange)currentComponentRange
285{
286  if (_commaDelimited)
287    {
288      return [self _commaDelimitedCurrentComponentRange];
289    }
290  else
291    {
292      return [self _defaultCurrentComponentRange];
293    }
294}
295
296- (NSRange)_defaultCurrentComponentRange
297{
298  return NSMakeRange(0,[[self stringValue] length]);
299}
300
301- (NSRange)_commaDelimitedCurrentComponentRange
302{
303  NSRange currentComponentRange;
304  NSUInteger componentEndInd, componentStartInd, insertionPoint;
305  NSString *insertionPtOnward, *toInsertionPt;
306
307  NSCharacterSet *commaCharSet    = [NSCharacterSet characterSetWithCharactersInString:@","];
308  NSCharacterSet *nonWhiteCharSet = [[NSCharacterSet whitespaceCharacterSet] invertedSet];
309
310  insertionPoint = [[[self window] fieldEditor:YES forObject:self] selectedRange].location;
311
312  // separate into halves of the string broken at insertionPoint
313  insertionPtOnward = [[self stringValue] substringFromIndex:insertionPoint];
314  toInsertionPt     = [[self stringValue] substringToIndex:insertionPoint];
315
316  // first we find the end of the component
317  // first approximation: the next comma after the insertion point
318  componentEndInd = [insertionPtOnward rangeOfCharacterFromSet:commaCharSet].location;
319
320  // if we didn't find a comma then the end of the string is the (approximate) end of the component
321  if (componentEndInd == NSNotFound)
322    componentEndInd = [insertionPtOnward length];
323
324  // we cut off any trailing white space to get the real end of the component
325  componentEndInd = [insertionPtOnward rangeOfCharacterFromSet:nonWhiteCharSet
326				       options:NSBackwardsSearch
327				       range:NSMakeRange(0,componentEndInd)].location;
328  if (componentEndInd == NSNotFound)
329    componentEndInd = 0;
330  else
331    componentEndInd++;
332
333  // now we have to find the beginning of the component
334  // first approximation: comma before the insertion point
335  componentStartInd = [toInsertionPt rangeOfCharacterFromSet:commaCharSet
336				     options:NSBackwardsSearch].location;
337
338  // if we didn't find a comma, the beginning of the string is the desired index
339  // if we did find a comma, we want the component to start with the next char
340  if (componentStartInd == NSNotFound)
341    componentStartInd = 0;
342  else
343    componentStartInd++;
344
345  // cut off whitespace in the beginning of the component
346  componentStartInd = [toInsertionPt rangeOfCharacterFromSet:nonWhiteCharSet
347				     options:0
348				     range:NSMakeRange(componentStartInd,
349						       [toInsertionPt length] - componentStartInd)].location;
350
351  // if we didn't find anything, the component begins at the insertion point.
352  if (componentStartInd == NSNotFound)
353    componentStartInd = [toInsertionPt length];
354
355  // set the current component range
356  currentComponentRange.location = componentStartInd;
357  currentComponentRange.length   = [toInsertionPt length] - componentStartInd + componentEndInd;
358
359  return currentComponentRange;
360}
361
362- (BOOL)textView:(NSTextView *)aTextView doCommandBySelector:(SEL)aSelector
363{
364  _textViewDoCommandBySelectorResponse = NO;
365  if ([self respondsToSelector:aSelector])
366    {
367      [self performSelector:aSelector withObject:nil];
368    }
369
370  return _textViewDoCommandBySelectorResponse;
371}
372
373- (void)moveDown:(id)sender
374{
375  int selectedRow;
376
377  selectedRow = [_sharedDropDownTableView selectedRow] + 1;
378
379  if (0 <= selectedRow && selectedRow < [_sharedDropDownTableView numberOfRows] )
380    {
381      [_sharedDropDownTableView selectRow:selectedRow
382				byExtendingSelection:NO];
383      [_sharedDropDownTableView scrollRowToVisible:selectedRow];
384      _textViewDoCommandBySelectorResponse = YES;
385    }
386
387  // LM
388#ifndef MACOSX
389  [[self window] makeFirstResponder: self];
390#endif
391}
392
393- (void)moveUp:(id)sender
394{
395  NSInteger selectedRow;
396
397  selectedRow = [_sharedDropDownTableView selectedRow] - 1;
398  if (0 <= selectedRow && selectedRow < [_sharedDropDownTableView numberOfRows] )
399    {
400      [_sharedDropDownTableView selectRow:selectedRow
401				byExtendingSelection:NO];
402      [_sharedDropDownTableView scrollRowToVisible:selectedRow];
403      _textViewDoCommandBySelectorResponse = YES;
404    }
405
406#ifndef MACOSX
407  [[self window] makeFirstResponder: self];
408#endif
409}
410
411- (void)deleteBackward:(id)sender
412{
413  NSRange selectedRange;
414
415  selectedRange = [[[self window] fieldEditor:YES forObject:self] selectedRange];
416  if (selectedRange.location != 0 || selectedRange.length != 0)
417    {
418      _justDeleted = YES;
419    }
420}
421
422
423//
424//
425//
426- (void) tableViewSelectionDidChange: (NSNotification *) theNotification
427{
428  NSMutableString *newString;
429  NSString *newComponent;
430  NSRange selectedRange;
431  NSInteger selectedRow;
432
433  selectedRow = [_sharedDropDownTableView selectedRow];
434
435  if (selectedRow < 0 || selectedRow >= [_cachedCompletions count])
436    {
437      return;
438    }
439
440  newComponent = [_cachedCompletions objectAtIndex: selectedRow];
441  newString = [NSMutableString stringWithString: [self stringValue]];
442  [newString replaceCharactersInRange: _componentRange withString:newComponent];
443  _componentRange.length = [newComponent length];
444  selectedRange = NSMakeRange(_componentRange.location + _prefixRange.length,
445			      _componentRange.length - _prefixRange.length);
446
447  [self setStringValue: newString];
448  [[[self window] fieldEditor:YES forObject:self] setSelectedRange:selectedRange];
449}
450
451- (id)tableView:(NSTableView *)aTableView objectValueForTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex
452{
453  if (rowIndex >= 0 && rowIndex < [_cachedCompletions count])
454    {
455      return [_cachedCompletions objectAtIndex: rowIndex];
456    }
457
458  return nil;
459}
460
461- (BOOL)tableView:(NSTableView *)aTableView shouldEditTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex
462{
463  return NO;
464}
465
466- (NSInteger)numberOfRowsInTableView:(NSTableView *)aTableView
467{
468  return [_cachedCompletions count];
469}
470
471- (void) viewWillMoveToWindow: (NSWindow *) newWindow
472{
473  [super viewWillMoveToWindow: newWindow];
474  [[NSNotificationCenter defaultCenter] removeObserver: self
475					name: NSWindowWillMoveNotification
476					object: [self window]];
477  [[NSNotificationCenter defaultCenter] removeObserver: self
478					name: NSWindowWillCloseNotification
479					object: [self window]];
480
481  // FIXME - This doesn't work under GNUstep - the notification is never posted.
482  [[NSNotificationCenter defaultCenter] addObserver: self
483					selector: @selector(windowWillMove:)
484					name: NSWindowWillMoveNotification
485					object: newWindow];
486  [[NSNotificationCenter defaultCenter] addObserver: self
487					selector: @selector(windowWillClose:)
488					name: NSWindowWillCloseNotification
489					object: newWindow];
490}
491
492- (void) windowWillClose: (NSNotification *) theNotification
493{
494  // We do the same thing as in -windowWillMove
495  [self windowWillMove: theNotification];
496}
497
498- (void)windowWillMove: (NSNotification *) theNotification
499{
500  [NSObject cancelPreviousPerformRequestsWithTarget: self
501	    selector: @selector(complete:)
502	    object: nil];
503  [self setDropDownIsDown: NO];
504}
505
506- (id)dataSource
507{
508  return _dataSource;
509}
510
511- (void)setDataSource:(id)dataSource
512{
513  _dataSource = dataSource;
514}
515
516- (BOOL)commaDelimited
517{
518  return _commaDelimited;
519}
520
521- (void)setCommaDelimited:(BOOL)commaDelimited
522{
523  _commaDelimited = commaDelimited;
524}
525
526- (float)completionDelay
527{
528  return _completionDelay;
529}
530
531- (void)setCompletionDelay:(float)completionDelay
532{
533  _completionDelay = completionDelay;
534}
535
536- (int)maximumDropDownRows
537{
538  return _maximumDropDownRows;
539}
540
541- (void)setMaximumDropDownRows:(int)maxRows
542{
543  _maximumDropDownRows = maxRows;
544}
545
546@end
547