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