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