API  0.9.8
 All Classes Files Functions Variables Typedefs Macros Groups Pages
CPSearchField.j
Go to the documentation of this 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 
24 
25 @global CPApp
26 
31 
32 var CPAutosavedRecentsChangedNotification = @"CPAutosavedRecentsChangedNotification";
33 
35 
42 @implementation CPSearchField : CPTextField
43 {
44  CPButton _searchButton;
45  CPButton _cancelButton;
46  CPMenu _searchMenuTemplate;
47  CPMenu _searchMenu;
48 
49  CPString _recentsAutosaveName;
50  CPArray _recentSearches;
51 
52  int _maximumRecents;
53  BOOL _sendsWholeSearchString;
54  BOOL _sendsSearchStringImmediately;
55  BOOL _canResignFirstResponder;
56  CPTimer _partialStringTimer;
57 }
58 
59 + (CPString)defaultThemeClass
60 {
61  return @"searchfield"
62 }
63 
64 + (CPDictionary)themeAttributes
65 {
66  return @{
67  @"image-search": [CPNull null],
68  @"image-find": [CPNull null],
69  @"image-cancel": [CPNull null],
70  @"image-cancel-pressed": [CPNull null],
71  @"image-search-inset" : CGInsetMake(0, 0, 0, 5),
72  @"image-cancel-inset" : CGInsetMake(0, 5, 0, 0)
73  };
74 }
75 
76 - (id)initWithFrame:(CGRect)frame
77 {
78  if (self = [super initWithFrame:frame])
79  {
80  _maximumRecents = 10;
81  _sendsWholeSearchString = NO;
82  _sendsSearchStringImmediately = NO;
83  _recentsAutosaveName = nil;
84 
85  [self _init];
86 #if PLATFORM(DOM)
87  _cancelButton._DOMElement.style.cursor = "default";
88  _searchButton._DOMElement.style.cursor = "default";
89 #endif
90  }
91 
92  return self;
93 }
94 
95 - (void)_init
96 {
97  _recentSearches = [CPArray array];
98 
99  [self setBezeled:YES];
100  [self setBezelStyle:CPTextFieldRoundedBezel];
101  [self setBordered:YES];
102  [self setEditable:YES];
103  [self setContinuous:YES];
104 
105  var bounds = [self bounds],
106  cancelButton = [[CPButton alloc] initWithFrame:[self cancelButtonRectForBounds:bounds]],
107  searchButton = [[CPButton alloc] initWithFrame:[self searchButtonRectForBounds:bounds]];
108 
109  [self setCancelButton:cancelButton];
110  [self resetCancelButton];
111 
112  [self setSearchButton:searchButton];
113  [self resetSearchButton];
114 
115  _canResignFirstResponder = YES;
116 }
117 
118 
119 #pragma mark -
120 #pragma mark Override observers
121 
122 - (void)_removeObservers
123 {
124  if (!_isObserving)
125  return;
126 
127  [super _removeObservers];
128 
129  [[CPNotificationCenter defaultCenter] removeObserver:self name:CPControlTextDidChangeNotification object:self];
130 }
131 
132 - (void)_addObservers
133 {
134  if (_isObserving)
135  return;
136 
137  [super _addObservers];
138 
139  [[CPNotificationCenter defaultCenter] addObserver:self selector:@selector(_searchFieldTextDidChange:) name:CPControlTextDidChangeNotification object:self];
140 }
141 
142 // Managing Buttons
147 - (void)setSearchButton:(CPButton)button
148 {
149  if (button != _searchButton)
150  {
151  [_searchButton removeFromSuperview];
152  _searchButton = button;
153 
154  [_searchButton setFrame:[self searchButtonRectForBounds:[self bounds]]];
155  [_searchButton setAutoresizingMask:CPViewMaxXMargin];
156  [self addSubview:_searchButton];
157  }
158 }
159 
164 - (CPButton)searchButton
165 {
166  return _searchButton;
167 }
168 
173 - (void)resetSearchButton
174 {
175  var button = [self searchButton],
176  searchButtonImage = (_searchMenuTemplate === nil) ? [self valueForThemeAttribute:@"image-search"] : [self valueForThemeAttribute:@"image-find"];
177 
178  [button setBordered:NO];
179  [button setImageScaling:CPImageScaleAxesIndependently];
180  [button setImage:searchButtonImage];
181  [button setAutoresizingMask:CPViewMaxXMargin];
182 }
183 
188 - (void)setCancelButton:(CPButton)button
189 {
190  if (button != _cancelButton)
191  {
192  [_cancelButton removeFromSuperview];
193  _cancelButton = button;
194 
195  [_cancelButton setFrame:[self cancelButtonRectForBounds:[self bounds]]];
196  [_cancelButton setAutoresizingMask:CPViewMinXMargin];
197  [_cancelButton setTarget:self];
198  [_cancelButton setAction:@selector(cancelOperation:)];
199  [_cancelButton setButtonType:CPMomentaryChangeButton];
200  [self _updateCancelButtonVisibility];
201  [self addSubview:_cancelButton];
202  }
203 }
204 
209 - (CPButton)cancelButton
210 {
211  return _cancelButton;
212 }
213 
218 - (void)resetCancelButton
219 {
220  var button = [self cancelButton];
221  [button setBordered:NO];
222  [button setImageScaling:CPImageScaleAxesIndependently];
223  [button setImage:[self valueForThemeAttribute:@"image-cancel"]];
224  [button setAlternateImage:[self valueForThemeAttribute:@"image-cancel-pressed"]];
225  [button setAutoresizingMask:CPViewMinXMargin];
226  [button setTarget:self];
227  [button setAction:@selector(cancelOperation:)];
228 }
229 
230 // Custom Layout
237 - (CGRect)searchTextRectForBounds:(CGRect)rect
238 {
239  var leftOffset = 0,
240  width = CGRectGetWidth(rect),
241  bounds = [self bounds];
242 
243  if (_searchButton)
244  {
245  var searchBounds = [self searchButtonRectForBounds:bounds];
246  leftOffset = CGRectGetMaxX(searchBounds) + 2;
247  }
248 
249  if (_cancelButton)
250  {
251  var cancelRect = [self cancelButtonRectForBounds:bounds];
252  width = CGRectGetMinX(cancelRect) - leftOffset;
253  }
254 
255  return CGRectMake(leftOffset, CGRectGetMinY(rect), width, CGRectGetHeight(rect));
256 }
257 
263 - (CGRect)searchButtonRectForBounds:(CGRect)rect
264 {
265  var size = [[self currentValueForThemeAttribute:@"image-search"] size] || CGSizeMakeZero(),
266  inset = [self currentValueForThemeAttribute:@"image-search-inset"];
267 
268  return CGRectMake(inset.left - inset.right, inset.top - inset.bottom + (CGRectGetHeight(rect) - size.height) / 2, size.width, size.height);
269 }
270 
276 - (CGRect)cancelButtonRectForBounds:(CGRect)rect
277 {
278  var size = [[self currentValueForThemeAttribute:@"image-cancel"] size] || CGSizeMakeZero(),
279  inset = [self currentValueForThemeAttribute:@"image-cancel-inset"];
280 
281  return CGRectMake(CGRectGetWidth(rect) - size.width + inset.left - inset.right, inset.top - inset.bottom + (CGRectGetHeight(rect) - size.width) / 2, size.height, size.height);
282 }
283 
284 // Managing Menu Templates
289 - (CPMenu)searchMenuTemplate
290 {
291  return _searchMenuTemplate;
292 }
293 
299 - (void)setSearchMenuTemplate:(CPMenu)aMenu
300 {
301  _searchMenuTemplate = aMenu;
302 
303  [self resetSearchButton];
304  [self _loadRecentSearchList];
305  [self _updateSearchMenu];
306 }
307 
308 // Managing Search Modes
313 - (BOOL)sendsWholeSearchString
314 {
315  return _sendsWholeSearchString;
316 }
317 
322 - (void)setSendsWholeSearchString:(BOOL)flag
323 {
324  _sendsWholeSearchString = flag;
325 }
326 
331 - (BOOL)sendsSearchStringImmediately
332 {
333  return _sendsSearchStringImmediately;
334 }
335 
340 - (void)setSendsSearchStringImmediately:(BOOL)flag
341 {
342  _sendsSearchStringImmediately = flag;
343 }
344 
345 // Managing Recent Search Strings
350 - (int)maximumRecents
351 {
352  return _maximumRecents;
353 }
354 
359 - (void)setMaximumRecents:(int)max
360 {
361  if (max > 254)
362  max = 254;
363  else if (max < 0)
364  max = 10;
365 
366  _maximumRecents = max;
367 }
368 
373 - (CPArray)recentSearches
374 {
375  return _recentSearches;
376 }
377 
383 - (void)setRecentSearches:(CPArray)searches
384 {
385  var max = MIN([self maximumRecents], [searches count]),
386  searches = [searches subarrayWithRange:CPMakeRange(0, max)];
387 
388  _recentSearches = searches;
389  [self _autosaveRecentSearchList];
390 }
391 
396 - (CPString)recentsAutosaveName
397 {
398  return _recentsAutosaveName;
399 }
400 
405 - (void)setRecentsAutosaveName:(CPString)name
406 {
407  if (_recentsAutosaveName != nil)
408  [self _deregisterForAutosaveNotification];
409 
410  _recentsAutosaveName = name;
411 
412  if (_recentsAutosaveName != nil)
413  [self _registerForAutosaveNotification];
414 }
415 
416 // Private methods and subclassing
417 
418 - (CGRect)contentRectForBounds:(CGRect)bounds
419 {
420  var superbounds = [super contentRectForBounds:bounds];
421  return [self searchTextRectForBounds:superbounds];
422 }
423 
424 + (double)_keyboardDelayForPartialSearchString:(CPString)string
425 {
426  return (6 - MIN([string length], 4)) / 10;
427 }
428 
430 {
431  return _searchMenu;
432 }
433 
434 - (BOOL)isOpaque
435 {
436  return [super isOpaque] && [_cancelButton isOpaque] && [_searchButton isOpaque];
437 }
438 
439 - (void)_updateCancelButtonVisibility
440 {
441  [_cancelButton setHidden:([[self stringValue] length] === 0)];
442 }
443 
444 - (void)_searchFieldTextDidChange:(CPNotification)aNotification
445 {
446  if (![self sendsWholeSearchString])
447  {
448  if ([self sendsSearchStringImmediately])
449  [self _sendPartialString];
450  else
451  {
452  [_partialStringTimer invalidate];
453  var timeInterval = [CPSearchField _keyboardDelayForPartialSearchString:[self stringValue]];
454 
455  _partialStringTimer = [CPTimer scheduledTimerWithTimeInterval:timeInterval
456  target:self
457  selector:@selector(_sendPartialString)
458  userInfo:nil
459  repeats:NO];
460  }
461  }
462 
463  [self _updateCancelButtonVisibility];
464 }
465 
466 - (void)_sendAction:(id)sender
467 {
468  [self sendAction:[self action] to:[self target]];
469 }
470 
471 - (BOOL)sendAction:(SEL)anAction to:(id)anObject
472 {
473  [super sendAction:anAction to:anObject];
474 
475  [_partialStringTimer invalidate];
476 
477  [self _addStringToRecentSearches:[self stringValue]];
478  [self _updateCancelButtonVisibility];
479 }
480 
481 - (void)_addStringToRecentSearches:(CPString)string
482 {
483  if (string === nil || string === @"" || [_recentSearches containsObject:string])
484  return;
485 
486  var searches = [CPMutableArray arrayWithArray:_recentSearches];
487  [searches addObject:string];
488  [self setRecentSearches:searches];
489  [self _updateSearchMenu];
490 }
491 
492 - (CPView)hitTest:(CGPoint)aPoint
493 {
494  // Make sure a hit anywhere within the search field returns the search field itself
495  if (CGRectContainsPoint([self frame], aPoint))
496  return self;
497  else
498  return nil;
499 }
500 
501 - (BOOL)resignFirstResponder
502 {
503  return _canResignFirstResponder && [super resignFirstResponder];
504 }
505 
506 - (void)mouseDown:(CPEvent)anEvent
507 {
508  var location = [anEvent locationInWindow],
509  point = [self convertPoint:location fromView:nil];
510 
511  if (CGRectContainsPoint([self searchButtonRectForBounds:[self bounds]], point))
512  {
513  if (_searchMenuTemplate == nil)
514  {
515  if ([_searchButton target] && [_searchButton action])
516  [_searchButton mouseDown:anEvent];
517  else
518  [self _sendAction:self];
519  }
520  else
521  [self _showMenu];
522  }
523  else if (CGRectContainsPoint([self cancelButtonRectForBounds:[self bounds]], point))
524  [_cancelButton mouseDown:anEvent];
525  else
526  [super mouseDown:anEvent];
527 }
528 
562 - (CPMenu)defaultSearchMenuTemplate
563 {
564  var template = [[CPMenu alloc] init],
565  item;
566 
567  item = [[CPMenuItem alloc] initWithTitle:@"Recent Searches"
568  action:nil
569  keyEquivalent:@""];
570  [item setTag:CPSearchFieldRecentsTitleMenuItemTag];
571  [item setEnabled:NO];
572  [template addItem:item];
573 
574  item = [[CPMenuItem alloc] initWithTitle:@"Recent search item"
575  action:@selector(_searchFieldSearch:)
576  keyEquivalent:@""];
577  [item setTag:CPSearchFieldRecentsMenuItemTag];
578  [item setTarget:self];
579  [template addItem:item];
580 
581  item = [[CPMenuItem alloc] initWithTitle:@"Clear Recent Searches"
582  action:@selector(_searchFieldClearRecents:)
583  keyEquivalent:@""];
584  [item setTag:CPSearchFieldClearRecentsMenuItemTag];
585  [item setTarget:self];
586  [template addItem:item];
587 
588  item = [[CPMenuItem alloc] initWithTitle:@"No Recent Searches"
589  action:nil
590  keyEquivalent:@""];
591  [item setTag:CPSearchFieldNoRecentsMenuItemTag];
592  [item setEnabled:NO];
593  [template addItem:item];
594 
595  return template;
596 }
597 
598 - (void)_updateSearchMenu
599 {
600  if (_searchMenuTemplate === nil)
601  return;
602 
603  var menu = [[CPMenu alloc] init],
604  countOfRecents = [_recentSearches count],
605  numberOfItems = [_searchMenuTemplate numberOfItems];
606 
607  for (var i = 0; i < numberOfItems; i++)
608  {
609  var item = [[_searchMenuTemplate itemAtIndex:i] copy];
610 
611  switch ([item tag])
612  {
614  if (countOfRecents === 0)
615  continue;
616 
617  if ([menu numberOfItems] > 0)
618  [self _addSeparatorToMenu:menu];
619  break;
620 
622  {
623  var itemAction = @selector(_searchFieldSearch:);
624 
625  for (var recentIndex = 0; recentIndex < countOfRecents; ++recentIndex)
626  {
627  // RECENT_SEARCH_PREFIX is a hack until CPMenuItem -setIndentationLevel works
628  var recentItem = [[CPMenuItem alloc] initWithTitle:RECENT_SEARCH_PREFIX + [_recentSearches objectAtIndex:recentIndex]
629  action:itemAction
630  keyEquivalent:[item keyEquivalent]];
631  [item setTarget:self];
632  [menu addItem:recentItem];
633  }
634 
635  continue;
636  }
637 
639  if (countOfRecents === 0)
640  continue;
641 
642  if ([menu numberOfItems] > 0)
643  [self _addSeparatorToMenu:menu];
644 
645  [item setAction:@selector(_searchFieldClearRecents:)];
646  [item setTarget:self];
647  break;
648 
650  if (countOfRecents !== 0)
651  continue;
652 
653  if ([menu numberOfItems] > 0)
654  [self _addSeparatorToMenu:menu];
655  break;
656  }
657 
658  [item setEnabled:([item isEnabled] && [item action] != nil && [item target] != nil)];
659  [menu addItem:item];
660  }
661 
662  [menu setDelegate:self];
663 
664  _searchMenu = menu;
665 }
666 
667 - (void)_addSeparatorToMenu:(CPMenu)aMenu
668 {
669  var separator = [CPMenuItem separatorItem];
670  [separator setEnabled:NO];
671  [aMenu addItem:separator];
672 }
673 
674 - (void)menuWillOpen:(CPMenu)menu
675 {
676  _canResignFirstResponder = NO;
677 }
678 
679 - (void)menuDidClose:(CPMenu)menu
680 {
681  _canResignFirstResponder = YES;
682 
683  [self becomeFirstResponder];
684 }
685 
686 - (void)_showMenu
687 {
688  if (_searchMenu === nil || [_searchMenu numberOfItems] === 0 || ![self isEnabled])
689  return;
690 
691  var aFrame = [[self superview] convertRect:[self frame] toView:nil],
692  location = CGPointMake(aFrame.origin.x + 10, aFrame.origin.y + aFrame.size.height - 4);
693 
694  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];
695 
696  [self selectAll:nil];
697  [CPMenu popUpContextMenu:_searchMenu withEvent:anEvent forView:self];
698 }
699 
700 - (void)_sendPartialString
701 {
702  [super sendAction:[self action] to:[self target]];
703  [_partialStringTimer invalidate];
704 }
705 
706 - (void)cancelOperation:(id)sender
707 {
708  [self setObjectValue:@""];
709  [self textDidChange:[CPNotification notificationWithName:CPControlTextDidChangeNotification object:self userInfo:nil]];
710 
711  [self _updateCancelButtonVisibility];
712 }
713 
714 - (void)_searchFieldSearch:(id)sender
715 {
716  var searchString = [[sender title] substringFromIndex:[RECENT_SEARCH_PREFIX length]];
717 
718  if ([sender tag] != CPSearchFieldRecentsMenuItemTag)
719  [self _addStringToRecentSearches:searchString];
720 
721  [self setObjectValue:searchString];
722  [self _sendPartialString];
723  [self selectAll:nil];
724 
725  [self _updateCancelButtonVisibility];
726 }
727 
728 - (void)_searchFieldClearRecents:(id)sender
729 {
730  [self setRecentSearches:[CPArray array]];
731  [self _updateSearchMenu];
732  [self setStringValue:@""];
733  [self _updateCancelButtonVisibility];
734  }
735 
736 - (void)_registerForAutosaveNotification
737 {
738  [[CPNotificationCenter defaultCenter] addObserver:self selector:@selector(_updateAutosavedRecents:) name:CPAutosavedRecentsChangedNotification object:_recentsAutosaveName];
739 }
740 
741 - (void)_deregisterForAutosaveNotification
742 {
743  [[CPNotificationCenter defaultCenter] removeObserver:self name:CPAutosavedRecentsChangedNotification object:_recentsAutosaveName];
744 }
745 
746 - (void)_autosaveRecentSearchList
747 {
748  if (_recentsAutosaveName != nil)
749  [[CPNotificationCenter defaultCenter] postNotificationName:CPAutosavedRecentsChangedNotification object:_recentsAutosaveName];
750 }
751 
752 - (void)_updateAutosavedRecents:(id)notification
753 {
754  var name = [notification object];
755  [[CPUserDefaults standardUserDefaults] setObject:_recentSearches forKey:name];
756 }
757 
758 - (void)_loadRecentSearchList
759 {
760  var name = [self recentsAutosaveName];
761  if (name === nil)
762  return;
763 
764  var list = [[CPUserDefaults standardUserDefaults] objectForKey:name];
765 
766  if (list !== nil)
767  _recentSearches = list;
768 }
769 
770 @end
771 
772 var CPRecentsAutosaveNameKey = @"CPRecentsAutosaveNameKey",
773  CPSendsWholeSearchStringKey = @"CPSendsWholeSearchStringKey",
774  CPSendsSearchStringImmediatelyKey = @"CPSendsSearchStringImmediatelyKey",
775  CPMaximumRecentsKey = @"CPMaximumRecentsKey",
776  CPSearchMenuTemplateKey = @"CPSearchMenuTemplateKey";
777 
778 @implementation CPSearchField (CPCoding)
779 
780 - (void)encodeWithCoder:(CPCoder)coder
781 {
782  [_searchButton removeFromSuperview];
783  [_cancelButton removeFromSuperview];
784 
785  [super encodeWithCoder:coder];
786 
787  if (_searchButton)
788  [self addSubview:_searchButton];
789  if (_cancelButton)
790  [self addSubview:_cancelButton];
791 
792  [coder encodeBool:_sendsWholeSearchString forKey:CPSendsWholeSearchStringKey];
793  [coder encodeBool:_sendsSearchStringImmediately forKey:CPSendsSearchStringImmediatelyKey];
794  [coder encodeInt:_maximumRecents forKey:CPMaximumRecentsKey];
795 
796  if (_recentsAutosaveName)
797  [coder encodeObject:_recentsAutosaveName forKey:CPRecentsAutosaveNameKey];
798 
799  if (_searchMenuTemplate)
800  [coder encodeObject:_searchMenuTemplate forKey:CPSearchMenuTemplateKey];
801 }
802 
803 - (id)initWithCoder:(CPCoder)coder
804 {
805  if (self = [super initWithCoder:coder])
806  {
807  [self setRecentsAutosaveName:[coder decodeObjectForKey:CPRecentsAutosaveNameKey]];
808  _sendsWholeSearchString = [coder decodeBoolForKey:CPSendsWholeSearchStringKey];
809  _sendsSearchStringImmediately = [coder decodeBoolForKey:CPSendsSearchStringImmediatelyKey];
810  _maximumRecents = [coder decodeIntForKey:CPMaximumRecentsKey];
811 
812  var template = [coder decodeObjectForKey:CPSearchMenuTemplateKey];
813 
814  if (template)
815  [self setSearchMenuTemplate:template];
816 
817  [self _init];
818  }
819 
820  return self;
821 }
822 
823 @end