PageRenderTime 43ms CodeModel.GetById 11ms app.highlight 13ms RepoModel.GetById 2ms app.codeStats 0ms

/AppKit/CPSearchField.j

http://github.com/cacaodev/cappuccino
Unknown | 825 lines | 676 code | 149 blank | 0 comment | 0 complexity | b9c97a6b044cb25babb557ca42cda803 MD5 | raw file
  1/*
  2 * CPSearchField.j
  3 * AppKit
  4 *
  5 * Created by cacaodev.
  6 * Copyright 2009.
  7 *
  8 * This library is free software; you can redistribute it and/or
  9 * modify it under the terms of the GNU Lesser General Public
 10 * License as published by the Free Software Foundation; either
 11 * version 2.1 of the License, or (at your option) any later version.
 12 *
 13 * This library is distributed in the hope that it will be useful,
 14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 16 * Lesser General Public License for more details.
 17 *
 18 * You should have received a copy of the GNU Lesser General Public
 19 * License along with this library; if not, write to the Free Software
 20 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 21 */
 22
 23@import "CPButton.j"
 24@import "CPMenu.j"
 25@import "CPMenuItem.j"
 26@import "CPTextField.j"
 27
 28@class CPUserDefaults
 29
 30@global CPApp
 31
 32CPSearchFieldRecentsTitleMenuItemTag    = 1000;
 33CPSearchFieldRecentsMenuItemTag         = 1001;
 34CPSearchFieldClearRecentsMenuItemTag    = 1002;
 35CPSearchFieldNoRecentsMenuItemTag       = 1003;
 36
 37var CPAutosavedRecentsChangedNotification = @"CPAutosavedRecentsChangedNotification";
 38
 39var RECENT_SEARCH_PREFIX = @"   ";
 40
 41/*!
 42    @ingroup appkit
 43    @class CPSearchField
 44    The CPSearchField class defines the programmatic interface for text fields that are optimized for text-based searches. A CPSearchField object directly inherits from the CPTextField class. The search field implemented by these classes presents a standard user interface for searches, including a search button, a cancel button, and a pop-up icon menu for listing recent search strings and custom search categories.
 45
 46    When the user types and then pauses, the text field's action message is sent to its target. You can query the text field's string value for the current text to search for. Do not rely on the sender of the action to be an CPMenu object because the menu may change. If you need to change the menu, modify the search menu template and call the setSearchMenuTemplate: method to update.
 47*/
 48@implementation CPSearchField : CPTextField
 49{
 50    CPButton    _searchButton;
 51    CPButton    _cancelButton;
 52    CPMenu      _searchMenuTemplate;
 53    CPMenu      _searchMenu;
 54
 55    CPString    _recentsAutosaveName;
 56    CPArray     _recentSearches;
 57
 58    int         _maximumRecents;
 59    BOOL        _sendsWholeSearchString;
 60    BOOL        _sendsSearchStringImmediately;
 61    BOOL        _canResignFirstResponder;
 62    CPTimer     _partialStringTimer;
 63}
 64
 65+ (CPString)defaultThemeClass
 66{
 67    return @"searchfield"
 68}
 69
 70+ (CPDictionary)themeAttributes
 71{
 72    return @{
 73            @"image-search": [CPNull null],
 74            @"image-find": [CPNull null],
 75            @"image-cancel": [CPNull null],
 76            @"image-cancel-pressed": [CPNull null]
 77        };
 78}
 79
 80- (id)initWithFrame:(CGRect)frame
 81{
 82    if (self = [super initWithFrame:frame])
 83    {
 84        _maximumRecents = 10;
 85        _sendsWholeSearchString = NO;
 86        _sendsSearchStringImmediately = NO;
 87        _recentsAutosaveName = nil;
 88
 89        [self _init];
 90#if PLATFORM(DOM)
 91        _cancelButton._DOMElement.style.cursor = "default";
 92        _searchButton._DOMElement.style.cursor = "default";
 93#endif
 94    }
 95
 96    return self;
 97}
 98
 99- (void)_init
100{
101    _recentSearches = [CPArray array];
102
103    [self setBezeled:YES];
104    [self setBezelStyle:CPTextFieldRoundedBezel];
105    [self setBordered:YES];
106    [self setEditable:YES];
107    [self setContinuous:YES];
108
109    var bounds = [self bounds],
110        cancelButton = [[CPButton alloc] initWithFrame:[self cancelButtonRectForBounds:bounds]],
111        searchButton = [[CPButton alloc] initWithFrame:[self searchButtonRectForBounds:bounds]];
112
113    [self setCancelButton:cancelButton];
114    [self resetCancelButton];
115
116    [self setSearchButton:searchButton];
117    [self resetSearchButton];
118
119    _canResignFirstResponder = YES;
120}
121
122
123#pragma mark -
124#pragma mark Override observers
125
126- (void)_removeObservers
127{
128    if (!_isObserving)
129        return;
130
131    [super _removeObservers];
132
133    [[CPNotificationCenter defaultCenter] removeObserver:self name:CPControlTextDidChangeNotification object:self];
134}
135
136- (void)_addObservers
137{
138    if (_isObserving)
139        return;
140
141    [super _addObservers];
142
143    [[CPNotificationCenter defaultCenter] addObserver:self selector:@selector(_searchFieldTextDidChange:) name:CPControlTextDidChangeNotification object:self];
144}
145
146// Managing Buttons
147/*!
148    Sets the button used to display the search-button image
149    @param button The search button.
150*/
151- (void)setSearchButton:(CPButton)button
152{
153    if (button != _searchButton)
154    {
155        [_searchButton removeFromSuperview];
156        _searchButton = button;
157
158        [_searchButton setFrame:[self searchButtonRectForBounds:[self bounds]]];
159        [_searchButton setAutoresizingMask:CPViewMaxXMargin];
160        [self addSubview:_searchButton];
161    }
162}
163
164/*!
165    Returns the button used to display the search-button image.
166    @return The search button.
167*/
168- (CPButton)searchButton
169{
170    return _searchButton;
171}
172
173/*!
174    Resets the search button to its default attributes.
175    This method resets the target, action, regular image, and pressed image. By default, when users click the search button or press the Return key, the action defined for the receiver is sent to its designated target. This method gives you a way to customize the search button for specific situations and then reset the button defaults without having to undo changes individually.
176*/
177- (void)resetSearchButton
178{
179    var button = [self searchButton],
180        searchButtonImage = (_searchMenuTemplate === nil) ? [self valueForThemeAttribute:@"image-search"] : [self valueForThemeAttribute:@"image-find"];
181
182    [button setBordered:NO];
183    [button setImageScaling:CPImageScaleAxesIndependently];
184    [button setImage:searchButtonImage];
185    [button setAutoresizingMask:CPViewMaxXMargin];
186}
187
188/*!
189    Sets the button object used to display the cancel-button image.
190    @param button The cancel button.
191*/
192- (void)setCancelButton:(CPButton)button
193{
194    if (button != _cancelButton)
195    {
196        [_cancelButton removeFromSuperview];
197        _cancelButton = button;
198
199        [_cancelButton setFrame:[self cancelButtonRectForBounds:[self bounds]]];
200        [_cancelButton setAutoresizingMask:CPViewMinXMargin];
201        [_cancelButton setTarget:self];
202        [_cancelButton setAction:@selector(cancelOperation:)];
203        [_cancelButton setButtonType:CPMomentaryChangeButton];
204        [self _updateCancelButtonVisibility];
205        [self addSubview:_cancelButton];
206    }
207}
208
209/*!
210    Returns the button object used to display the cancel-button image.
211    @return The cancel button.
212*/
213- (CPButton)cancelButton
214{
215    return _cancelButton;
216}
217
218/*!
219    Resets the cancel button to its default attributes.
220    This method resets the target, action, regular image, and pressed image. This method gives you a way to customize the cancel button for specific situations and then reset the button defaults without having to undo changes individually.
221*/
222- (void)resetCancelButton
223{
224    var button = [self cancelButton];
225    [button setBordered:NO];
226    [button setImageScaling:CPImageScaleAxesIndependently];
227    [button setImage:[self valueForThemeAttribute:@"image-cancel"]];
228    [button setAlternateImage:[self valueForThemeAttribute:@"image-cancel-pressed"]];
229    [button setAutoresizingMask:CPViewMinXMargin];
230    [button setTarget:self];
231    [button setAction:@selector(cancelOperation:)];
232}
233
234// Custom Layout
235/*!
236    Modifies the bounding rectangle for the search-text field.
237    @param rect The current bounding rectangle for the search text field.
238    @return The updated bounding rectangle to use for the search text field. The default value is the value passed into the rect parameter.
239    Subclasses can override this method to return a new bounding rectangle for the text-field object. You might use this method to provide a custom layout for the search field control.
240*/
241- (CGRect)searchTextRectForBounds:(CGRect)rect
242{
243    var leftOffset = 0,
244        width = CGRectGetWidth(rect),
245        bounds = [self bounds];
246
247    if (_searchButton)
248    {
249        var searchBounds = [self searchButtonRectForBounds:bounds];
250        leftOffset = CGRectGetMaxX(searchBounds) + 2;
251    }
252
253    if (_cancelButton)
254    {
255        var cancelRect = [self cancelButtonRectForBounds:bounds];
256        width = CGRectGetMinX(cancelRect) - leftOffset;
257    }
258
259    return CGRectMake(leftOffset, CGRectGetMinY(rect), width, CGRectGetHeight(rect));
260}
261
262/*!
263    Modifies the bounding rectangle for the search button.
264    @param rect The current bounding rectangle for the search button.
265    Subclasses can override this method to return a new bounding rectangle for the search button. You might use this method to provide a custom layout for the search field control.
266*/
267- (CGRect)searchButtonRectForBounds:(CGRect)rect
268{
269    var size = [[self valueForThemeAttribute:@"image-search"] size] || CGSizeMakeZero();
270
271    return CGRectMake(5, (CGRectGetHeight(rect) - size.height) / 2, size.width, size.height);
272}
273
274/*!
275    Modifies the bounding rectangle for the cancel button.
276    @param rect The updated bounding rectangle to use for the cancel button. The default value is the value passed into the rect parameter.
277    Subclasses can override this method to return a new bounding rectangle for the cancel button. You might use this method to provide a custom layout for the search field control.
278*/
279- (CGRect)cancelButtonRectForBounds:(CGRect)rect
280{
281    var size = [[self valueForThemeAttribute:@"image-cancel"] size] || CGSizeMakeZero();
282
283    return CGRectMake(CGRectGetWidth(rect) - size.width - 5, (CGRectGetHeight(rect) - size.width) / 2, size.height, size.height);
284}
285
286// Managing Menu Templates
287/*!
288    Returns the menu template object used to dynamically construct the search pop-up icon menu.
289    @return The current menu template.
290*/
291- (CPMenu)searchMenuTemplate
292{
293    return _searchMenuTemplate;
294}
295
296/*!
297    Sets the menu template object used to dynamically construct the receiver's pop-up icon menu.
298    @param menu The menu template to use.
299    The receiver looks for the tag constants described in ŇMenu tagsÓ to determine how to populate the menu with items related to recent searches. See ŇConfiguring a Search MenuÓ for a sample of how you might set up the search menu template.
300*/
301- (void)setSearchMenuTemplate:(CPMenu)aMenu
302{
303    _searchMenuTemplate = aMenu;
304
305    [self resetSearchButton];
306    [self _loadRecentSearchList];
307    [self _updateSearchMenu];
308}
309
310// Managing Search Modes
311/*!
312    Returns a Boolean value indicating whether the receiver sends the search action message when the user clicks the search button (or presses return) or after each keystroke.
313    @return \c YES if the action message is sent all at once when the user clicks the search button or presses return; otherwise, NO if the search string is sent after each keystroke. The default value is NO.
314*/
315- (BOOL)sendsWholeSearchString
316{
317    return _sendsWholeSearchString;
318}
319
320/*!
321    Sets whether the receiver sends the search action message when the user clicks the search button (or presses return) or after each keystroke.
322    @param flag \c YES to send the action message all at once when the user clicks the search button or presses return; otherwise, NO to send the search string after each keystroke.
323*/
324- (void)setSendsWholeSearchString:(BOOL)flag
325{
326    _sendsWholeSearchString = flag;
327}
328
329/*!
330    Returns a Boolean value indicating whether the receiver sends its action immediately upon being notified of changes to the search field text or after a brief pause.
331    @return \c YES if the text field sends its action immediately upon notification of any changes to the search field; otherwise, NO.
332*/
333- (BOOL)sendsSearchStringImmediately
334{
335    return _sendsSearchStringImmediately;
336}
337
338/*!
339    Sets whether the text field sends its action message to the target immediately upon notification of any changes to the search field text or after a brief pause.
340    @param flag \c YES to send the text field's action immediately upon notification of any changes to the search field; otherwise, NO if you want the text field to pause briefly before sending its action message. Pausing gives the user the opportunity to type more text into the search field before initiating the search.
341*/
342- (void)setSendsSearchStringImmediately:(BOOL)flag
343{
344    _sendsSearchStringImmediately = flag;
345}
346
347// Managing Recent Search Strings
348/*!
349    Returns the maximum number of recent search strings to display in the custom search menu.
350    @return The maximum number of search strings that can appear in the menu. This value is between 0 and 254.
351*/
352- (int)maximumRecents
353{
354    return _maximumRecents;
355}
356
357/*!
358    Sets the maximum number of search strings that can appear in the search menu.
359    @param maxRecents The maximum number of search strings that can appear in the menu. This value can be between 0 and 254. Specifying a value less than 0 sets the value to the default, which is 10. Specifying a value greater than 254 sets the maximum to 254.
360*/
361- (void)setMaximumRecents:(int)max
362{
363    if (max > 254)
364        max = 254;
365    else if (max < 0)
366        max = 10;
367
368    _maximumRecents = max;
369}
370
371/*!
372    Returns the list of recent search strings for the control.
373    @return An array of \c CPString objects, each of which contains a search string either displayed in the search menu or from a recent autosave archive. If there have been no recent searches and no prior searches saved under an autosave name, this array may be empty.
374 */
375- (CPArray)recentSearches
376{
377    return _recentSearches;
378}
379
380/*!
381    Sets the list of recent search strings to list in the pop-up icon menu of the receiver.
382    @param searches An array of CPString objects containing the search strings.
383    You might use this method to set the recent list of searches from an archived copy.
384*/
385- (void)setRecentSearches:(CPArray)searches
386{
387    var max = MIN([self maximumRecents], [searches count]),
388        searches = [searches subarrayWithRange:CPMakeRange(0, max)];
389
390    _recentSearches = searches;
391    [self _autosaveRecentSearchList];
392}
393
394/*!
395    Returns the key under which the prior list of recent search strings has been archived.
396    @return The autosave name, which is used as a key in the standard user defaults to save the recent searches. The default value is nil, which causes searches not to be autosaved.
397*/
398- (CPString)recentsAutosaveName
399{
400    return _recentsAutosaveName;
401}
402
403/*!
404    Sets the autosave name under which the receiver automatically archives the list of recent search strings.
405    @param name The autosave name, which is used as a key in the standard user defaults to save the recent searches. If you specify nil or an empty string for this parameter, no autosave name is set and searches are not autosaved.
406*/
407- (void)setRecentsAutosaveName:(CPString)name
408{
409    if (_recentsAutosaveName != nil)
410        [self _deregisterForAutosaveNotification];
411
412    _recentsAutosaveName = name;
413
414    if (_recentsAutosaveName != nil)
415      [self _registerForAutosaveNotification];
416}
417
418// Private methods and subclassing
419
420- (CGRect)contentRectForBounds:(CGRect)bounds
421{
422    var superbounds = [super contentRectForBounds:bounds];
423    return [self searchTextRectForBounds:superbounds];
424}
425
426+ (double)_keyboardDelayForPartialSearchString:(CPString)string
427{
428    return (6 - MIN([string length], 4)) / 10;
429}
430
431- (CPMenu)menu
432{
433    return _searchMenu;
434}
435
436- (BOOL)isOpaque
437{
438  return [super isOpaque] && [_cancelButton isOpaque] && [_searchButton isOpaque];
439}
440
441- (void)_updateCancelButtonVisibility
442{
443    [_cancelButton setHidden:([[self stringValue] length] === 0)];
444}
445
446- (void)_searchFieldTextDidChange:(CPNotification)aNotification
447{
448    if (![self sendsWholeSearchString])
449    {
450        if ([self sendsSearchStringImmediately])
451            [self _sendPartialString];
452        else
453        {
454            [_partialStringTimer invalidate];
455            var timeInterval = [CPSearchField _keyboardDelayForPartialSearchString:[self stringValue]];
456
457            _partialStringTimer = [CPTimer scheduledTimerWithTimeInterval:timeInterval
458                                                                   target:self
459                                                                 selector:@selector(_sendPartialString)
460                                                                 userInfo:nil
461                                                                  repeats:NO];
462        }
463    }
464
465    [self _updateCancelButtonVisibility];
466}
467
468- (void)_sendAction:(id)sender
469{
470    [self sendAction:[self action] to:[self target]];
471}
472
473- (BOOL)sendAction:(SEL)anAction to:(id)anObject
474{
475    [super sendAction:anAction to:anObject];
476
477    [_partialStringTimer invalidate];
478
479    [self _addStringToRecentSearches:[self stringValue]];
480    [self _updateCancelButtonVisibility];
481}
482
483- (void)_addStringToRecentSearches:(CPString)string
484{
485    if (string === nil || string === @"" || [_recentSearches containsObject:string])
486        return;
487
488    var searches = [CPMutableArray arrayWithArray:_recentSearches];
489    [searches addObject:string];
490    [self setRecentSearches:searches];
491    [self _updateSearchMenu];
492}
493
494- (CPView)hitTest:(CGPoint)aPoint
495{
496    // Make sure a hit anywhere within the search field returns the search field itself
497    if (CGRectContainsPoint([self frame], aPoint))
498        return self;
499    else
500        return nil;
501}
502
503- (BOOL)resignFirstResponder
504{
505    return _canResignFirstResponder && [super resignFirstResponder];
506}
507
508- (void)mouseDown:(CPEvent)anEvent
509{
510    var location = [anEvent locationInWindow],
511        point = [self convertPoint:location fromView:nil];
512
513    if (CGRectContainsPoint([self searchButtonRectForBounds:[self bounds]], point))
514    {
515        if (_searchMenuTemplate == nil)
516        {
517            if ([_searchButton target] && [_searchButton action])
518                [_searchButton mouseDown:anEvent];
519            else
520                [self _sendAction:self];
521        }
522        else
523           [self _showMenu];
524    }
525    else if (CGRectContainsPoint([self cancelButtonRectForBounds:[self bounds]], point))
526        [_cancelButton mouseDown:anEvent];
527    else
528        [super mouseDown:anEvent];
529}
530
531/*!
532    Provides the common case items for a recent searches menu. If there are not recent searches,
533    displays a single disabled item:
534
535        No Recent Searches
536
537    If there are 1 more recent searches, it displays:
538
539        Recent Searches
540           recent search 1
541           recent search 2
542           etc.
543        ---------------------
544        Clear Recent Searches
545
546    If you wish to add items before or after the template, you can. If you put items
547    before, a separator will automatically be placed before the default template item.
548    If you add items after the default template, it is your responsibility to add a separator.
549
550    To add a custom item:
551
552    item = [[CPMenuItem alloc] initWithTitle:@"google"
553                                      action:@selector(google:)
554                               keyEquivalent:@""];
555    [item setTag:700];
556    [item setTarget:self];
557    [template addItem:item];
558
559    Be sure that your custom items do not use tags in the range 1000-1003 inclusive.
560    If you wish to maintain state in custom menu items that you add, you will need to maintain
561    the item state yourself, then in the action method of the custom items, modify the items
562    in the search menu template and send [searchField setSearchMenuTemplate:template] to update the menu.
563*/
564- (CPMenu)defaultSearchMenuTemplate
565{
566    var template = [[CPMenu alloc] init],
567        item;
568
569    item = [[CPMenuItem alloc] initWithTitle:@"Recent Searches"
570                                      action:nil
571                               keyEquivalent:@""];
572    [item setTag:CPSearchFieldRecentsTitleMenuItemTag];
573    [item setEnabled:NO];
574    [template addItem:item];
575
576    item = [[CPMenuItem alloc] initWithTitle:@"Recent search item"
577                                      action:@selector(_searchFieldSearch:)
578                               keyEquivalent:@""];
579    [item setTag:CPSearchFieldRecentsMenuItemTag];
580    [item setTarget:self];
581    [template addItem:item];
582
583    item = [[CPMenuItem alloc] initWithTitle:@"Clear Recent Searches"
584                                      action:@selector(_searchFieldClearRecents:)
585                               keyEquivalent:@""];
586    [item setTag:CPSearchFieldClearRecentsMenuItemTag];
587    [item setTarget:self];
588    [template addItem:item];
589
590    item = [[CPMenuItem alloc] initWithTitle:@"No Recent Searches"
591                                      action:nil
592                               keyEquivalent:@""];
593    [item setTag:CPSearchFieldNoRecentsMenuItemTag];
594    [item setEnabled:NO];
595    [template addItem:item];
596
597    return template;
598}
599
600- (void)_updateSearchMenu
601{
602    if (_searchMenuTemplate === nil)
603        return;
604
605    var menu = [[CPMenu alloc] init],
606        countOfRecents = [_recentSearches count],
607        numberOfItems = [_searchMenuTemplate numberOfItems];
608
609    for (var i = 0; i < numberOfItems; i++)
610    {
611        var item = [[_searchMenuTemplate itemAtIndex:i] copy];
612
613        switch ([item tag])
614        {
615            case CPSearchFieldRecentsTitleMenuItemTag:
616                if (countOfRecents === 0)
617                    continue;
618
619                if ([menu numberOfItems] > 0)
620                    [self _addSeparatorToMenu:menu];
621                break;
622
623            case CPSearchFieldRecentsMenuItemTag:
624            {
625                var itemAction = @selector(_searchFieldSearch:);
626
627                for (var recentIndex = 0; recentIndex < countOfRecents; ++recentIndex)
628                {
629                    // RECENT_SEARCH_PREFIX is a hack until CPMenuItem -setIndentationLevel works
630                    var recentItem = [[CPMenuItem alloc] initWithTitle:RECENT_SEARCH_PREFIX + [_recentSearches objectAtIndex:recentIndex]
631                                                                 action:itemAction
632                                                          keyEquivalent:[item keyEquivalent]];
633                    [item setTarget:self];
634                    [menu addItem:recentItem];
635                }
636
637                continue;
638            }
639
640            case CPSearchFieldClearRecentsMenuItemTag:
641                if (countOfRecents === 0)
642                    continue;
643
644                if ([menu numberOfItems] > 0)
645                    [self _addSeparatorToMenu:menu];
646
647                [item setAction:@selector(_searchFieldClearRecents:)];
648                [item setTarget:self];
649                break;
650
651            case CPSearchFieldNoRecentsMenuItemTag:
652                if (countOfRecents !== 0)
653                    continue;
654
655                if ([menu numberOfItems] > 0)
656                    [self _addSeparatorToMenu:menu];
657                break;
658            }
659
660        [item setEnabled:([item isEnabled] && [item action] != nil && [item target] != nil)];
661        [menu addItem:item];
662    }
663
664    [menu setDelegate:self];
665
666    _searchMenu = menu;
667}
668
669- (void)_addSeparatorToMenu:(CPMenu)aMenu
670{
671    var separator = [CPMenuItem separatorItem];
672    [separator setEnabled:NO];
673    [aMenu addItem:separator];
674}
675
676- (void)menuWillOpen:(CPMenu)menu
677{
678    _canResignFirstResponder = NO;
679}
680
681- (void)menuDidClose:(CPMenu)menu
682{
683    _canResignFirstResponder = YES;
684
685    [self becomeFirstResponder];
686}
687
688- (void)_showMenu
689{
690    if (_searchMenu === nil || [_searchMenu numberOfItems] === 0 || ![self isEnabled])
691        return;
692
693    var aFrame = [[self superview] convertRect:[self frame] toView:nil],
694        location = CGPointMake(aFrame.origin.x + 10, aFrame.origin.y + aFrame.size.height - 4);
695
696    var anEvent = [CPEvent mouseEventWithType:CPRightMouseDown location:location modifierFlags:0 timestamp:[[CPApp currentEvent] timestamp] windowNumber:[[self window] windowNumber] context:nil eventNumber:1 clickCount:1 pressure:0];
697
698    [self selectAll:nil];
699    [CPMenu popUpContextMenu:_searchMenu withEvent:anEvent forView:self];
700}
701
702- (void)_sendPartialString
703{
704    [super sendAction:[self action] to:[self target]];
705    [_partialStringTimer invalidate];
706}
707
708- (void)cancelOperation:(id)sender
709{
710    [self setObjectValue:@""];
711    [self textDidChange:[CPNotification notificationWithName:CPControlTextDidChangeNotification object:self userInfo:nil]];
712
713    [self _updateCancelButtonVisibility];
714}
715
716- (void)_searchFieldSearch:(id)sender
717{
718    var searchString = [[sender title] substringFromIndex:[RECENT_SEARCH_PREFIX length]];
719
720    if ([sender tag] != CPSearchFieldRecentsMenuItemTag)
721        [self _addStringToRecentSearches:searchString];
722
723    [self setObjectValue:searchString];
724    [self _sendPartialString];
725    [self selectAll:nil];
726
727    [self _updateCancelButtonVisibility];
728}
729
730- (void)_searchFieldClearRecents:(id)sender
731{
732    [self setRecentSearches:[CPArray array]];
733    [self _updateSearchMenu];
734    [self setStringValue:@""];
735    [self _updateCancelButtonVisibility];
736 }
737
738- (void)_registerForAutosaveNotification
739{
740    [[CPNotificationCenter defaultCenter] addObserver:self selector:@selector(_updateAutosavedRecents:) name:CPAutosavedRecentsChangedNotification object:_recentsAutosaveName];
741}
742
743- (void)_deregisterForAutosaveNotification
744{
745    [[CPNotificationCenter defaultCenter] removeObserver:self name:CPAutosavedRecentsChangedNotification object:_recentsAutosaveName];
746}
747
748- (void)_autosaveRecentSearchList
749{
750    if (_recentsAutosaveName != nil)
751        [[CPNotificationCenter defaultCenter] postNotificationName:CPAutosavedRecentsChangedNotification object:_recentsAutosaveName];
752}
753
754- (void)_updateAutosavedRecents:(id)notification
755{
756    var name = [notification object];
757    [[CPUserDefaults standardUserDefaults] setObject:_recentSearches forKey:name];
758}
759
760- (void)_loadRecentSearchList
761{
762    var name = [self recentsAutosaveName];
763    if (name === nil)
764        return;
765
766    var list = [[CPUserDefaults standardUserDefaults] objectForKey:name];
767
768    if (list !== nil)
769        _recentSearches = list;
770}
771
772@end
773
774var CPRecentsAutosaveNameKey            = @"CPRecentsAutosaveNameKey",
775    CPSendsWholeSearchStringKey         = @"CPSendsWholeSearchStringKey",
776    CPSendsSearchStringImmediatelyKey   = @"CPSendsSearchStringImmediatelyKey",
777    CPMaximumRecentsKey                 = @"CPMaximumRecentsKey",
778    CPSearchMenuTemplateKey             = @"CPSearchMenuTemplateKey";
779
780@implementation CPSearchField (CPCoding)
781
782- (void)encodeWithCoder:(CPCoder)coder
783{
784    [_searchButton removeFromSuperview];
785    [_cancelButton removeFromSuperview];
786
787    [super encodeWithCoder:coder];
788
789    if (_searchButton)
790        [self addSubview:_searchButton];
791    if (_cancelButton)
792        [self addSubview:_cancelButton];
793
794    [coder encodeBool:_sendsWholeSearchString forKey:CPSendsWholeSearchStringKey];
795    [coder encodeBool:_sendsSearchStringImmediately forKey:CPSendsSearchStringImmediatelyKey];
796    [coder encodeInt:_maximumRecents forKey:CPMaximumRecentsKey];
797
798    if (_recentsAutosaveName)
799        [coder encodeObject:_recentsAutosaveName forKey:CPRecentsAutosaveNameKey];
800
801    if (_searchMenuTemplate)
802        [coder encodeObject:_searchMenuTemplate forKey:CPSearchMenuTemplateKey];
803}
804
805- (id)initWithCoder:(CPCoder)coder
806{
807    if (self = [super initWithCoder:coder])
808    {
809        [self setRecentsAutosaveName:[coder decodeObjectForKey:CPRecentsAutosaveNameKey]];
810        _sendsWholeSearchString   = [coder decodeBoolForKey:CPSendsWholeSearchStringKey];
811        _sendsSearchStringImmediately = [coder decodeBoolForKey:CPSendsSearchStringImmediatelyKey];
812        _maximumRecents           = [coder decodeIntForKey:CPMaximumRecentsKey];
813
814        var template              = [coder decodeObjectForKey:CPSearchMenuTemplateKey];
815
816        if (template)
817            [self setSearchMenuTemplate:template];
818
819        [self _init];
820    }
821
822    return self;
823}
824
825@end