1/* 2 NSSearchFieldCell.h 3 4 Text field cell class for text search 5 6 Copyright (C) 2004 Free Software Foundation, Inc. 7 8 Author: H. Nikolaus Schaller <hns@computer.org> 9 Date: Dec 2004 10 Author: Fred Kiefer <fredkiefer@gmx.de> 11 Date: Mar 2006 12 13 This file is part of the GNUstep GUI Library. 14 15 This library is free software; you can redistribute it and/or 16 modify it under the terms of the GNU Lesser General Public 17 License as published by the Free Software Foundation; either 18 version 2 of the License, or (at your option) any later version. 19 20 This library is distributed in the hope that it will be useful, 21 but WITHOUT ANY WARRANTY; without even the implied warranty of 22 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 23 Lesser General Public License for more details. 24 25 You should have received a copy of the GNU Lesser General Public 26 License along with this library; see the file COPYING.LIB. 27 If not, see <http://www.gnu.org/licenses/> or write to the 28 Free Software Foundation, 51 Franklin Street, Fifth Floor, 29 Boston, MA 02110-1301, USA. 30*/ 31 32#import <Foundation/NSArray.h> 33#import <Foundation/NSException.h> 34#import <Foundation/NSNotification.h> 35#import <Foundation/NSString.h> 36#import <Foundation/NSUserDefaults.h> 37 38#import "AppKit/NSApplication.h" 39#import "AppKit/NSButtonCell.h" 40#import "AppKit/NSEvent.h" 41#import "AppKit/NSImage.h" 42#import "AppKit/NSMenu.h" 43#import "AppKit/NSMenuView.h" 44#import "AppKit/NSPopUpButtonCell.h" 45#import "AppKit/NSSearchField.h" 46#import "AppKit/NSSearchFieldCell.h" 47#import "AppKit/NSWindow.h" 48 49@interface NSSearchFieldCell (Private) 50 51- (NSMenu *) _buildTemplate; 52- (void) _openPopup: (id)sender; 53- (void) _clearSearches: (id)sender; 54- (void) _loadSearches; 55- (void) _saveSearches; 56 57@end /* NSSearchFieldCell Private */ 58 59 60@implementation NSSearchFieldCell 61 62#define ICON_WIDTH 16 63 64// Inlined method 65 66static inline NSRect textCellFrameFromRect(NSRect cellRect) 67// Not the drawed part, precises just the part which receives events 68{ 69 return NSMakeRect(cellRect.origin.x + ICON_WIDTH, 70 NSMinY(cellRect), 71 NSWidth(cellRect) - 2*ICON_WIDTH, 72 NSHeight(cellRect)); 73} 74 75- (id) initTextCell:(NSString *)aString 76{ 77 self = [super initTextCell: aString]; 78 if (self) 79 { 80 NSButtonCell *c; 81 // NSMenu *template; 82 83 c = [[NSButtonCell alloc] initImageCell: nil]; 84 [self setCancelButtonCell: c]; 85 RELEASE(c); 86 [self resetCancelButtonCell]; 87 88 c = [[NSButtonCell alloc] initImageCell: nil]; 89 [self setSearchButtonCell: c]; 90 RELEASE(c); 91 [self resetSearchButtonCell]; 92 93/* Don't set the searchMenuTemplate unless it is explicitly set in code or by a nib connection 94 template = [self _buildTemplate]; 95 [self setSearchMenuTemplate: template]; 96 RELEASE(template); 97*/ 98 99 //_recent_searches = [[NSMutableArray alloc] init]; 100 //_recents_autosave_name = nil; 101 _max_recents = 10; 102 [self _loadSearches]; 103 } 104 105 return self; 106} 107 108- (void) dealloc 109{ 110 RELEASE(_cancel_button_cell); 111 RELEASE(_search_button_cell); 112 RELEASE(_recent_searches); 113 RELEASE(_recents_autosave_name); 114 RELEASE(_menu_template); 115 116 [super dealloc]; 117} 118 119- (id) copyWithZone:(NSZone *) zone; 120{ 121 NSSearchFieldCell *c = [super copyWithZone: zone]; 122 123 c->_cancel_button_cell = [_cancel_button_cell copyWithZone: zone]; 124 c->_search_button_cell = [_search_button_cell copyWithZone: zone]; 125 c->_recent_searches = [_recent_searches mutableCopyWithZone: zone]; 126 c->_recents_autosave_name = [_recents_autosave_name copyWithZone: zone]; 127 c->_menu_template = [_menu_template copyWithZone: zone]; 128 129 return c; 130} 131 132- (BOOL) isOpaque 133{ 134 // only if all components are opaque 135 return [super isOpaque] && [_cancel_button_cell isOpaque] && 136 [_search_button_cell isOpaque]; 137} 138 139- (void) drawWithFrame: (NSRect)cellFrame inView: (NSView*)controlView 140{ 141 [_search_button_cell drawWithFrame: [self searchButtonRectForBounds: cellFrame] 142 inView: controlView]; 143 [super drawWithFrame: [self searchTextRectForBounds: cellFrame] 144 inView: controlView]; 145 if ([[self stringValue] length] > 0) 146 [_cancel_button_cell drawWithFrame: [self cancelButtonRectForBounds: cellFrame] 147 inView: controlView]; 148} 149 150- (BOOL) sendsWholeSearchString 151{ 152 return _sends_whole_search_string; 153} 154 155- (void) setSendsWholeSearchString: (BOOL)flag 156{ 157 _sends_whole_search_string = flag; 158} 159 160- (BOOL) sendsSearchStringImmediately 161{ 162 return _sends_search_string_immediatly; 163} 164 165- (void) setSendsSearchStringImmediately: (BOOL)flag 166{ 167 _sends_search_string_immediatly = flag; 168} 169 170- (NSInteger) maximumRecents 171{ 172 return _max_recents; 173} 174 175- (void) setMaximumRecents: (NSInteger)max 176{ 177 if (max > 254) 178 { 179 max = 254; 180 } 181 else if (max < 0) 182 { 183 max = 10; 184 } 185 186 _max_recents = max; 187} 188 189- (NSArray *) recentSearches 190{ 191 return _recent_searches; 192} 193 194- (NSString *) recentsAutosaveName 195{ 196 return _recents_autosave_name; 197} 198 199- (void) setRecentsAutosaveName: (NSString *)name 200{ 201 ASSIGN(_recents_autosave_name, name); 202 [self _loadSearches]; 203} 204 205- (void) setRecentSearches: (NSArray *)searches 206{ 207 int max; 208 NSMutableArray *mutableSearches; 209 210 max = [self maximumRecents]; 211 if ([searches count] > max) 212 { 213 id buffer[max]; 214 215 [searches getObjects: buffer range: NSMakeRange(0, max)]; 216 mutableSearches = [[NSMutableArray alloc] initWithObjects: buffer count: max]; 217 } 218 else 219 { 220 mutableSearches = [[NSMutableArray alloc] initWithArray: searches]; 221 } 222 [_recent_searches release]; 223 _recent_searches = mutableSearches; 224 [self _saveSearches]; 225} 226 227- (void) addToRecentSearches:(NSString *)searchTerm 228{ 229 if (!_recent_searches) 230 { 231 ASSIGN(_recent_searches, [NSMutableArray array]); 232 } 233 if (searchTerm != nil && [searchTerm length] > 0 234 && [_recent_searches indexOfObject: searchTerm] == NSNotFound) 235 { 236 [_recent_searches addObject: searchTerm]; 237 [self _saveSearches]; 238 } 239} 240 241- (NSMenu *) searchMenuTemplate 242{ 243 return _menu_template; 244} 245 246- (void) setSearchMenuTemplate: (NSMenu *)menu 247{ 248 ASSIGN(_menu_template, menu); 249 if (menu) 250 { 251 [[self searchButtonCell] setTarget: self]; 252 [[self searchButtonCell] setAction: @selector(_openPopup:)]; 253 [[self searchButtonCell] sendActionOn: NSLeftMouseDownMask]; 254 } 255 else 256 { 257 [self resetSearchButtonCell]; 258 } 259} 260 261- (NSButtonCell *) cancelButtonCell 262{ 263 return _cancel_button_cell; 264} 265 266- (void) setCancelButtonCell: (NSButtonCell *)cell 267{ 268 ASSIGN(_cancel_button_cell, cell); 269} 270 271- (NSButtonCell *) searchButtonCell 272{ 273 return _search_button_cell; 274} 275 276- (void) setSearchButtonCell: (NSButtonCell *)cell 277{ 278 ASSIGN(_search_button_cell, cell); 279} 280 281- (void) resetCancelButtonCell 282{ 283 NSButtonCell *c; 284 285 c = [self cancelButtonCell]; 286 // configure the button 287 [c setButtonType: NSMomentaryChangeButton]; 288 [c setBezelStyle: NSRegularSquareBezelStyle]; 289 [c setBordered: NO]; 290 [c setBezeled: NO]; 291 [c setEditable: NO]; 292 [c setImagePosition: NSImageOnly]; 293 [c setImage: [NSImage imageNamed: @"GSStop"]]; 294 [c setAction: @selector(clearSearch:)]; 295 [c setTarget: self]; 296 [c setKeyEquivalent: @"\e"]; 297 [c setKeyEquivalentModifierMask: 0]; 298} 299 300- (void) resetSearchButtonCell 301{ 302 NSButtonCell *c; 303 304 c = [self searchButtonCell]; 305 // configure the button 306 [c setButtonType: NSMomentaryChangeButton]; 307 [c setBezelStyle: NSRegularSquareBezelStyle]; 308 [c setBordered: NO]; 309 [c setBezeled: NO]; 310 [c setEditable: NO]; 311 [c setImagePosition: NSImageOnly]; 312 [c setImage: [NSImage imageNamed: @"GSSearch"]]; 313// [c setAction: [self action]]; 314// [c setTarget: [self target]]; 315 [c setAction: @selector(performClick:)]; 316 [c setTarget: self]; 317 [c sendActionOn: NSLeftMouseUpMask]; 318 [c setKeyEquivalent: @"\r"]; 319 [c setKeyEquivalentModifierMask: 0]; 320} 321 322- (NSRect) cancelButtonRectForBounds: (NSRect)rect 323{ 324 NSRect part, clear; 325 326 NSDivideRect(rect, &clear, &part, ICON_WIDTH, NSMaxXEdge); 327 return clear; 328} 329 330- (NSRect) searchTextRectForBounds: (NSRect)rect 331{ 332 NSRect search, text, clear, part; 333 334 if (!_search_button_cell) 335 { 336 // nothing to split off 337 part = rect; 338 } 339 else 340 { 341 NSDivideRect(rect, &search, &part, ICON_WIDTH, NSMinXEdge); 342 } 343 344 if (!_cancel_button_cell) 345 { 346 // nothing to split off 347 text = part; 348 } 349 else 350 { 351 NSDivideRect(part, &clear, &text, ICON_WIDTH, NSMaxXEdge); 352 } 353 354 return text; 355} 356 357- (NSRect) searchButtonRectForBounds: (NSRect)rect; 358{ 359 NSRect search, part; 360 361 NSDivideRect(rect, &search, &part, ICON_WIDTH, NSMinXEdge); 362 return search; 363} 364 365- (void) editWithFrame: (NSRect)aRect 366 inView: (NSView*)controlView 367 editor: (NSText*)textObject 368 delegate: (id)anObject 369 event: (NSEvent*)theEvent 370{ 371 // constrain to visible text area 372 [super editWithFrame: [self searchTextRectForBounds: aRect] 373 inView: controlView 374 editor: textObject 375 delegate: anObject 376 event: theEvent]; 377} 378 379- (void) endEditing: (NSText *)editor 380{ 381 [self addToRecentSearches: [[[editor string] copy] autorelease]]; 382 [super endEditing: editor]; 383 [[NSNotificationCenter defaultCenter] 384 removeObserver: self 385 name: NSTextDidChangeNotification 386 object: editor]; 387} 388 389- (void) selectWithFrame: (NSRect)aRect 390 inView: (NSView*)controlView 391 editor: (NSText*)textObject 392 delegate: (id)anObject 393 start: (NSInteger)selStart 394 length: (NSInteger)selLength 395{ 396 // constrain to visible text area 397 [super selectWithFrame: [self searchTextRectForBounds: aRect] 398 inView: controlView 399 editor: textObject 400 delegate: anObject 401 start: selStart 402 length: selLength]; 403 [[NSNotificationCenter defaultCenter] 404 addObserver: self 405 selector: @selector(textDidChange:) 406 name: NSTextDidChangeNotification 407 object: textObject]; 408} 409 410- (BOOL) trackMouse: (NSEvent *)event 411 inRect: (NSRect)cellFrame 412 ofView: (NSView *)controlView 413 untilMouseUp: (BOOL)untilMouseUp 414{ 415 NSRect rect; 416 NSPoint thePoint; 417 NSPoint location = [event locationInWindow]; 418 NSText *currentEditor; 419 420 thePoint = [controlView convertPoint: location fromView: nil]; 421 422 // check if we are within the search/stop buttons 423 rect = [self searchButtonRectForBounds: cellFrame]; 424 if ([controlView mouse: thePoint inRect: rect]) 425 { 426 return [[self searchButtonCell] trackMouse: event 427 inRect: rect 428 ofView: controlView 429 untilMouseUp: untilMouseUp]; 430 } 431 432 rect = [self cancelButtonRectForBounds: cellFrame]; 433 if ([controlView mouse: thePoint inRect: rect]) 434 { 435 return [[self cancelButtonCell] trackMouse: event 436 inRect: rect 437 ofView: controlView 438 untilMouseUp: untilMouseUp]; 439 } 440 441 currentEditor = ([controlView isKindOfClass:[NSControl class]] 442 ? [(NSControl *)controlView currentEditor] 443 : nil); 444 if (currentEditor) 445 { 446 [currentEditor mouseDown: event]; 447 return YES; 448 } 449 450 return [super trackMouse: event 451 inRect: [self searchTextRectForBounds: cellFrame] 452 ofView: controlView 453 untilMouseUp: untilMouseUp]; 454} 455 456- (void) resetCursorRect: (NSRect)cellFrame inView: (NSView *)controlView 457{ 458 [super resetCursorRect: textCellFrameFromRect(cellFrame) 459 inView: controlView]; 460} 461 462- (void) textDidChange: (NSNotification *)notification 463{ 464 NSText *textObject; 465 [_control_view setNeedsDisplay:YES]; 466 467 // make textChanged send action (unless disabled) 468 if (_sends_whole_search_string) 469 { 470 // ignore 471 return; 472 } 473 474 textObject = [notification object]; 475 // copy the current NSTextEdit string so that it can be read from the NSSearchFieldCell! 476 [self setStringValue: [textObject string]]; 477 [NSApp sendAction:[self action] to:[self target] from:_control_view]; 478} 479 480- (void) clearSearch:(id)sender 481{ 482 [self setStringValue:@""]; 483 [NSApp sendAction:[self action] to:[self target] from:_control_view]; 484 [_control_view setNeedsDisplay:YES]; 485} 486 487// 488// NSCoding protocol 489// 490- (void) encodeWithCoder: (NSCoder*)aCoder 491{ 492 NSInteger max = [self maximumRecents]; 493 494 [super encodeWithCoder: aCoder]; 495 496 if ([aCoder allowsKeyedCoding]) 497 { 498 [aCoder encodeObject: _search_button_cell forKey: @"NSSearchButtonCell"]; 499 [aCoder encodeObject: _cancel_button_cell forKey: @"NSCancelButtonCell"]; 500 [aCoder encodeObject: _recents_autosave_name forKey: @"NSRecentsAutosaveName"]; 501 [aCoder encodeBool: _sends_whole_search_string forKey: @"NSSendsWholeSearchString"]; 502 [aCoder encodeInt: max forKey: @"NSMaximumRecents"]; 503 } 504 else 505 { 506 [aCoder encodeObject: _search_button_cell]; 507 [aCoder encodeObject: _cancel_button_cell]; 508 [aCoder encodeObject: _recents_autosave_name]; 509 [aCoder encodeValueOfObjCType: @encode(BOOL) 510 at: &_sends_whole_search_string]; 511 [aCoder encodeValueOfObjCType: @encode(unsigned int) 512 at: &max]; 513 } 514} 515 516- (id) initWithCoder: (NSCoder*)aDecoder 517{ 518 self = [super initWithCoder: aDecoder]; 519 520 if (self != nil) 521 { 522 if ([aDecoder allowsKeyedCoding]) 523 { 524 [self setSearchButtonCell: [aDecoder decodeObjectForKey: @"NSSearchButtonCell"]]; 525 [self setCancelButtonCell: [aDecoder decodeObjectForKey: @"NSCancelButtonCell"]]; 526 [self setRecentsAutosaveName: [aDecoder decodeObjectForKey: @"NSRecentsAutosaveName"]]; 527 [self setSendsWholeSearchString: [aDecoder decodeBoolForKey: @"NSSendsWholeSearchString"]]; 528 [self setMaximumRecents: [aDecoder decodeIntForKey: @"NSMaximumRecents"]]; 529 } 530 else 531 { 532 NSInteger max; 533 534 [self setSearchButtonCell: [aDecoder decodeObject]]; 535 [self setCancelButtonCell: [aDecoder decodeObject]]; 536 [self setRecentsAutosaveName: [aDecoder decodeObject]]; 537 [aDecoder decodeValueOfObjCType: @encode(BOOL) at: &_sends_whole_search_string]; 538 [aDecoder decodeValueOfObjCType: @encode(unsigned int) at: &max]; 539 [self setMaximumRecents: max]; 540 } 541 542 [self resetCancelButtonCell]; 543 [self resetSearchButtonCell]; 544 } 545 546 return self; 547} 548 549@end /* NSSearchFieldCell */ 550 551 552@implementation NSSearchFieldCell (Private) 553 554/* Set up a default template 555 */ 556- (NSMenu *) _buildTemplate 557{ 558 NSMenu *template; 559 NSMenuItem *item; 560 561 template = [[NSMenu alloc] init]; 562 563 item = [[NSMenuItem alloc] initWithTitle: @"Recent searches" 564 action: NULL 565 keyEquivalent: @""]; 566 [item setTag: NSSearchFieldRecentsTitleMenuItemTag]; 567 [template addItem: item]; 568 RELEASE(item); 569 570 item = [[NSMenuItem alloc] initWithTitle: @"Recent search item" 571 action: @selector(search:) 572 keyEquivalent: @""]; 573 [item setTag: NSSearchFieldRecentsMenuItemTag]; 574 [template addItem: item]; 575 RELEASE(item); 576 577 item = [[NSMenuItem alloc] initWithTitle: @"Clear recent searches" 578 action: @selector(_clearSearches:) 579 keyEquivalent: @""]; 580 [item setTag: NSSearchFieldClearRecentsMenuItemTag]; 581 [item setTarget: self]; 582 [template addItem: item]; 583 584 RELEASE(item); 585 item = [[NSMenuItem alloc] initWithTitle: @"No recent searches" 586 action: NULL 587 keyEquivalent: @""]; 588 [item setTag: NSSearchFieldNoRecentsMenuItemTag]; 589 [template addItem: item]; 590 RELEASE(item); 591 592 return template; 593} 594 595- (void) _openPopup: (id)sender 596{ 597 NSMenu *template; 598 NSMenu *popupmenu; 599 NSMenuView *mr; 600 NSWindow *cvWin; 601 NSRect cellFrame; 602 int i; 603 int recentCount = [_recent_searches count]; 604 NSPopUpButtonCell *pbcell = [[NSPopUpButtonCell alloc] initTextCell:nil pullsDown:NO]; 605 int selectedItemIndex = -1, newSelectedItemIndex; 606 607 template = [self searchMenuTemplate]; 608 popupmenu = [[NSMenu alloc] init]; 609 610 // Fill the popup menu 611 for (i = 0; i < [template numberOfItems]; i++) 612 { 613 int tag; 614 NSMenuItem *item, *newItem = nil; 615 616 item = (NSMenuItem*)[template itemAtIndex: i]; 617 if ([item state]) 618 selectedItemIndex = [popupmenu numberOfItems]; // remember index of previously selected item 619 tag = [item tag]; 620 if (tag == NSSearchFieldRecentsTitleMenuItemTag) 621 { 622 if (recentCount > 0) // only show items with this tag if there are recent searches 623 { 624 newItem = [[item copy] autorelease]; 625 } 626 } 627 else if (tag == NSSearchFieldClearRecentsMenuItemTag) 628 { 629 if (recentCount > 0) // only show items with this tag if there are recent searches 630 { 631 newItem = [[item copy] autorelease]; 632 [newItem setTarget:self]; 633 [newItem setAction:@selector(_clearSearches:)]; 634 } 635 } 636 else if (tag == NSSearchFieldNoRecentsMenuItemTag) 637 { 638 if (recentCount == 0) // only show items with this tag if there are NO recent searches 639 { 640 newItem = [[item copy] autorelease]; 641 } 642 } 643 else if (tag == NSSearchFieldRecentsMenuItemTag) 644 { 645 int j; 646 647 for (j = 0; j < recentCount; j++) 648 { 649 id <NSMenuItem> searchItem = [popupmenu addItemWithTitle: 650 [_recent_searches objectAtIndex: j] 651 action: 652 @selector(_searchForRecent:) 653 keyEquivalent: 654 [item keyEquivalent]]; 655 [searchItem setTarget: self]; 656 } 657 } 658 else // copy all other items without special tags from the template into the popup 659 { 660 newItem = [[item copy] autorelease]; 661 } 662 663 if (newItem != nil) 664 { 665 [popupmenu addItem: newItem]; 666 } 667 } 668 669 [pbcell setMenu:popupmenu]; 670 [pbcell selectItemAtIndex:selectedItemIndex]; 671 [[popupmenu itemAtIndex:selectedItemIndex] setState:NSOffState]; // ensure that state resets fully 672 [[popupmenu itemAtIndex:selectedItemIndex] setState:NSOnState]; 673 674 // Prepare to display the popup 675 cvWin = [_control_view window]; 676 cellFrame = [_control_view frame]; 677 cellFrame = [[_control_view superview] convertRect:cellFrame toView:nil]; // convert to window coordinates 678 cellFrame.origin = [cvWin convertBaseToScreen:cellFrame.origin]; // convert to screen coordinates 679 mr = [popupmenu menuRepresentation]; 680 681 // Ask the MenuView to attach the menu to this rect 682 [mr setWindowFrameForAttachingToRect: cellFrame 683 onScreen: [cvWin screen] 684 preferredEdge: NSMinYEdge 685 popUpSelectedItem: -1]; 686 687 // Last, display the window 688 [[mr window] orderFrontRegardless]; 689 690 [mr mouseDown: [NSApp currentEvent]]; 691 newSelectedItemIndex = [pbcell indexOfSelectedItem]; 692 if (newSelectedItemIndex != selectedItemIndex && newSelectedItemIndex != -1 693 && newSelectedItemIndex < [template numberOfItems]) 694 { 695 int tag = [[template itemAtIndex:newSelectedItemIndex] tag]; 696 if (tag != NSSearchFieldRecentsTitleMenuItemTag && tag != NSSearchFieldClearRecentsMenuItemTag 697 && tag != NSSearchFieldNoRecentsMenuItemTag && tag != NSSearchFieldRecentsMenuItemTag 698 && ![[template itemAtIndex:newSelectedItemIndex] isSeparatorItem]) 699 { 700 //new selected item within the template that's not a template special item 701 [[template itemAtIndex:selectedItemIndex] setState:NSOffState]; 702 [[template itemAtIndex:newSelectedItemIndex] setState:NSOnState]; 703 } 704 } 705 AUTORELEASE(popupmenu); 706 AUTORELEASE(pbcell); 707} 708 709- (void) _searchForRecent: (id)sender 710{ 711 NSString *searchTerm = [sender title]; 712 713 [self setStringValue: searchTerm]; 714 [self performClick: self]; // do the search 715 [(id)_control_view selectText: self]; 716} 717 718- (void) _clearSearches: (id)sender 719{ 720 [self setRecentSearches: [NSArray array]]; 721} 722 723- (void) _loadSearches 724{ 725 NSArray *list; 726 NSString *name = [self recentsAutosaveName]; 727 728 if (name) 729 { 730 list = [[NSUserDefaults standardUserDefaults] 731 stringArrayForKey: name]; 732 [self setRecentSearches: list]; 733 } 734} 735 736- (void) _saveSearches 737{ 738 NSArray *list = [self recentSearches]; 739 NSString *name = [self recentsAutosaveName]; 740 741 if (name && list) 742 { 743 [[NSUserDefaults standardUserDefaults] 744 setObject: list forKey: name]; 745 } 746} 747 748@end /* NSSearchFieldCell Private */ 749