API  0.9.6
 All Classes Files Functions Variables 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 
28 
33 
37 
38 var CPAutosavedRecentsChangedNotification = @"CPAutosavedRecentsChangedNotification";
39 
41 
49 @implementation CPSearchField : CPTextField
50 {
51  CPButton _searchButton;
52  CPButton _cancelButton;
53  CPMenu _searchMenuTemplate;
54  CPMenu _searchMenu;
55 
56  CPString _recentsAutosaveName;
57  CPArray _recentSearches;
58 
59  int _maximumRecents;
60  BOOL _sendsWholeSearchString;
61  BOOL _sendsSearchStringImmediately;
62  BOOL _canResignFirstResponder;
63  CPTimer _partialStringTimer;
64 }
65 
66 + (CPString)defaultThemeClass
67 {
68  return @"searchfield"
69 }
70 
71 + (void)initialize
72 {
73  if (self !== [CPSearchField class])
74  return;
75 
76  var bundle = [CPBundle bundleForClass:self];
77  CPSearchFieldSearchImage = [[CPImage alloc] initWithContentsOfFile:[bundle pathForResource:@"CPSearchField/CPSearchFieldSearch.png"] size:_CGSizeMake(SEARCH_BUTTON_DEFAULT_WIDTH, BUTTON_DEFAULT_HEIGHT)];
78  CPSearchFieldFindImage = [[CPImage alloc] initWithContentsOfFile:[bundle pathForResource:@"CPSearchField/CPSearchFieldFind.png"] size:_CGSizeMake(SEARCH_BUTTON_DEFAULT_WIDTH, BUTTON_DEFAULT_HEIGHT)];
79  CPSearchFieldCancelImage = [[CPImage alloc] initWithContentsOfFile:[bundle pathForResource:@"CPSearchField/CPSearchFieldCancel.png"] size:_CGSizeMake(CANCEL_BUTTON_DEFAULT_WIDTH, BUTTON_DEFAULT_HEIGHT)];
80  CPSearchFieldCancelPressedImage = [[CPImage alloc] initWithContentsOfFile:[bundle pathForResource:@"CPSearchField/CPSearchFieldCancelPressed.png"] size:_CGSizeMake(CANCEL_BUTTON_DEFAULT_WIDTH, BUTTON_DEFAULT_HEIGHT)];
81 }
82 
83 - (id)initWithFrame:(CGRect)frame
84 {
85  if (self = [super initWithFrame:frame])
86  {
87  _maximumRecents = 10;
88  _sendsWholeSearchString = NO;
89  _sendsSearchStringImmediately = NO;
90  _recentsAutosaveName = nil;
91 
92  [self _init];
93 #if PLATFORM(DOM)
94  _cancelButton._DOMElement.style.cursor = "default";
95  _searchButton._DOMElement.style.cursor = "default";
96 #endif
97  }
98 
99  return self;
100 }
101 
102 - (void)_init
103 {
104  _recentSearches = [CPArray array];
105 
106  [self setBezeled:YES];
107  [self setBezelStyle:CPTextFieldRoundedBezel];
108  [self setBordered:YES];
109  [self setEditable:YES];
110  [self setContinuous:YES];
111 
112  var bounds = [self bounds],
113  cancelButton = [[CPButton alloc] initWithFrame:[self cancelButtonRectForBounds:bounds]],
114  searchButton = [[CPButton alloc] initWithFrame:[self searchButtonRectForBounds:bounds]];
115 
116  [self setCancelButton:cancelButton];
117  [self resetCancelButton];
118 
119  [self setSearchButton:searchButton];
120  [self resetSearchButton];
121 
122  _canResignFirstResponder = YES;
123 }
124 
125 - (void)viewWillMoveToSuperview:(CPView)aView
126 {
127  [super viewWillMoveToSuperview:aView];
128 
129  // First we remove any observer that may have been in place to avoid memory leakage.
130  [[CPNotificationCenter defaultCenter] removeObserver:self name:CPControlTextDidChangeNotification object:self];
131 
132  // Register the observe here if we need to.
133  if (aView)
134  [[CPNotificationCenter defaultCenter] addObserver:self selector:@selector(_searchFieldTextDidChange:) name:CPControlTextDidChangeNotification object:self];
135 }
136 
137 // Managing Buttons
142 - (void)setSearchButton:(CPButton)button
143 {
144  if (button != _searchButton)
145  {
146  [_searchButton removeFromSuperview];
147  _searchButton = button;
148 
149  [_searchButton setFrame:[self searchButtonRectForBounds:[self bounds]]];
150  [_searchButton setAutoresizingMask:CPViewMaxXMargin];
151  [self addSubview:_searchButton];
152  }
153 }
154 
159 - (CPButton)searchButton
160 {
161  return _searchButton;
162 }
163 
168 - (void)resetSearchButton
169 {
170  var button = [self searchButton],
171  searchButtonImage = (_searchMenuTemplate === nil) ? CPSearchFieldSearchImage : CPSearchFieldFindImage;
172 
173  [button setBordered:NO];
174  [button setImageScaling:CPImageScaleAxesIndependently];
175  [button setImage:searchButtonImage];
176  [button setAutoresizingMask:CPViewMaxXMargin];
177 }
178 
183 - (void)setCancelButton:(CPButton)button
184 {
185  if (button != _cancelButton)
186  {
187  [_cancelButton removeFromSuperview];
188  _cancelButton = button;
189 
190  [_cancelButton setFrame:[self cancelButtonRectForBounds:[self bounds]]];
191  [_cancelButton setAutoresizingMask:CPViewMinXMargin];
192  [_cancelButton setTarget:self];
193  [_cancelButton setAction:@selector(cancelOperation:)];
194  [self _updateCancelButtonVisibility];
195  [self addSubview:_cancelButton];
196  }
197 }
198 
203 - (CPButton)cancelButton
204 {
205  return _cancelButton;
206 }
207 
212 - (void)resetCancelButton
213 {
214  var button = [self cancelButton];
215  [button setBordered:NO];
216  [button setImageScaling:CPImageScaleAxesIndependently];
217  [button setImage:CPSearchFieldCancelImage];
218  [button setAlternateImage:CPSearchFieldCancelPressedImage];
219  [button setAutoresizingMask:CPViewMinXMargin];
220  [button setTarget:self];
221  [button setAction:@selector(cancelOperation:)];
222 }
223 
224 // Custom Layout
231 - (CGRect)searchTextRectForBounds:(CGRect)rect
232 {
233  var leftOffset = 0,
234  width = _CGRectGetWidth(rect),
235  bounds = [self bounds];
236 
237  if (_searchButton)
238  {
239  var searchBounds = [self searchButtonRectForBounds:bounds];
240  leftOffset = _CGRectGetMaxX(searchBounds) + 2;
241  }
242 
243  if (_cancelButton)
244  {
245  var cancelRect = [self cancelButtonRectForBounds:bounds];
246  width = _CGRectGetMinX(cancelRect) - leftOffset;
247  }
248 
249  return _CGRectMake(leftOffset, _CGRectGetMinY(rect), width, _CGRectGetHeight(rect));
250 }
251 
257 - (CGRect)searchButtonRectForBounds:(CGRect)rect
258 {
259  return _CGRectMake(5, (_CGRectGetHeight(rect) - BUTTON_DEFAULT_HEIGHT) / 2, SEARCH_BUTTON_DEFAULT_WIDTH, BUTTON_DEFAULT_HEIGHT);
260 }
261 
267 - (CGRect)cancelButtonRectForBounds:(CGRect)rect
268 {
269  return _CGRectMake(_CGRectGetWidth(rect) - CANCEL_BUTTON_DEFAULT_WIDTH - 5, (_CGRectGetHeight(rect) - CANCEL_BUTTON_DEFAULT_WIDTH) / 2, BUTTON_DEFAULT_HEIGHT, BUTTON_DEFAULT_HEIGHT);
270 }
271 
272 // Managing Menu Templates
277 - (CPMenu)searchMenuTemplate
278 {
279  return _searchMenuTemplate;
280 }
281 
287 - (void)setSearchMenuTemplate:(CPMenu)aMenu
288 {
289  _searchMenuTemplate = aMenu;
290 
291  [self resetSearchButton];
292  [self _loadRecentSearchList];
293  [self _updateSearchMenu];
294 }
295 
296 // Managing Search Modes
301 - (BOOL)sendsWholeSearchString
302 {
303  return _sendsWholeSearchString;
304 }
305 
310 - (void)setSendsWholeSearchString:(BOOL)flag
311 {
312  _sendsWholeSearchString = flag;
313 }
314 
319 - (BOOL)sendsSearchStringImmediately
320 {
321  return _sendsSearchStringImmediately;
322 }
323 
328 - (void)setSendsSearchStringImmediately:(BOOL)flag
329 {
330  _sendsSearchStringImmediately = flag;
331 }
332 
333 // Managing Recent Search Strings
338 - (int)maximumRecents
339 {
340  return _maximumRecents;
341 }
342 
347 - (void)setMaximumRecents:(int)max
348 {
349  if (max > 254)
350  max = 254;
351  else if (max < 0)
352  max = 10;
353 
354  _maximumRecents = max;
355 }
356 
361 - (CPArray)recentSearches
362 {
363  return _recentSearches;
364 }
365 
371 - (void)setRecentSearches:(CPArray)searches
372 {
373  var max = MIN([self maximumRecents], [searches count]),
374  searches = [searches subarrayWithRange:CPMakeRange(0, max)];
375 
376  _recentSearches = searches;
377  [self _autosaveRecentSearchList];
378 }
379 
384 - (CPString)recentsAutosaveName
385 {
386  return _recentsAutosaveName;
387 }
388 
393 - (void)setRecentsAutosaveName:(CPString)name
394 {
395  if (_recentsAutosaveName != nil)
396  [self _deregisterForAutosaveNotification];
397 
398  _recentsAutosaveName = name;
399 
400  if (_recentsAutosaveName != nil)
401  [self _registerForAutosaveNotification];
402 }
403 
404 // Private methods and subclassing
405 
406 - (CGRect)contentRectForBounds:(CGRect)bounds
407 {
408  var superbounds = [super contentRectForBounds:bounds];
409  return [self searchTextRectForBounds:superbounds];
410 }
411 
412 + (double)_keyboardDelayForPartialSearchString:(CPString)string
413 {
414  return (6 - MIN([string length], 4)) / 10;
415 }
416 
418 {
419  return _searchMenu;
420 }
421 
422 - (BOOL)isOpaque
423 {
424  return [super isOpaque] && [_cancelButton isOpaque] && [_searchButton isOpaque];
425 }
426 
427 - (void)_updateCancelButtonVisibility
428 {
429  [_cancelButton setHidden:([[self stringValue] length] === 0)];
430 }
431 
432 - (void)_searchFieldTextDidChange:(CPNotification)aNotification
433 {
434  if (![self sendsWholeSearchString])
435  {
436  if ([self sendsSearchStringImmediately])
437  [self _sendPartialString];
438  else
439  {
440  [_partialStringTimer invalidate];
441  var timeInterval = [CPSearchField _keyboardDelayForPartialSearchString:[self stringValue]];
442 
443  _partialStringTimer = [CPTimer scheduledTimerWithTimeInterval:timeInterval
444  target:self
445  selector:@selector(_sendPartialString)
446  userInfo:nil
447  repeats:NO];
448  }
449  }
450 
451  [self _updateCancelButtonVisibility];
452 }
453 
454 - (void)_sendAction:(id)sender
455 {
456  [self sendAction:[self action] to:[self target]];
457 }
458 
459 - (void)sendAction:(SEL)anAction to:(id)anObject
460 {
461  [super sendAction:anAction to:anObject];
462 
463  [_partialStringTimer invalidate];
464 
465  [self _addStringToRecentSearches:[self stringValue]];
466  [self _updateCancelButtonVisibility];
467 }
468 
469 - (void)_addStringToRecentSearches:(CPString)string
470 {
471  if (string === nil || string === @"" || [_recentSearches containsObject:string])
472  return;
473 
474  var searches = [CPMutableArray arrayWithArray:_recentSearches];
475  [searches addObject:string];
476  [self setRecentSearches:searches];
477  [self _updateSearchMenu];
478 }
479 
480 - (CPView)hitTest:(CGPoint)aPoint
481 {
482  // Make sure a hit anywhere within the search field returns the search field itself
483  if (_CGRectContainsPoint([self frame], aPoint))
484  return self;
485  else
486  return nil;
487 }
488 
489 - (BOOL)resignFirstResponder
490 {
491  return _canResignFirstResponder && [super resignFirstResponder];
492 }
493 
494 - (void)mouseDown:(CPEvent)anEvent
495 {
496  var location = [anEvent locationInWindow],
497  point = [self convertPoint:location fromView:nil];
498 
499  if (_CGRectContainsPoint([self searchButtonRectForBounds:[self bounds]], point))
500  {
501  if (_searchMenuTemplate == nil)
502  {
503  if ([_searchButton target] && [_searchButton action])
504  [_searchButton mouseDown:anEvent];
505  else
506  [self _sendAction:self];
507  }
508  else
509  [self _showMenu];
510  }
511  else if (_CGRectContainsPoint([self cancelButtonRectForBounds:[self bounds]], point))
512  [_cancelButton mouseDown:anEvent];
513  else
514  [super mouseDown:anEvent];
515 }
516 
550 - (CPMenu)defaultSearchMenuTemplate
551 {
552  var template = [[CPMenu alloc] init],
553  item;
554 
555  item = [[CPMenuItem alloc] initWithTitle:@"Recent Searches"
556  action:nil
557  keyEquivalent:@""];
558  [item setTag:CPSearchFieldRecentsTitleMenuItemTag];
559  [item setEnabled:NO];
560  [template addItem:item];
561 
562  item = [[CPMenuItem alloc] initWithTitle:@"Recent search item"
563  action:@selector(_searchFieldSearch:)
564  keyEquivalent:@""];
565  [item setTag:CPSearchFieldRecentsMenuItemTag];
566  [item setTarget:self];
567  [template addItem:item];
568 
569  item = [[CPMenuItem alloc] initWithTitle:@"Clear Recent Searches"
570  action:@selector(_searchFieldClearRecents:)
571  keyEquivalent:@""];
572  [item setTag:CPSearchFieldClearRecentsMenuItemTag];
573  [item setTarget:self];
574  [template addItem:item];
575 
576  item = [[CPMenuItem alloc] initWithTitle:@"No Recent Searches"
577  action:nil
578  keyEquivalent:@""];
579  [item setTag:CPSearchFieldNoRecentsMenuItemTag];
580  [item setEnabled:NO];
581  [template addItem:item];
582 
583  return template;
584 }
585 
586 - (void)_updateSearchMenu
587 {
588  if (_searchMenuTemplate === nil)
589  return;
590 
591  var menu = [[CPMenu alloc] init],
592  countOfRecents = [_recentSearches count],
593  numberOfItems = [_searchMenuTemplate numberOfItems];
594 
595  for (var i = 0; i < numberOfItems; i++)
596  {
597  var item = [[_searchMenuTemplate itemAtIndex:i] copy];
598 
599  switch ([item tag])
600  {
602  if (countOfRecents === 0)
603  continue;
604 
605  if ([menu numberOfItems] > 0)
606  [self _addSeparatorToMenu:menu];
607  break;
608 
610  {
611  var itemAction = @selector(_searchFieldSearch:);
612 
613  for (var recentIndex = 0; recentIndex < countOfRecents; ++recentIndex)
614  {
615  // RECENT_SEARCH_PREFIX is a hack until CPMenuItem -setIndentationLevel works
616  var recentItem = [[CPMenuItem alloc] initWithTitle:RECENT_SEARCH_PREFIX + [_recentSearches objectAtIndex:recentIndex]
617  action:itemAction
618  keyEquivalent:[item keyEquivalent]];
619  [item setTarget:self];
620  [menu addItem:recentItem];
621  }
622 
623  continue;
624  }
625 
627  if (countOfRecents === 0)
628  continue;
629 
630  if ([menu numberOfItems] > 0)
631  [self _addSeparatorToMenu:menu];
632 
633  [item setAction:@selector(_searchFieldClearRecents:)];
634  [item setTarget:self];
635  break;
636 
638  if (countOfRecents !== 0)
639  continue;
640 
641  if ([menu numberOfItems] > 0)
642  [self _addSeparatorToMenu:menu];
643  break;
644  }
645 
646  [item setEnabled:([item isEnabled] && [item action] != nil && [item target] != nil)];
647  [menu addItem:item];
648  }
649 
650  [menu setDelegate:self];
651 
652  _searchMenu = menu;
653 }
654 
655 - (void)_addSeparatorToMenu:(CPMenu)aMenu
656 {
657  var separator = [CPMenuItem separatorItem];
658  [separator setEnabled:NO];
659  [aMenu addItem:separator];
660 }
661 
662 - (void)menuWillOpen:(CPMenu)menu
663 {
664  _canResignFirstResponder = NO;
665 }
666 
667 - (void)menuDidClose:(CPMenu)menu
668 {
669  _canResignFirstResponder = YES;
670 
671  [self becomeFirstResponder];
672 }
673 
674 - (void)_showMenu
675 {
676  if (_searchMenu === nil || [_searchMenu numberOfItems] === 0 || ![self isEnabled])
677  return;
678 
679  var aFrame = [[self superview] convertRect:[self frame] toView:nil],
680  location = CPMakePoint(aFrame.origin.x + 10, aFrame.origin.y + aFrame.size.height - 4);
681 
682  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];
683 
684  [self selectAll:nil];
685  [CPMenu popUpContextMenu:_searchMenu withEvent:anEvent forView:self];
686 }
687 
688 - (void)_sendPartialString
689 {
690  [super sendAction:[self action] to:[self target]];
691  [_partialStringTimer invalidate];
692 }
693 
694 - (void)cancelOperation:(id)sender
695 {
696  [self setObjectValue:@""];
697  [self textDidChange:[CPNotification notificationWithName:CPControlTextDidChangeNotification object:self userInfo:nil]];
698 
699  [self _updateCancelButtonVisibility];
700 }
701 
702 - (void)_searchFieldSearch:(id)sender
703 {
704  var searchString = [[sender title] substringFromIndex:[RECENT_SEARCH_PREFIX length]];
705 
706  if ([sender tag] != CPSearchFieldRecentsMenuItemTag)
707  [self _addStringToRecentSearches:searchString];
708 
709  [self setObjectValue:searchString];
710  [self _sendPartialString];
711  [self selectAll:nil];
712 
713  [self _updateCancelButtonVisibility];
714 }
715 
716 - (void)_searchFieldClearRecents:(id)sender
717 {
718  [self setRecentSearches:[CPArray array]];
719  [self _updateSearchMenu];
720  [self setStringValue:@""];
721  [self _updateCancelButtonVisibility];
722  }
723 
724 - (void)_registerForAutosaveNotification
725 {
726  [[CPNotificationCenter defaultCenter] addObserver:self selector:@selector(_updateAutosavedRecents:) name:CPAutosavedRecentsChangedNotification object:_recentsAutosaveName];
727 }
728 
729 - (void)_deregisterForAutosaveNotification
730 {
731  [[CPNotificationCenter defaultCenter] removeObserver:self name:CPAutosavedRecentsChangedNotification object:_recentsAutosaveName];
732 }
733 
734 - (void)_autosaveRecentSearchList
735 {
736  if (_recentsAutosaveName != nil)
737  [[CPNotificationCenter defaultCenter] postNotificationName:CPAutosavedRecentsChangedNotification object:_recentsAutosaveName];
738 }
739 
740 - (void)_updateAutosavedRecents:(id)notification
741 {
742  var name = [notification object];
743  [[CPUserDefaults standardUserDefaults] setObject:_recentSearches forKey:name];
744 }
745 
746 - (void)_loadRecentSearchList
747 {
748  var name = [self recentsAutosaveName];
749  if (name === nil)
750  return;
751 
752  var list = [[CPUserDefaults standardUserDefaults] objectForKey:name];
753 
754  if (list !== nil)
755  _recentSearches = list;
756 }
757 
758 @end
759 
760 var CPRecentsAutosaveNameKey = @"CPRecentsAutosaveNameKey",
761  CPSendsWholeSearchStringKey = @"CPSendsWholeSearchStringKey",
762  CPSendsSearchStringImmediatelyKey = @"CPSendsSearchStringImmediatelyKey",
763  CPMaximumRecentsKey = @"CPMaximumRecentsKey",
764  CPSearchMenuTemplateKey = @"CPSearchMenuTemplateKey";
765 
766 @implementation CPSearchField (CPCoding)
767 
768 - (void)encodeWithCoder:(CPCoder)coder
769 {
770  [_searchButton removeFromSuperview];
771  [_cancelButton removeFromSuperview];
772 
773  [super encodeWithCoder:coder];
774 
775  if (_searchButton)
776  [self addSubview:_searchButton];
777  if (_cancelButton)
778  [self addSubview:_cancelButton];
779 
780  [coder encodeBool:_sendsWholeSearchString forKey:CPSendsWholeSearchStringKey];
781  [coder encodeBool:_sendsSearchStringImmediately forKey:CPSendsSearchStringImmediatelyKey];
782  [coder encodeInt:_maximumRecents forKey:CPMaximumRecentsKey];
783 
784  if (_recentsAutosaveName)
785  [coder encodeObject:_recentsAutosaveName forKey:CPRecentsAutosaveNameKey];
786 
787  if (_searchMenuTemplate)
788  [coder encodeObject:_searchMenuTemplate forKey:CPSearchMenuTemplateKey];
789 }
790 
791 - (id)initWithCoder:(CPCoder)coder
792 {
793  if (self = [super initWithCoder:coder])
794  {
795  [self setRecentsAutosaveName:[coder decodeObjectForKey:CPRecentsAutosaveNameKey]];
796  _sendsWholeSearchString = [coder decodeBoolForKey:CPSendsWholeSearchStringKey];
797  _sendsSearchStringImmediately = [coder decodeBoolForKey:CPSendsSearchStringImmediatelyKey];
798  _maximumRecents = [coder decodeIntForKey:CPMaximumRecentsKey];
799 
800  var template = [coder decodeObjectForKey:CPSearchMenuTemplateKey];
801 
802  if (template)
803  [self setSearchMenuTemplate:template];
804 
805  [self _init];
806  }
807 
808  return self;
809 }
810 
811 @end