/* NSSearchFieldCell.h Text field cell class for text search Copyright (C) 2004 Free Software Foundation, Inc. Author: H. Nikolaus Schaller Date: Dec 2004 Author: Fred Kiefer Date: Mar 2006 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 #import #import "AppKit/NSApplication.h" #import "AppKit/NSButtonCell.h" #import "AppKit/NSEvent.h" #import "AppKit/NSImage.h" #import "AppKit/NSMenu.h" #import "AppKit/NSMenuView.h" #import "AppKit/NSPopUpButtonCell.h" #import "AppKit/NSSearchField.h" #import "AppKit/NSSearchFieldCell.h" #import "AppKit/NSWindow.h" @interface NSSearchFieldCell (Private) - (NSMenu *) _buildTemplate; - (void) _openPopup: (id)sender; - (void) _clearSearches: (id)sender; - (void) _loadSearches; - (void) _saveSearches; @end /* NSSearchFieldCell Private */ @implementation NSSearchFieldCell #define ICON_WIDTH 16 // Inlined method static inline NSRect textCellFrameFromRect(NSRect cellRect) // Not the drawed part, precises just the part which receives events { return NSMakeRect(cellRect.origin.x + ICON_WIDTH, NSMinY(cellRect), NSWidth(cellRect) - 2*ICON_WIDTH, NSHeight(cellRect)); } - (id) initTextCell:(NSString *)aString { self = [super initTextCell: aString]; if (self) { NSButtonCell *c; // NSMenu *template; c = [[NSButtonCell alloc] initImageCell: nil]; [self setCancelButtonCell: c]; RELEASE(c); [self resetCancelButtonCell]; c = [[NSButtonCell alloc] initImageCell: nil]; [self setSearchButtonCell: c]; RELEASE(c); [self resetSearchButtonCell]; /* Don't set the searchMenuTemplate unless it is explicitly set in code or by a nib connection template = [self _buildTemplate]; [self setSearchMenuTemplate: template]; RELEASE(template); */ //_recent_searches = [[NSMutableArray alloc] init]; //_recents_autosave_name = nil; _max_recents = 10; [self _loadSearches]; } return self; } - (void) dealloc { RELEASE(_cancel_button_cell); RELEASE(_search_button_cell); RELEASE(_recent_searches); RELEASE(_recents_autosave_name); RELEASE(_menu_template); [super dealloc]; } - (id) copyWithZone:(NSZone *) zone; { NSSearchFieldCell *c = [super copyWithZone: zone]; c->_cancel_button_cell = [_cancel_button_cell copyWithZone: zone]; c->_search_button_cell = [_search_button_cell copyWithZone: zone]; c->_recent_searches = [_recent_searches mutableCopyWithZone: zone]; c->_recents_autosave_name = [_recents_autosave_name copyWithZone: zone]; c->_menu_template = [_menu_template copyWithZone: zone]; return c; } - (BOOL) isOpaque { // only if all components are opaque return [super isOpaque] && [_cancel_button_cell isOpaque] && [_search_button_cell isOpaque]; } - (void) drawWithFrame: (NSRect)cellFrame inView: (NSView*)controlView { [_search_button_cell drawWithFrame: [self searchButtonRectForBounds: cellFrame] inView: controlView]; [super drawWithFrame: [self searchTextRectForBounds: cellFrame] inView: controlView]; if ([[self stringValue] length] > 0) [_cancel_button_cell drawWithFrame: [self cancelButtonRectForBounds: cellFrame] inView: controlView]; } - (BOOL) sendsWholeSearchString { return _sends_whole_search_string; } - (void) setSendsWholeSearchString: (BOOL)flag { _sends_whole_search_string = flag; } - (BOOL) sendsSearchStringImmediately { return _sends_search_string_immediatly; } - (void) setSendsSearchStringImmediately: (BOOL)flag { _sends_search_string_immediatly = flag; } - (NSInteger) maximumRecents { return _max_recents; } - (void) setMaximumRecents: (NSInteger)max { if (max > 254) { max = 254; } else if (max < 0) { max = 10; } _max_recents = max; } - (NSArray *) recentSearches { return _recent_searches; } - (NSString *) recentsAutosaveName { return _recents_autosave_name; } - (void) setRecentsAutosaveName: (NSString *)name { ASSIGN(_recents_autosave_name, name); [self _loadSearches]; } - (void) setRecentSearches: (NSArray *)searches { int max; NSMutableArray *mutableSearches; max = [self maximumRecents]; if ([searches count] > max) { id buffer[max]; [searches getObjects: buffer range: NSMakeRange(0, max)]; mutableSearches = [[NSMutableArray alloc] initWithObjects: buffer count: max]; } else { mutableSearches = [[NSMutableArray alloc] initWithArray: searches]; } [_recent_searches release]; _recent_searches = mutableSearches; [self _saveSearches]; } - (void) addToRecentSearches:(NSString *)searchTerm { if (!_recent_searches) { ASSIGN(_recent_searches, [NSMutableArray array]); } if (searchTerm != nil && [searchTerm length] > 0 && [_recent_searches indexOfObject: searchTerm] == NSNotFound) { [_recent_searches addObject: searchTerm]; [self _saveSearches]; } } - (NSMenu *) searchMenuTemplate { return _menu_template; } - (void) setSearchMenuTemplate: (NSMenu *)menu { ASSIGN(_menu_template, menu); if (menu) { [[self searchButtonCell] setTarget: self]; [[self searchButtonCell] setAction: @selector(_openPopup:)]; [[self searchButtonCell] sendActionOn: NSLeftMouseDownMask]; } else { [self resetSearchButtonCell]; } } - (NSButtonCell *) cancelButtonCell { return _cancel_button_cell; } - (void) setCancelButtonCell: (NSButtonCell *)cell { ASSIGN(_cancel_button_cell, cell); } - (NSButtonCell *) searchButtonCell { return _search_button_cell; } - (void) setSearchButtonCell: (NSButtonCell *)cell { ASSIGN(_search_button_cell, cell); } - (void) resetCancelButtonCell { NSButtonCell *c; c = [self cancelButtonCell]; // configure the button [c setButtonType: NSMomentaryChangeButton]; [c setBezelStyle: NSRegularSquareBezelStyle]; [c setBordered: NO]; [c setBezeled: NO]; [c setEditable: NO]; [c setImagePosition: NSImageOnly]; [c setImage: [NSImage imageNamed: @"GSStop"]]; [c setAction: @selector(clearSearch:)]; [c setTarget: self]; [c setKeyEquivalent: @"\e"]; [c setKeyEquivalentModifierMask: 0]; } - (void) resetSearchButtonCell { NSButtonCell *c; c = [self searchButtonCell]; // configure the button [c setButtonType: NSMomentaryChangeButton]; [c setBezelStyle: NSRegularSquareBezelStyle]; [c setBordered: NO]; [c setBezeled: NO]; [c setEditable: NO]; [c setImagePosition: NSImageOnly]; [c setImage: [NSImage imageNamed: @"GSSearch"]]; // [c setAction: [self action]]; // [c setTarget: [self target]]; [c setAction: @selector(performClick:)]; [c setTarget: self]; [c sendActionOn: NSLeftMouseUpMask]; [c setKeyEquivalent: @"\r"]; [c setKeyEquivalentModifierMask: 0]; } - (NSRect) cancelButtonRectForBounds: (NSRect)rect { NSRect part, clear; NSDivideRect(rect, &clear, &part, ICON_WIDTH, NSMaxXEdge); return clear; } - (NSRect) searchTextRectForBounds: (NSRect)rect { NSRect search, text, clear, part; if (!_search_button_cell) { // nothing to split off part = rect; } else { NSDivideRect(rect, &search, &part, ICON_WIDTH, NSMinXEdge); } if (!_cancel_button_cell) { // nothing to split off text = part; } else { NSDivideRect(part, &clear, &text, ICON_WIDTH, NSMaxXEdge); } return text; } - (NSRect) searchButtonRectForBounds: (NSRect)rect; { NSRect search, part; NSDivideRect(rect, &search, &part, ICON_WIDTH, NSMinXEdge); return search; } - (void) editWithFrame: (NSRect)aRect inView: (NSView*)controlView editor: (NSText*)textObject delegate: (id)anObject event: (NSEvent*)theEvent { // constrain to visible text area [super editWithFrame: [self searchTextRectForBounds: aRect] inView: controlView editor: textObject delegate: anObject event: theEvent]; } - (void) endEditing: (NSText *)editor { [self addToRecentSearches: [[[editor string] copy] autorelease]]; [super endEditing: editor]; [[NSNotificationCenter defaultCenter] removeObserver: self name: NSTextDidChangeNotification object: editor]; } - (void) selectWithFrame: (NSRect)aRect inView: (NSView*)controlView editor: (NSText*)textObject delegate: (id)anObject start: (NSInteger)selStart length: (NSInteger)selLength { // constrain to visible text area [super selectWithFrame: [self searchTextRectForBounds: aRect] inView: controlView editor: textObject delegate: anObject start: selStart length: selLength]; [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(textDidChange:) name: NSTextDidChangeNotification object: textObject]; } - (BOOL) trackMouse: (NSEvent *)event inRect: (NSRect)cellFrame ofView: (NSView *)controlView untilMouseUp: (BOOL)untilMouseUp { NSRect rect; NSPoint thePoint; NSPoint location = [event locationInWindow]; NSText *currentEditor; thePoint = [controlView convertPoint: location fromView: nil]; // check if we are within the search/stop buttons rect = [self searchButtonRectForBounds: cellFrame]; if ([controlView mouse: thePoint inRect: rect]) { return [[self searchButtonCell] trackMouse: event inRect: rect ofView: controlView untilMouseUp: untilMouseUp]; } rect = [self cancelButtonRectForBounds: cellFrame]; if ([controlView mouse: thePoint inRect: rect]) { return [[self cancelButtonCell] trackMouse: event inRect: rect ofView: controlView untilMouseUp: untilMouseUp]; } currentEditor = ([controlView isKindOfClass:[NSControl class]] ? [(NSControl *)controlView currentEditor] : nil); if (currentEditor) { [currentEditor mouseDown: event]; return YES; } return [super trackMouse: event inRect: [self searchTextRectForBounds: cellFrame] ofView: controlView untilMouseUp: untilMouseUp]; } - (void) resetCursorRect: (NSRect)cellFrame inView: (NSView *)controlView { [super resetCursorRect: textCellFrameFromRect(cellFrame) inView: controlView]; } - (void) textDidChange: (NSNotification *)notification { NSText *textObject; [_control_view setNeedsDisplay:YES]; // make textChanged send action (unless disabled) if (_sends_whole_search_string) { // ignore return; } textObject = [notification object]; // copy the current NSTextEdit string so that it can be read from the NSSearchFieldCell! [self setStringValue: [textObject string]]; [NSApp sendAction:[self action] to:[self target] from:_control_view]; } - (void) clearSearch:(id)sender { [self setStringValue:@""]; [NSApp sendAction:[self action] to:[self target] from:_control_view]; [_control_view setNeedsDisplay:YES]; } // // NSCoding protocol // - (void) encodeWithCoder: (NSCoder*)aCoder { NSInteger max = [self maximumRecents]; [super encodeWithCoder: aCoder]; if ([aCoder allowsKeyedCoding]) { [aCoder encodeObject: _search_button_cell forKey: @"NSSearchButtonCell"]; [aCoder encodeObject: _cancel_button_cell forKey: @"NSCancelButtonCell"]; [aCoder encodeObject: _recents_autosave_name forKey: @"NSRecentsAutosaveName"]; [aCoder encodeBool: _sends_whole_search_string forKey: @"NSSendsWholeSearchString"]; [aCoder encodeInt: max forKey: @"NSMaximumRecents"]; } else { [aCoder encodeObject: _search_button_cell]; [aCoder encodeObject: _cancel_button_cell]; [aCoder encodeObject: _recents_autosave_name]; [aCoder encodeValueOfObjCType: @encode(BOOL) at: &_sends_whole_search_string]; [aCoder encodeValueOfObjCType: @encode(unsigned int) at: &max]; } } - (id) initWithCoder: (NSCoder*)aDecoder { self = [super initWithCoder: aDecoder]; if (self != nil) { if ([aDecoder allowsKeyedCoding]) { [self setSearchButtonCell: [aDecoder decodeObjectForKey: @"NSSearchButtonCell"]]; [self setCancelButtonCell: [aDecoder decodeObjectForKey: @"NSCancelButtonCell"]]; [self setRecentsAutosaveName: [aDecoder decodeObjectForKey: @"NSRecentsAutosaveName"]]; [self setSendsWholeSearchString: [aDecoder decodeBoolForKey: @"NSSendsWholeSearchString"]]; [self setMaximumRecents: [aDecoder decodeIntForKey: @"NSMaximumRecents"]]; } else { NSInteger max; [self setSearchButtonCell: [aDecoder decodeObject]]; [self setCancelButtonCell: [aDecoder decodeObject]]; [self setRecentsAutosaveName: [aDecoder decodeObject]]; [aDecoder decodeValueOfObjCType: @encode(BOOL) at: &_sends_whole_search_string]; [aDecoder decodeValueOfObjCType: @encode(unsigned int) at: &max]; [self setMaximumRecents: max]; } [self resetCancelButtonCell]; [self resetSearchButtonCell]; } return self; } @end /* NSSearchFieldCell */ @implementation NSSearchFieldCell (Private) /* Set up a default template */ - (NSMenu *) _buildTemplate { NSMenu *template; NSMenuItem *item; template = [[NSMenu alloc] init]; item = [[NSMenuItem alloc] initWithTitle: @"Recent searches" action: NULL keyEquivalent: @""]; [item setTag: NSSearchFieldRecentsTitleMenuItemTag]; [template addItem: item]; RELEASE(item); item = [[NSMenuItem alloc] initWithTitle: @"Recent search item" action: @selector(search:) keyEquivalent: @""]; [item setTag: NSSearchFieldRecentsMenuItemTag]; [template addItem: item]; RELEASE(item); item = [[NSMenuItem alloc] initWithTitle: @"Clear recent searches" action: @selector(_clearSearches:) keyEquivalent: @""]; [item setTag: NSSearchFieldClearRecentsMenuItemTag]; [item setTarget: self]; [template addItem: item]; RELEASE(item); item = [[NSMenuItem alloc] initWithTitle: @"No recent searches" action: NULL keyEquivalent: @""]; [item setTag: NSSearchFieldNoRecentsMenuItemTag]; [template addItem: item]; RELEASE(item); return template; } - (void) _openPopup: (id)sender { NSMenu *template; NSMenu *popupmenu; NSMenuView *mr; NSWindow *cvWin; NSRect cellFrame; int i; int recentCount = [_recent_searches count]; NSPopUpButtonCell *pbcell = [[NSPopUpButtonCell alloc] initTextCell:nil pullsDown:NO]; int selectedItemIndex = -1, newSelectedItemIndex; template = [self searchMenuTemplate]; popupmenu = [[NSMenu alloc] init]; // Fill the popup menu for (i = 0; i < [template numberOfItems]; i++) { int tag; NSMenuItem *item, *newItem = nil; item = (NSMenuItem*)[template itemAtIndex: i]; if ([item state]) selectedItemIndex = [popupmenu numberOfItems]; // remember index of previously selected item tag = [item tag]; if (tag == NSSearchFieldRecentsTitleMenuItemTag) { if (recentCount > 0) // only show items with this tag if there are recent searches { newItem = [[item copy] autorelease]; } } else if (tag == NSSearchFieldClearRecentsMenuItemTag) { if (recentCount > 0) // only show items with this tag if there are recent searches { newItem = [[item copy] autorelease]; [newItem setTarget:self]; [newItem setAction:@selector(_clearSearches:)]; } } else if (tag == NSSearchFieldNoRecentsMenuItemTag) { if (recentCount == 0) // only show items with this tag if there are NO recent searches { newItem = [[item copy] autorelease]; } } else if (tag == NSSearchFieldRecentsMenuItemTag) { int j; for (j = 0; j < recentCount; j++) { id searchItem = [popupmenu addItemWithTitle: [_recent_searches objectAtIndex: j] action: @selector(_searchForRecent:) keyEquivalent: [item keyEquivalent]]; [searchItem setTarget: self]; } } else // copy all other items without special tags from the template into the popup { newItem = [[item copy] autorelease]; } if (newItem != nil) { [popupmenu addItem: newItem]; } } [pbcell setMenu:popupmenu]; [pbcell selectItemAtIndex:selectedItemIndex]; [[popupmenu itemAtIndex:selectedItemIndex] setState:NSOffState]; // ensure that state resets fully [[popupmenu itemAtIndex:selectedItemIndex] setState:NSOnState]; // Prepare to display the popup cvWin = [_control_view window]; cellFrame = [_control_view frame]; cellFrame = [[_control_view superview] convertRect:cellFrame toView:nil]; // convert to window coordinates cellFrame.origin = [cvWin convertBaseToScreen:cellFrame.origin]; // convert to screen coordinates mr = [popupmenu menuRepresentation]; // Ask the MenuView to attach the menu to this rect [mr setWindowFrameForAttachingToRect: cellFrame onScreen: [cvWin screen] preferredEdge: NSMinYEdge popUpSelectedItem: -1]; // Last, display the window [[mr window] orderFrontRegardless]; [mr mouseDown: [NSApp currentEvent]]; newSelectedItemIndex = [pbcell indexOfSelectedItem]; if (newSelectedItemIndex != selectedItemIndex && newSelectedItemIndex != -1 && newSelectedItemIndex < [template numberOfItems]) { int tag = [[template itemAtIndex:newSelectedItemIndex] tag]; if (tag != NSSearchFieldRecentsTitleMenuItemTag && tag != NSSearchFieldClearRecentsMenuItemTag && tag != NSSearchFieldNoRecentsMenuItemTag && tag != NSSearchFieldRecentsMenuItemTag && ![[template itemAtIndex:newSelectedItemIndex] isSeparatorItem]) { //new selected item within the template that's not a template special item [[template itemAtIndex:selectedItemIndex] setState:NSOffState]; [[template itemAtIndex:newSelectedItemIndex] setState:NSOnState]; } } AUTORELEASE(popupmenu); AUTORELEASE(pbcell); } - (void) _searchForRecent: (id)sender { NSString *searchTerm = [sender title]; [self setStringValue: searchTerm]; [self performClick: self]; // do the search [(id)_control_view selectText: self]; } - (void) _clearSearches: (id)sender { [self setRecentSearches: [NSArray array]]; } - (void) _loadSearches { NSArray *list; NSString *name = [self recentsAutosaveName]; if (name) { list = [[NSUserDefaults standardUserDefaults] stringArrayForKey: name]; [self setRecentSearches: list]; } } - (void) _saveSearches { NSArray *list = [self recentSearches]; NSString *name = [self recentsAutosaveName]; if (name && list) { [[NSUserDefaults standardUserDefaults] setObject: list forKey: name]; } } @end /* NSSearchFieldCell Private */