API  0.9.7
 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 
24 @class CPUserDefaults
25 
26 @global CPApp
27 
28 CPSearchFieldRecentsTitleMenuItemTag = 1000;
29 CPSearchFieldRecentsMenuItemTag = 1001;
30 CPSearchFieldClearRecentsMenuItemTag = 1002;
31 CPSearchFieldNoRecentsMenuItemTag = 1003;
32 
33 var CPAutosavedRecentsChangedNotification = @"CPAutosavedRecentsChangedNotification";
34 
35 var RECENT_SEARCH_PREFIX = @" ";
36 
44 @implementation CPSearchField : CPTextField
45 {
46  CPButton _searchButton;
47  CPButton _cancelButton;
48  CPMenu _searchMenuTemplate;
49  CPMenu _searchMenu;
50 
51  CPString _recentsAutosaveName;
52  CPArray _recentSearches;
53 
54  int _maximumRecents;
55  BOOL _sendsWholeSearchString;
56  BOOL _sendsSearchStringImmediately;
57  BOOL _canResignFirstResponder;
58  CPTimer _partialStringTimer;
59 }
60 
61 + (CPString)defaultThemeClass
62 {
63  return @"searchfield"
64 }
65 
66 + (CPDictionary)themeAttributes
67 {
68  return @{
69  @"image-search": [CPNull null],
70  @"image-find": [CPNull null],
71  @"image-cancel": [CPNull null],
72  @"image-cancel-pressed": [CPNull null]
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 - (void)viewWillMoveToSuperview:(CPView)aView
119 {
120  [super viewWillMoveToSuperview:aView];
121 
122  // First we remove any observer that may have been in place to avoid memory leakage.
123  [[CPNotificationCenter defaultCenter] removeObserver:self name:CPControlTextDidChangeNotification object:self];
124 
125  // Register the observe here if we need to.
126  if (aView)
127  [[CPNotificationCenter defaultCenter] addObserver:self selector:@selector(_searchFieldTextDidChange:) name:CPControlTextDidChangeNotification object:self];
128 }
129 
130 // Managing Buttons
135 - (void)setSearchButton:(CPButton)button
136 {
137  if (button != _searchButton)
138  {
139  [_searchButton removeFromSuperview];
140  _searchButton = button;
141 
142  [_searchButton setFrame:[self searchButtonRectForBounds:[self bounds]]];
143  [_searchButton setAutoresizingMask:CPViewMaxXMargin];
144  [self addSubview:_searchButton];
145  }
146 }
147 
152 - (CPButton)searchButton
153 {
154  return _searchButton;
155 }
156 
161 - (void)resetSearchButton
162 {
163  var button = [self searchButton],
164  searchButtonImage = (_searchMenuTemplate === nil) ? [self valueForThemeAttribute:@"image-search"] : [self valueForThemeAttribute:@"image-find"];
165 
166  [button setBordered:NO];
167  [button setImageScaling:CPImageScaleAxesIndependently];
168  [button setImage:searchButtonImage];
169  [button setAutoresizingMask:CPViewMaxXMargin];
170 }
171 
176 - (void)setCancelButton:(CPButton)button
177 {
178  if (button != _cancelButton)
179  {
180  [_cancelButton removeFromSuperview];
181  _cancelButton = button;
182 
183  [_cancelButton setFrame:[self cancelButtonRectForBounds:[self bounds]]];
184  [_cancelButton setAutoresizingMask:CPViewMinXMargin];
185  [_cancelButton setTarget:self];
186  [_cancelButton setAction:@selector(cancelOperation:)];
187  [_cancelButton setButtonType:CPMomentaryChangeButton];
188  [self _updateCancelButtonVisibility];
189  [self addSubview:_cancelButton];
190  }
191 }
192 
197 - (CPButton)cancelButton
198 {
199  return _cancelButton;
200 }
201 
206 - (void)resetCancelButton
207 {
208  var button = [self cancelButton];
209  [button setBordered:NO];
210  [button setImageScaling:CPImageScaleAxesIndependently];
211  [button setImage:[self valueForThemeAttribute:@"image-cancel"]];
212  [button setAlternateImage:[self valueForThemeAttribute:@"image-cancel-pressed"]];
213  [button setAutoresizingMask:CPViewMinXMargin];
214  [button setTarget:self];
215  [button setAction:@selector(cancelOperation:)];
216 }
217 
218 // Custom Layout
225 - (CGRect)searchTextRectForBounds:(CGRect)rect
226 {
227  var leftOffset = 0,
228  width = CGRectGetWidth(rect),
229  bounds = [self bounds];
230 
231  if (_searchButton)
232  {
233  var searchBounds = [self searchButtonRectForBounds:bounds];
234  leftOffset = CGRectGetMaxX(searchBounds) + 2;
235  }
236 
237  if (_cancelButton)
238  {
239  var cancelRect = [self cancelButtonRectForBounds:bounds];
240  width = CGRectGetMinX(cancelRect) - leftOffset;
241  }
242 
243  return CGRectMake(leftOffset, CGRectGetMinY(rect), width, CGRectGetHeight(rect));
244 }
245 
251 - (CGRect)searchButtonRectForBounds:(CGRect)rect
252 {
253  var size = [[self valueForThemeAttribute:@"image-search"] size] || CGSizeMakeZero();
254 
255  return CGRectMake(5, (CGRectGetHeight(rect) - size.height) / 2, size.width, size.height);
256 }
257 
263 - (CGRect)cancelButtonRectForBounds:(CGRect)rect
264 {
265  var size = [[self valueForThemeAttribute:@"image-cancel"] size] || CGSizeMakeZero();
266 
267  return CGRectMake(CGRectGetWidth(rect) - size.width - 5, (CGRectGetHeight(rect) - size.width) / 2, size.height, size.height);
268 }
269 
270 // Managing Menu Templates
275 - (CPMenu)searchMenuTemplate
276 {
277  return _searchMenuTemplate;
278 }
279 
285 - (void)setSearchMenuTemplate:(CPMenu)aMenu
286 {
287  _searchMenuTemplate = aMenu;
288 
289  [self resetSearchButton];
290  [self _loadRecentSearchList];
291  [self _updateSearchMenu];
292 }
293 
294 // Managing Search Modes
299 - (BOOL)sendsWholeSearchString
300 {
301  return _sendsWholeSearchString;
302 }
303 
308 - (void)setSendsWholeSearchString:(BOOL)flag
309 {
310  _sendsWholeSearchString = flag;
311 }
312 
317 - (BOOL)sendsSearchStringImmediately
318 {
319  return _sendsSearchStringImmediately;
320 }
321 
326 - (void)setSendsSearchStringImmediately:(BOOL)flag
327 {
328  _sendsSearchStringImmediately = flag;
329 }
330 
331 // Managing Recent Search Strings
336 - (int)maximumRecents
337 {
338  return _maximumRecents;
339 }
340 
345 - (void)setMaximumRecents:(int)max
346 {
347  if (max > 254)
348  max = 254;
349  else if (max < 0)
350  max = 10;
351 
352  _maximumRecents = max;
353 }
354 
359 - (CPArray)recentSearches
360 {
361  return _recentSearches;
362 }
363 
369 - (void)setRecentSearches:(CPArray)searches
370 {
371  var max = MIN([self maximumRecents], [searches count]),
372  searches = [searches subarrayWithRange:CPMakeRange(0, max)];
373 
374  _recentSearches = searches;
375  [self _autosaveRecentSearchList];
376 }
377 
382 - (CPString)recentsAutosaveName
383 {
384  return _recentsAutosaveName;
385 }
386 
391 - (void)setRecentsAutosaveName:(CPString)name
392 {
393  if (_recentsAutosaveName != nil)
394  [self _deregisterForAutosaveNotification];
395 
396  _recentsAutosaveName = name;
397 
398  if (_recentsAutosaveName != nil)
399  [self _registerForAutosaveNotification];
400 }
401 
402 // Private methods and subclassing
403 
404 - (CGRect)contentRectForBounds:(CGRect)bounds
405 {
406  var superbounds = [super contentRectForBounds:bounds];
407  return [self searchTextRectForBounds:superbounds];
408 }
409 
410 + (double)_keyboardDelayForPartialSearchString:(CPString)string
411 {
412  return (6 - MIN([string length], 4)) / 10;
413 }
414 
415 - (CPMenu)menu
416 {
417  return _searchMenu;
418 }
419 
420 - (BOOL)isOpaque
421 {
422  return [super isOpaque] && [_cancelButton isOpaque] && [_searchButton isOpaque];
423 }
424 
425 - (void)_updateCancelButtonVisibility
426 {
427  [_cancelButton setHidden:([[self stringValue] length] === 0)];
428 }
429 
430 - (void)_searchFieldTextDidChange:(CPNotification)aNotification
431 {
432  if (![self sendsWholeSearchString])
433  {
434  if ([self sendsSearchStringImmediately])
435  [self _sendPartialString];
436  else
437  {
438  [_partialStringTimer invalidate];
439  var timeInterval = [CPSearchField _keyboardDelayForPartialSearchString:[self stringValue]];
440 
441  _partialStringTimer = [CPTimer scheduledTimerWithTimeInterval:timeInterval
442  target:self
443  selector:@selector(_sendPartialString)
444  userInfo:nil
445  repeats:NO];
446  }
447  }
448 
449  [self _updateCancelButtonVisibility];
450 }
451 
452 - (void)_sendAction:(id)sender
453 {
454  [self sendAction:[self action] to:[self target]];
455 }
456 
457 - (BOOL)sendAction:(SEL)anAction to:(id)anObject
458 {
459  [super sendAction:anAction to:anObject];
460 
461  [_partialStringTimer invalidate];
462 
463  [self _addStringToRecentSearches:[self stringValue]];
464  [self _updateCancelButtonVisibility];
465 }
466 
467 - (void)_addStringToRecentSearches:(CPString)string
468 {
469  if (string === nil || string === @"" || [_recentSearches containsObject:string])
470  return;
471 
472  var searches = [CPMutableArray arrayWithArray:_recentSearches];
473  [searches addObject:string];
474  [self setRecentSearches:searches];
475  [self _updateSearchMenu];
476 }
477 
478 - (CPView)hitTest:(CGPoint)aPoint
479 {
480  // Make sure a hit anywhere within the search field returns the search field itself
481  if (CGRectContainsPoint([self frame], aPoint))
482  return self;
483  else
484  return nil;
485 }
486 
487 - (BOOL)resignFirstResponder
488 {
489  return _canResignFirstResponder && [super resignFirstResponder];
490 }
491 
492 - (void)mouseDown:(CPEvent)anEvent
493 {
494  var location = [anEvent locationInWindow],
495  point = [self convertPoint:location fromView:nil];
496 
497  if (CGRectContainsPoint([self searchButtonRectForBounds:[self bounds]], point))
498  {
499  if (_searchMenuTemplate == nil)
500  {
501  if ([_searchButton target] && [_searchButton action])
502  [_searchButton mouseDown:anEvent];
503  else
504  [self _sendAction:self];
505  }
506  else
507  [self _showMenu];
508  }
509  else if (CGRectContainsPoint([self cancelButtonRectForBounds:[self bounds]], point))
510  [_cancelButton mouseDown:anEvent];
511  else
512  [super mouseDown:anEvent];
513 }
514 
548 - (CPMenu)defaultSearchMenuTemplate
549 {
550  var template = [[CPMenu alloc] init],
551  item;
552 
553  item = [[CPMenuItem alloc] initWithTitle:@"Recent Searches"
554  action:nil
555  keyEquivalent:@""];
556  [item setTag:CPSearchFieldRecentsTitleMenuItemTag];
557  [item setEnabled:NO];
558  [template addItem:item];
559 
560  item = [[CPMenuItem alloc] initWithTitle:@"Recent search item"
561  action:@selector(_searchFieldSearch:)
562  keyEquivalent:@""];
563  [item setTag:CPSearchFieldRecentsMenuItemTag];
564  [item setTarget:self];
565  [template addItem:item];
566 
567  item = [[CPMenuItem alloc] initWithTitle:@"Clear Recent Searches"
568  action:@selector(_searchFieldClearRecents:)
569  keyEquivalent:@""];
570  [item setTag:CPSearchFieldClearRecentsMenuItemTag];
571  [item setTarget:self];
572  [template addItem:item];
573 
574  item = [[CPMenuItem alloc] initWithTitle:@"No Recent Searches"
575  action:nil
576  keyEquivalent:@""];
577  [item setTag:CPSearchFieldNoRecentsMenuItemTag];
578  [item setEnabled:NO];
579  [template addItem:item];
580 
581  return template;
582 }
583 
584 - (void)_updateSearchMenu
585 {
586  if (_searchMenuTemplate === nil)
587  return;
588 
589  var menu = [[CPMenu alloc] init],
590  countOfRecents = [_recentSearches count],
591  numberOfItems = [_searchMenuTemplate numberOfItems];
592 
593  for (var i = 0; i < numberOfItems; i++)
594  {
595  var item = [[_searchMenuTemplate itemAtIndex:i] copy];
596 
597  switch ([item tag])
598  {
599  case CPSearchFieldRecentsTitleMenuItemTag:
600  if (countOfRecents === 0)
601  continue;
602 
603  if ([menu numberOfItems] > 0)
604  [self _addSeparatorToMenu:menu];
605  break;
606 
607  case CPSearchFieldRecentsMenuItemTag:
608  {
609  var itemAction = @selector(_searchFieldSearch:);
610 
611  for (var recentIndex = 0; recentIndex < countOfRecents; ++recentIndex)
612  {
613  // RECENT_SEARCH_PREFIX is a hack until CPMenuItem -setIndentationLevel works
614  var recentItem = [[CPMenuItem alloc] initWithTitle:RECENT_SEARCH_PREFIX + [_recentSearches objectAtIndex:recentIndex]
615  action:itemAction
616  keyEquivalent:[item keyEquivalent]];
617  [item setTarget:self];
618  [menu addItem:recentItem];
619  }
620 
621  continue;
622  }
623 
624  case CPSearchFieldClearRecentsMenuItemTag:
625  if (countOfRecents === 0)
626  continue;
627 
628  if ([menu numberOfItems] > 0)
629  [self _addSeparatorToMenu:menu];
630 
631  [item setAction:@selector(_searchFieldClearRecents:)];
632  [item setTarget:self];
633  break;
634 
635  case CPSearchFieldNoRecentsMenuItemTag:
636  if (countOfRecents !== 0)
637  continue;
638 
639  if ([menu numberOfItems] > 0)
640  [self _addSeparatorToMenu:menu];
641  break;
642  }
643 
644  [item setEnabled:([item isEnabled] && [item action] != nil && [item target] != nil)];
645  [menu addItem:item];
646  }
647 
648  [menu setDelegate:self];
649 
650  _searchMenu = menu;
651 }
652 
653 - (void)_addSeparatorToMenu:(CPMenu)aMenu
654 {
655  var separator = [CPMenuItem separatorItem];
656  [separator setEnabled:NO];
657  [aMenu addItem:separator];
658 }
659 
660 - (void)menuWillOpen:(CPMenu)menu
661 {
662  _canResignFirstResponder = NO;
663 }
664 
665 - (void)menuDidClose:(CPMenu)menu
666 {
667  _canResignFirstResponder = YES;
668 
669  [self becomeFirstResponder];
670 }
671 
672 - (void)_showMenu
673 {
674  if (_searchMenu === nil || [_searchMenu numberOfItems] === 0 || ![self isEnabled])
675  return;
676 
677  var aFrame = [[self superview] convertRect:[self frame] toView:nil],
678  location = CGPointMake(aFrame.origin.x + 10, aFrame.origin.y + aFrame.size.height - 4);
679 
680  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];
681 
682  [self selectAll:nil];
683  [CPMenu popUpContextMenu:_searchMenu withEvent:anEvent forView:self];
684 }
685 
686 - (void)_sendPartialString
687 {
688  [super sendAction:[self action] to:[self target]];
689  [_partialStringTimer invalidate];
690 }
691 
692 - (void)cancelOperation:(id)sender
693 {
694  [self setObjectValue:@""];
695  [self textDidChange:[CPNotification notificationWithName:CPControlTextDidChangeNotification object:self userInfo:nil]];
696 
697  [self _updateCancelButtonVisibility];
698 }
699 
700 - (void)_searchFieldSearch:(id)sender
701 {
702  var searchString = [[sender title] substringFromIndex:[RECENT_SEARCH_PREFIX length]];
703 
704  if ([sender tag] != CPSearchFieldRecentsMenuItemTag)
705  [self _addStringToRecentSearches:searchString];
706 
707  [self setObjectValue:searchString];
708  [self _sendPartialString];
709  [self selectAll:nil];
710 
711  [self _updateCancelButtonVisibility];
712 }
713 
714 - (void)_searchFieldClearRecents:(id)sender
715 {
716  [self setRecentSearches:[CPArray array]];
717  [self _updateSearchMenu];
718  [self setStringValue:@""];
719  [self _updateCancelButtonVisibility];
720  }
721 
722 - (void)_registerForAutosaveNotification
723 {
724  [[CPNotificationCenter defaultCenter] addObserver:self selector:@selector(_updateAutosavedRecents:) name:CPAutosavedRecentsChangedNotification object:_recentsAutosaveName];
725 }
726 
727 - (void)_deregisterForAutosaveNotification
728 {
729  [[CPNotificationCenter defaultCenter] removeObserver:self name:CPAutosavedRecentsChangedNotification object:_recentsAutosaveName];
730 }
731 
732 - (void)_autosaveRecentSearchList
733 {
734  if (_recentsAutosaveName != nil)
735  [[CPNotificationCenter defaultCenter] postNotificationName:CPAutosavedRecentsChangedNotification object:_recentsAutosaveName];
736 }
737 
738 - (void)_updateAutosavedRecents:(id)notification
739 {
740  var name = [notification object];
741  [[CPUserDefaults standardUserDefaults] setObject:_recentSearches forKey:name];
742 }
743 
744 - (void)_loadRecentSearchList
745 {
746  var name = [self recentsAutosaveName];
747  if (name === nil)
748  return;
749 
750  var list = [[CPUserDefaults standardUserDefaults] objectForKey:name];
751 
752  if (list !== nil)
753  _recentSearches = list;
754 }
755 
756 @end
757 
758 var CPRecentsAutosaveNameKey = @"CPRecentsAutosaveNameKey",
759  CPSendsWholeSearchStringKey = @"CPSendsWholeSearchStringKey",
760  CPSendsSearchStringImmediatelyKey = @"CPSendsSearchStringImmediatelyKey",
761  CPMaximumRecentsKey = @"CPMaximumRecentsKey",
762  CPSearchMenuTemplateKey = @"CPSearchMenuTemplateKey";
763 
764 @implementation CPSearchField (CPCoding)
765 
766 - (void)encodeWithCoder:(CPCoder)coder
767 {
768  [_searchButton removeFromSuperview];
769  [_cancelButton removeFromSuperview];
770 
771  [super encodeWithCoder:coder];
772 
773  if (_searchButton)
774  [self addSubview:_searchButton];
775  if (_cancelButton)
776  [self addSubview:_cancelButton];
777 
778  [coder encodeBool:_sendsWholeSearchString forKey:CPSendsWholeSearchStringKey];
779  [coder encodeBool:_sendsSearchStringImmediately forKey:CPSendsSearchStringImmediatelyKey];
780  [coder encodeInt:_maximumRecents forKey:CPMaximumRecentsKey];
781 
782  if (_recentsAutosaveName)
783  [coder encodeObject:_recentsAutosaveName forKey:CPRecentsAutosaveNameKey];
784 
785  if (_searchMenuTemplate)
786  [coder encodeObject:_searchMenuTemplate forKey:CPSearchMenuTemplateKey];
787 }
788 
789 - (id)initWithCoder:(CPCoder)coder
790 {
791  if (self = [super initWithCoder:coder])
792  {
793  [self setRecentsAutosaveName:[coder decodeObjectForKey:CPRecentsAutosaveNameKey]];
794  _sendsWholeSearchString = [coder decodeBoolForKey:CPSendsWholeSearchStringKey];
795  _sendsSearchStringImmediately = [coder decodeBoolForKey:CPSendsSearchStringImmediatelyKey];
796  _maximumRecents = [coder decodeIntForKey:CPMaximumRecentsKey];
797 
798  var template = [coder decodeObjectForKey:CPSearchMenuTemplateKey];
799 
800  if (template)
801  [self setSearchMenuTemplate:template];
802 
803  [self _init];
804  }
805 
806  return self;
807 }
808 
809 @end