API  0.9.7
 All Classes Files Functions Variables Macros Groups Pages
CPComboBox.j
Go to the documentation of this file.
1 /*
2  * CPComboBox.j
3  * AppKit
4  *
5  * Created by Aparajita Fishman.
6  * Copyright (c) 2012, The Cappuccino Foundation
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 CPComboBoxSelectionDidChangeNotification = @"CPComboBoxSelectionDidChangeNotification";
26 CPComboBoxSelectionIsChangingNotification = @"CPComboBoxSelectionIsChangingNotification";
27 CPComboBoxWillDismissNotification = @"CPComboBoxWillDismissNotification";
28 CPComboBoxWillPopUpNotification = @"CPComboBoxWillPopUpNotification";
29 
30 CPComboBoxStateButtonBordered = CPThemeState("button-bordered");
31 
32 var CPComboBoxTextSubview = @"text",
36 
37 
38 @implementation CPComboBox : CPTextField
39 {
40  CPArray _items;
41  _CPPopUpList _listDelegate;
42  CPComboBoxDataSource _dataSource;
43  BOOL _usesDataSource;
44  BOOL _completes;
45  BOOL _canComplete;
46  int _numberOfVisibleItems;
47  BOOL _forceSelection;
48  BOOL _hasVerticalScroller;
49  CPString _selectedStringValue;
50  BOOL _popUpButtonCausedResign;
51 }
52 
53 + (CPString)defaultThemeClass
54 {
55  return "combobox";
56 }
57 
58 + (CPDictionary)themeAttributes
59 {
60  return @{
61  @"popup-button-size": CGSizeMake(21.0, 29.0),
62  @"border-inset": CGInsetMake(3.0, 3.0, 3.0, 3.0),
63  };
64 }
65 
66 + (Class)_binderClassForBinding:(CPString)aBinding
67 {
68  if (aBinding === CPContentBinding || aBinding === CPContentValuesBinding)
69  return [_CPComboBoxContentBinder class];
70 
71  return [super _binderClassForBinding:aBinding];
72 }
73 
74 - (id)initWithFrame:(CGRect)aFrame
75 {
76  self = [super initWithFrame:aFrame];
77 
78  if (self)
79  [self _initComboBox];
80 
81  return self;
82 }
83 
84 - (void)_initComboBox
85 {
86  _items = [CPArray array];
87 // _listClass = [_CPPopUpList class];
88  _usesDataSource = NO;
89  _completes = NO;
90  _canComplete = NO;
91  _numberOfVisibleItems = CPComboBoxDefaultNumberOfVisibleItems;
92  _forceSelection = NO;
93  _hasVerticalScroller = YES;
94  _selectedStringValue = @"";
95  _popUpButtonCausedResign = NO;
96 
97  [self setTheme:[CPTheme defaultTheme]];
98  [self setBordered:YES];
99  [self setBezeled:YES];
100  [self setEditable:YES];
101  [self setThemeState:CPComboBoxStateButtonBordered];
102 }
103 
104 #pragma mark Setting Display Attributes
105 
106 - (BOOL)hasVerticalScroller
107 {
108  return _hasVerticalScroller;
109 }
110 
111 - (void)setHasVerticalScroller:(BOOL)flag
112 {
113  flag = !!flag;
114 
115  if (_hasVerticalScroller === flag)
116  return;
117 
118  _hasVerticalScroller = flag;
119  [[_listDelegate scrollView] setHasVerticalScroller:flag];
120 }
121 
122 - (CGSize)intercellSpacing
123 {
124  return [[_listDelegate tableView] intercellSpacing];
125 }
126 
127 - (void)setIntercellSpacing:(CGSize)aSize
128 {
129  [[_listDelegate tableView] setIntercellSpacing:aSize];
130 }
131 
132 - (BOOL)isButtonBordered
133 {
134  return [self hasThemeState:CPComboBoxStateButtonBordered];
135 }
136 
137 - (void)setButtonBordered:(BOOL)flag
138 {
139  if (!!flag)
140  [self setThemeState:CPComboBoxStateButtonBordered];
141  else
142  [self unsetThemeState:CPComboBoxStateButtonBordered];
143 }
144 
145 - (float)itemHeight
146 {
147  return [[_listDelegate tableView] rowHeight];
148 }
149 
150 - (void)setItemHeight:(float)itemHeight
151 {
152  [[_listDelegate tableView] setRowHeight:itemHeight];
153 
154  // FIXME: This shouldn't be necessary, but CPTableView does not tile after setRowHeight
155  [[_listDelegate tableView] reloadData];
156 }
157 
158 - (int)numberOfVisibleItems
159 {
160  return _numberOfVisibleItems;
161 }
162 
163 - (void)setNumberOfVisibleItems:(int)visibleItems
164 {
165  // There should always be at least 1 visible item!
166  _numberOfVisibleItems = MAX(visibleItems, 1);
167 }
168 
169 #pragma mark Setting a Delegate
170 
171 - (id /*< CPComboBoxDelegate >*/)delegate
172 {
173  return [super delegate];
174 }
175 
182 - (void)setDelegate:(id /*< CPComboBoxDelegate >*/)aDelegate
183 {
184  var delegate = [self delegate];
185 
186  if (aDelegate === delegate)
187  return;
188 
189  var defaultCenter = [CPNotificationCenter defaultCenter];
190 
191  if (delegate)
192  {
193  [defaultCenter removeObserver:delegate name:CPComboBoxSelectionIsChangingNotification object:self];
194  [defaultCenter removeObserver:delegate name:CPComboBoxSelectionDidChangeNotification object:self];
195  [defaultCenter removeObserver:delegate name:CPComboBoxWillDismissNotification object:self];
196  [defaultCenter removeObserver:delegate name:CPComboBoxWillPopUpNotification object:self];
197  }
198 
199  if (aDelegate)
200  {
201  if ([aDelegate respondsToSelector:@selector(comboBoxSelectionIsChanging:)])
202  [defaultCenter addObserver:delegate
203  selector:@selector(comboBoxSelectionIsChanging:)
204  name:CPComboBoxSelectionIsChangingNotification
205  object:self];
206 
207  if ([aDelegate respondsToSelector:@selector(comboBoxSelectionDidChange:)])
208  [defaultCenter addObserver:delegate
209  selector:@selector(comboBoxSelectionDidChange:)
210  name:CPComboBoxSelectionDidChangeNotification
211  object:self];
212 
213  if ([aDelegate respondsToSelector:@selector(comboBoxWillPopUp:)])
214  [defaultCenter addObserver:delegate
215  selector:@selector(comboBoxWillPopUp:)
216  name:CPComboBoxWillPopUpNotification
217  object:self];
218 
219  if ([aDelegate respondsToSelector:@selector(comboBoxWillDismiss:)])
220  [defaultCenter addObserver:delegate
221  selector:@selector(comboBoxWillDissmis:)
222  name:CPComboBoxWillDismissNotification
223  object:self];
224  }
225 
226  [super setDelegate:aDelegate];
227 }
228 
229 #pragma mark Setting a Data Source
230 
231 - (id /*< CPComboBoxDataSource >*/)dataSource
232 {
233  if (!_usesDataSource)
234  [self _dataSourceWarningForMethod:_cmd condition:NO];
235 
236  return _dataSource;
237 }
238 
239 - (void)setDataSource:(id /*< CPComboBoxDataSource >*/)aSource
240 {
241  if (!_usesDataSource)
242  [self _dataSourceWarningForMethod:_cmd condition:NO];
243  else if (_dataSource !== aSource)
244  {
245  if (![aSource respondsToSelector:@selector(numberOfItemsInComboBox:)] ||
246  ![aSource respondsToSelector:@selector(comboBox:objectValueForItemAtIndex:)])
247  {
248  CPLog.warn("Illegal %s data source (%s). Must implement numberOfItemsInComboBox: and comboBox:objectValueForItemAtIndex:", [self className], [aSource description]);
249  }
250  else
251  _dataSource = aSource;
252  }
253 }
254 
255 - (BOOL)usesDataSource
256 {
257  return _usesDataSource;
258 }
259 
260 - (void)setUsesDataSource:(BOOL)flag
261 {
262  flag = !!flag;
263 
264  if (_usesDataSource === flag)
265  return;
266 
267  _usesDataSource = flag;
268 
269  // Cocoa empties the internal item list if usesDataSource is YES
270  if (_usesDataSource)
271  [_items removeAllObjects];
272 
273  [self reloadData];
274 }
275 
276 #pragma mark Working with an Internal List
277 
278 - (void)addItemsWithObjectValues:(CPArray)objects
279 {
280  [_items addObjectsFromArray:objects];
281 
282  [self reloadDataSourceForSelector:_cmd];
283 }
284 
285 - (void)addItemWithObjectValue:(id)anObject
286 {
287  [_items addObject:anObject];
288 
289  [self reloadDataSourceForSelector:_cmd];
290 }
291 
292 - (void)insertItemWithObjectValue:(id)anObject atIndex:(int)anIndex
293 {
294  // Issue the warning first, because removeObjectAtIndex may raise
295  if (_usesDataSource)
296  [self _dataSourceWarningForMethod:_cmd condition:YES];
297 
298  [_items insertObject:anObject atIndex:anIndex];
299  [self reloadData];
300 }
301 
309 - (CPArray)objectValues
310 {
311  if (_usesDataSource)
312  [self _dataSourceWarningForMethod:_cmd condition:YES];
313 
314  return _items;
315 }
316 
317 - (void)removeAllItems
318 {
319  [_items removeAllObjects];
320 
321  [self reloadDataSourceForSelector:_cmd];
322 }
323 
324 - (void)removeItemAtIndex:(int)index
325 {
326  // Issue the warning first, because removeObjectAtIndex may raise
327  if (_usesDataSource)
328  [self _dataSourceWarningForMethod:_cmd condition:YES];
329 
330  [_items removeObjectAtIndex:index];
331  [self reloadData];
332 }
333 
334 - (void)removeItemWithObjectValue:(id)anObject
335 {
336  [_items removeObject:anObject];
337 
338  [self reloadDataSourceForSelector:_cmd];
339 }
340 
341 - (int)numberOfItems
342 {
343  if (_usesDataSource)
344  return [_dataSource numberOfItemsInComboBox:self];
345  else
346  return _items.length;
347 }
348 
349 #pragma mark Manipulating the Displayed List
350 
354 - (_CPPopUpList)listDelegate
355 {
356  return _listDelegate;
357 }
358 
364 - (void)setListDelegate:(_CPPopUpList)aDelegate
365 {
366  if (_listDelegate === aDelegate)
367  return;
368 
369  var defaultCenter = [CPNotificationCenter defaultCenter];
370 
371  if (_listDelegate)
372  {
373  [defaultCenter removeObserver:self name:_CPPopUpListWillPopUpNotification object:_listDelegate];
374  [defaultCenter removeObserver:self name:_CPPopUpListWillDismissNotification object:_listDelegate];
375  [defaultCenter removeObserver:self name:_CPPopUpListDidDismissNotification object:_listDelegate];
376  [defaultCenter removeObserver:self name:_CPPopUpListItemWasClickedNotification object:_listDelegate];
377 
378  var oldTableView = [_listDelegate tableView];
379 
380  if (oldTableView)
381  {
382  [defaultCenter removeObserver:self name:CPTableViewSelectionIsChangingNotification object:oldTableView];
383  [defaultCenter removeObserver:self name:CPTableViewSelectionDidChangeNotification object:oldTableView];
384  }
385  }
386 
387  _listDelegate = aDelegate;
388 
389  [defaultCenter addObserver:self
390  selector:@selector(comboBoxWillPopUp:)
391  name:_CPPopUpListWillPopUpNotification
392  object:_listDelegate];
393 
394  [defaultCenter addObserver:self
395  selector:@selector(comboBoxWillDismiss:)
396  name:_CPPopUpListWillDismissNotification
397  object:_listDelegate];
398 
399  [defaultCenter addObserver:self
400  selector:@selector(listDidDismiss:)
401  name:_CPPopUpListDidDismissNotification
402  object:_listDelegate];
403 
404  [defaultCenter addObserver:self
405  selector:@selector(itemWasClicked:)
406  name:_CPPopUpListItemWasClickedNotification
407  object:_listDelegate];
408 
409  [[_listDelegate scrollView] setHasVerticalScroller:_hasVerticalScroller];
410 
411  var tableView = [_listDelegate tableView];
412 
413  [defaultCenter addObserver:self
414  selector:@selector(comboBoxSelectionIsChanging:)
415  name:CPTableViewSelectionIsChangingNotification
416  object:tableView];
417 
418  [defaultCenter addObserver:self
419  selector:@selector(comboBoxSelectionDidChange:)
420  name:CPTableViewSelectionDidChangeNotification
421  object:tableView];
422 
423  // Apply our text style to the list
424  [_listDelegate setFont:[self font]];
425  [_listDelegate setAlignment:[self alignment]];
426 }
427 
428 - (int)indexOfItemWithObjectValue:(id)anObject
429 {
430  if (_usesDataSource)
431  [self _dataSourceWarningForMethod:_cmd condition:YES];
432 
433  return [_items indexOfObject:anObject];
434 }
435 
436 - (id)itemObjectValueAtIndex:(int)index
437 {
438  if (_usesDataSource)
439  [self _dataSourceWarningForMethod:_cmd condition:YES];
440 
441  return [_items objectAtIndex:index];
442 }
443 
444 - (void)noteNumberOfItemsChanged
445 {
446  [[_listDelegate tableView] noteNumberOfRowsChanged];
447 }
448 
449 - (void)scrollItemAtIndexToTop:(int)index
450 {
451  [_listDelegate scrollItemAtIndexToTop:index];
452 }
453 
454 - (void)scrollItemAtIndexToVisible:(int)index
455 {
456  [[_listDelegate tableView] scrollRowToVisible:index];
457 }
458 
459 - (void)reloadData
460 {
461  [[_listDelegate tableView] reloadData];
462 }
463 
465 - (void)popUpList
466 {
467  if (!_listDelegate)
468  [self setListDelegate:[[_CPPopUpList alloc] initWithDataSource:self]];
469 
470  [self _selectMatchingItem];
471 
472  // Note the offset here is 1 less than the focus ring width because the outer edge
473  // of the focus ring is very transparent and it looks better if the list is closer.
474  if (CPComboBoxFocusRingWidth < 0)
475  {
476  var inset = [self currentValueForThemeAttribute:@"border-inset"];
477 
478  CPComboBoxFocusRingWidth = inset.bottom;
479  }
480 
481  [_listDelegate popUpRelativeToRect:[self _borderFrame] view:self offset:CPComboBoxFocusRingWidth - 1];
482 }
483 
485 - (BOOL)listIsVisible
486 {
487  return _listDelegate ? [_listDelegate isVisible] : NO;
488 }
489 
491 - (void)reloadDataSourceForSelector:(SEL)cmd
492 {
493  if (_usesDataSource)
494  [self _dataSourceWarningForMethod:cmd condition:YES]
495  else
496  [self reloadData];
497 }
498 
504 - (BOOL)takeStringValueFromList
505 {
506  if (_usesDataSource && _dataSource && [_dataSource numberOfItemsInComboBox:self] === 0)
507  return NO;
508 
509  var selectedStringValue = [_listDelegate selectedStringValue];
510 
511  if (selectedStringValue === nil)
512  return NO;
513  else
514  _selectedStringValue = selectedStringValue;
515 
516  [self setStringValue:_selectedStringValue];
517  [self _reverseSetBinding];
518 
519  return YES;
520 }
521 
526 - (void)listDidDismiss:(CPNotification)aNotification
527 {
528  [[self window] makeFirstResponder:self];
529 }
530 
535 - (void)itemWasClicked:(CPNotification)aNotification
536 {
538  [self sendAction:[self action] to:[self target]];
539 }
540 
541 #pragma mark Manipulating the Selection
542 
543 - (void)deselectItemAtIndex:(int)index
544 {
545  var table = [_listDelegate tableView],
546  row = [table selectedRow];
547 
548  if (row !== index)
549  return;
550 
551  [table deselectRow:index];
552 }
553 
554 - (int)indexOfSelectedItem
555 {
556  return [[_listDelegate tableView] selectedRow];
557 }
558 
559 - (id)objectValueOfSelectedItem
560 {
561  var row = [[_listDelegate tableView] selectedRow];
562 
563  if (row >= 0)
564  {
565  if (_usesDataSource)
566  [self _dataSourceWarningForMethod:_cmd condition:YES];
567 
568  return _items[row];
569  }
570 
571  return nil;
572 }
573 
574 - (void)selectItemAtIndex:(int)index
575 {
576  var table = [_listDelegate tableView],
577  row = [table selectedRow];
578 
579  if (row === index)
580  return;
581 
582  [table selectRowIndexes:[CPIndexSet indexSetWithIndex:index] byExtendingSelection:NO];
583 }
584 
585 - (void)selectItemWithObjectValue:(id)anObject
586 {
587  var index = [self indexOfItemWithObjectValue:anObject];
588 
589  if (index !== CPNotFound)
590  [self selectItemAtIndex:index];
591 }
592 
593 #pragma mark Completing the Text Field
594 
595 - (BOOL)completes
596 {
597  return _completes;
598 }
599 
600 - (void)setCompletes:(BOOL)flag
601 {
602  _completes = !!flag;
603 }
604 
605 - (CPString)completedString:(CPString)substring
606 {
607  if (_usesDataSource)
608  return [self comboBoxCompletedString:substring];
609  else
610  {
611  var index = [_items indexOfObjectPassingTest:CPComboBoxCompletionTest context:substring];
612 
613  return index !== CPNotFound ? _items[index] : nil;
614  }
615 }
616 
621 - (BOOL)forceSelection
622 {
623  return _forceSelection;
624 }
625 
635 - (void)setForceSelection:(BOOL)flag
636 {
637  _forceSelection = !!flag;
638 }
639 
640 #pragma mark CPTextField Delegate Methods and Overrides
641 
643 - (BOOL)sendAction:(SEL)anAction to:(id)anObject
644 {
645  // When the action is sent, be sure to get the value and close the list.
646  // This covers the case where the action is triggered by pressing a key
647  // that triggers the text field action.
648 
649  if ([self listIsVisible])
650  {
652  [_listDelegate close];
653  }
654 
655  return [super sendAction:anAction to:anObject];
656 }
657 
659 - (void)setObjectValue:(id)object
660 {
661  [super setObjectValue:object];
662 
663  _selectedStringValue = [self stringValue];
664 }
665 
667 - (void)interpretKeyEvents:(CPArray)events
668 {
669  var theEvent = events[0];
670 
671  // Only if characters are added at the end of the value can completion occur
672  _canComplete = NO;
673 
674  if (_completes)
675  {
676  if (![theEvent _couldBeKeyEquivalent] && [theEvent characters].charAt(0) !== CPDeleteCharacter)
677  {
678  var value = [self _inputElement].value,
679  selectedRange = [self selectedRange];
680 
681  _canComplete = CPMaxRange(selectedRange) === value.length;
682  }
683  }
684 
685  [super interpretKeyEvents:events];
686 }
687 
689 - (void)paste:(id)sender
690 {
691  if (_completes)
692  {
693  // Completion can occur only if pasting at the end of the value
694  var value = [self _inputElement].value,
695  selectedRange = [self selectedRange];
696 
697  _canComplete = CPMaxRange(selectedRange) === value.length;
698  }
699  else
700  _canComplete = NO;
701 
702  [super paste:sender];
703 }
704 
706 - (void)textDidChange:(CPNotification)aNotification
707 {
708  /*
709  Completion is attempted iff:
710  - _completes is YES
711  - Characters were added at the end of the value
712  */
713  var uncompletedString = [self stringValue],
714  newString = uncompletedString;
715 
716  if (_completes && _canComplete)
717  {
718  newString = [self completedString:uncompletedString];
719 
720  if (newString && newString.length > uncompletedString.length)
721  {
722  [self setStringValue:newString];
723  [self setSelectedRange:CPMakeRange(uncompletedString.length, newString.length - uncompletedString.length)];
724  }
725  }
726 
727  [self _selectMatchingItem];
728  _canComplete = NO;
729 
730  [super textDidChange:aNotification];
731 }
732 
737 - (BOOL)performKeyEquivalent:(CPEvent)anEvent
738 {
739  if ([[self window] firstResponder] === self)
740  {
741  var key = [anEvent charactersIgnoringModifiers];
742 
743  switch (key)
744  {
746  if (![self listIsVisible])
747  {
748  [self popUpList];
749  return YES;
750  }
751  break;
752 
753  case CPEscapeFunctionKey:
754  if ([self listIsVisible])
755  {
756  // If we are forcing a selection and the user has entered a value which is not
757  // in the list, revert to the most recent valid value.
758  if (_forceSelection && ([self _inputElement].value !== _selectedStringValue))
759  [self setStringValue:_selectedStringValue];
760  }
761  break;
762  }
763 
764  if ([_listDelegate performKeyEquivalent:anEvent])
765  return YES;
766  }
767 
768  return [super performKeyEquivalent:anEvent];
769 }
770 
772 - (BOOL)resignFirstResponder
773 {
774  var buttonCausedResign = _popUpButtonCausedResign;
775 
776  _popUpButtonCausedResign = NO;
777 
778  /*
779  If the list or popup button is clicked, we lose focus. The list will refuse first responder,
780  and we refuse to resign. But we still have to manually restore the focus to the input element.
781  */
782  var shouldResign = !buttonCausedResign && (!_listDelegate || [_listDelegate controllingViewShouldResign]);
783 
784  if (!shouldResign)
785  {
786 #if PLATFORM(DOM)
787  // In FireFox this needs to be done in setTimeout, otherwise there is no caret
788  // We have to save the input element now, when we lose focus it will change.
789  var element = [self _inputElement];
790  window.setTimeout(function() { element.focus(); }, 0);
791 #endif
792 
793  return NO;
794  }
795 
796  // The list was not clicked, we need to close it now
797  [_listDelegate close];
798 
799  // If the field is empty, allow it to remain empty.
800  // Otherwise restore the most recently selected value if forcing selection.
801  var value = [self stringValue];
802 
803  if (value)
804  {
805  if (_forceSelection && ![value isEqual:_selectedStringValue])
806  [self setStringValue:_selectedStringValue];
807  }
808  else
809  _selectedStringValue = @"";
810 
811  return [super resignFirstResponder];
812 }
813 
814 - (void)setFont:(CPFont)aFont
815 {
816  [super setFont:aFont];
817  [_listDelegate setFont:aFont];
818 }
819 
820 - (void)setAlignment:(CPTextAlignment)alignment
821 {
822  [super setAlignment:alignment];
823  [_listDelegate setAlignment:alignment];
824 }
825 
826 #pragma mark Pop Up Button Layout
827 
828 - (CGRect)popupButtonRectForBounds:(CGRect)bounds
829 {
830  var borderInset = [self currentValueForThemeAttribute:@"border-inset"],
831  buttonSize = [self currentValueForThemeAttribute:@"popup-button-size"];
832 
833  bounds.origin.x = CGRectGetMaxX(bounds) - borderInset.right - buttonSize.width;
834  bounds.origin.y += borderInset.top;
835 
836  bounds.size.width = buttonSize.width;
837  bounds.size.height = buttonSize.height;
838 
839  return bounds;
840 }
841 
842 - (CGRect)rectForEphemeralSubviewNamed:(CPString)aName
843 {
844  if (aName === "popup-button-view")
845  return [self popupButtonRectForBounds:[self bounds]];
846 
847  return [super rectForEphemeralSubviewNamed:aName];
848 }
849 
850 - (CPView)createEphemeralSubviewNamed:(CPString)aName
851 {
852  if (aName === "popup-button-view")
853  {
854  var view = [[_CPComboBoxPopUpButton alloc] initWithFrame:CGRectMakeZero() comboBox:self];
855 
856  return view;
857  }
858 
859  return [super createEphemeralSubviewNamed:aName];
860 }
861 
862 - (void)layoutSubviews
863 {
864  [super layoutSubviews];
865 
866  var popupButtonView = [self layoutEphemeralSubviewNamed:@"popup-button-view"
867  positioned:CPWindowAbove
868  relativeToEphemeralSubviewNamed:@"content-view"];
869 }
870 
871 #pragma mark Internal Helpers
872 
874 - (void)_dataSourceWarningForMethod:(SEL)cmd condition:(CPString)flag
875 {
876  CPLog.warn("-[%s %s] should not be called when usesDataSource is set to %s", [self className], cmd, flag ? "YES" : "NO");
877 }
878 
883 - (void)_selectMatchingItem
884 {
885  var index = CPNotFound,
886  stringValue = [self stringValue];
887 
888  if (_usesDataSource)
889  {
890  if (_dataSource && [_dataSource respondsToSelector:@selector(comboBox:indexOfItemWithStringValue:)])
891  index = [_dataSource comboBox:self indexOfItemWithStringValue:stringValue]
892  }
893  else
894  index = [self indexOfItemWithObjectValue:stringValue];
895 
896  [_listDelegate selectRow:index];
897 
898  // selectRow scrolls the row to visible, if a row is selected scroll it to the top
899  if (index !== CPNotFound)
900  {
901  [_listDelegate scrollItemAtIndexToTop:index];
902  _selectedStringValue = stringValue;
903  }
904 }
905 
910 - (CGRect)_borderFrame
911 {
912  var inset = [self currentValueForThemeAttribute:@"border-inset"],
913  frame = [self bounds];
914 
915  frame.origin.x += inset.left;
916  frame.origin.y += inset.top;
917  frame.size.width -= inset.left + inset.right;
918  frame.size.height -= inset.top + inset.bottom;
919 
920  return frame;
921 }
922 
923 /* @ignore */
924 - (void)_popUpButtonWasClicked
925 {
926  if (![self isEnabled])
927  return;
928 
929  // If we are currently the first responder, we will be asked to resign when the list pops up.
930  // Set a flag to let resignResponder know that the button was clicked and we should not resign.
931  var firstResponder = [[self window] firstResponder];
932 
933  _popUpButtonCausedResign = firstResponder === self;
934 
935  if ([self listIsVisible])
936  [_listDelegate close];
937  else
938  {
939  if (firstResponder !== self)
940  [[self window] makeFirstResponder:self];
941 
942  [self popUpList];
943  }
944 }
945 
946 @end
947 
949 
951 - (void)comboBoxSelectionIsChanging:(CPNotification)aNotification
952 {
953  [[CPNotificationCenter defaultCenter] postNotificationName:CPComboBoxSelectionIsChangingNotification object:self];
954 }
955 
957 - (void)comboBoxSelectionDidChange:(CPNotification)aNotification
958 {
959  [[CPNotificationCenter defaultCenter] postNotificationName:CPComboBoxSelectionDidChangeNotification object:self];
960 }
961 
963 - (void)comboBoxWillPopUp:(CPNotification)aNotification
964 {
965  [[CPNotificationCenter defaultCenter] postNotificationName:CPComboBoxWillPopUpNotification object:self];
966 }
967 
969 - (void)comboBoxWillDismiss:(CPNotification)aNotification
970 {
971  [[CPNotificationCenter defaultCenter] postNotificationName:CPComboBoxWillDismissNotification object:self];
972 }
973 
974 @end
975 
977 
979 - (CPString)comboBoxCompletedString:(CPString)uncompletedString
980 {
981  if ([_dataSource respondsToSelector:@selector(comboBox:completedString:)])
982  return [_dataSource comboBox:self completedString:uncompletedString];
983  else
984  return nil;
985 }
986 
987 @end
988 
989 @implementation CPComboBox (_CPPopUpListDataSource)
990 
991 - (int)numberOfItemsInList:(_CPPopUpList)aList
992 {
993  return [self numberOfItems];
994 }
995 
996 - (int)numberOfVisibleItemsInList:(_CPPopUpList)aList
997 {
998  return [self numberOfVisibleItems];
999 }
1000 
1001 - (id)list:(_CPPopUpList)aList objectValueForItemAtIndex:(int)index
1002 {
1003  if (_usesDataSource)
1004  return [_dataSource comboBox:self objectValueForItemAtIndex:index];
1005  else
1006  return _items[index];
1007 }
1008 
1009 - (id)list:(_CPPopUpList)aList displayValueForObjectValue:(id)aValue
1010 {
1011  return aValue || @"";
1012 }
1013 
1014 - (CPString)list:(_CPPopUpList)aList stringValueForObjectValue:(id)aValue
1015 {
1016  return String(aValue);
1017 }
1018 
1019 @end
1020 
1021 @implementation CPComboBox (Bindings)
1022 
1024 - (void)setContentValues:(CPArray)anArray
1025 {
1026  [self setUsesDataSource:NO];
1027  [self removeAllItems];
1028  [self addItemsWithObjectValues:anArray];
1029 }
1030 
1032 - (void)setContent:(CPArray)anArray
1033 {
1034  [self setUsesDataSource:NO];
1035 
1036  // Directly nuke _items, [_items removeAll] will trigger an extra call to setContent
1037  _items = [];
1038 
1039  var values = [];
1040 
1041  [anArray enumerateObjectsUsingBlock:function(object)
1042  {
1043  values.push([object description]);
1044  }];
1045 
1046  [self addItemsWithObjectValues:values];
1047 }
1048 
1049 @end
1050 
1051 var CPComboBoxItemsKey = @"CPComboBoxItemsKey",
1052  CPComboBoxListKey = @"CPComboBoxListKey",
1053  CPComboBoxDelegateKey = @"CPComboBoxDelegateKey",
1054  CPComboBoxDataSourceKey = @"CPComboBoxDataSourceKey",
1055  CPComboBoxUsesDataSourceKey = @"CPComboBoxUsesDataSourceKey",
1056  CPComboBoxCompletesKey = @"CPComboBoxCompletesKey",
1057  CPComboBoxNumberOfVisibleItemsKey = @"CPComboBoxNumberOfVisibleItemsKey",
1058  CPComboBoxHasVerticalScrollerKey = @"CPComboBoxHasVerticalScrollerKey",
1059  CPComboBoxButtonBorderedKey = @"CPComboBoxButtonBorderedKey";
1060 
1061 @implementation CPComboBox (CPCoding)
1062 
1063 - (id)initWithCoder:(CPCoder)aCoder
1064 {
1065  self = [super initWithCoder:aCoder];
1066 
1067  if (self)
1068  {
1069  [self _initComboBox];
1070 
1071  _items = [aCoder decodeObjectForKey:CPComboBoxItemsKey];
1072  _listDelegate = [aCoder decodeObjectForKey:CPComboBoxListKey];
1073  _delegate = [aCoder decodeObjectForKey:CPComboBoxDelegateKey];
1074  _dataSource = [aCoder decodeObjectForKey:CPComboBoxDataSourceKey];
1075  _usesDataSource = [aCoder decodeBoolForKey:CPComboBoxUsesDataSourceKey];
1076  _completes = [aCoder decodeBoolForKey:CPComboBoxCompletesKey];
1077  _numberOfVisibleItems = [aCoder decodeIntForKey:CPComboBoxNumberOfVisibleItemsKey];
1078  _hasVerticalScroller = [aCoder decodeBoolForKey:CPComboBoxHasVerticalScrollerKey];
1079  [self setButtonBordered:[aCoder decodeBoolForKey:CPComboBoxButtonBorderedKey]];
1080  }
1081 
1082  return self;
1083 }
1084 
1085 - (void)encodeWithCoder:(CPCoder)aCoder
1086 {
1087  [super encodeWithCoder:aCoder];
1088 
1089  [aCoder encodeObject:_items forKey:CPComboBoxItemsKey];
1090  [aCoder encodeObject:_listDelegate forKey:CPComboBoxListKey];
1091  [aCoder encodeObject:_delegate forKey:CPComboBoxDelegateKey];
1092  [aCoder encodeObject:_dataSource forKey:CPComboBoxDataSourceKey];
1093  [aCoder encodeBool:_usesDataSource forKey:CPComboBoxUsesDataSourceKey];
1094  [aCoder encodeBool:_completes forKey:CPComboBoxCompletesKey];
1095  [aCoder encodeInt:_numberOfVisibleItems forKey:CPComboBoxNumberOfVisibleItemsKey];
1096  [aCoder encodeBool:_hasVerticalScroller forKey:CPComboBoxHasVerticalScrollerKey];
1097  [aCoder encodeBool:[self isButtonBordered] forKey:CPComboBoxButtonBorderedKey];
1098 }
1099 
1100 @end
1101 
1102 
1103 var CPComboBoxCompletionTest = function(object, index, context)
1104 {
1105  return object.toString().indexOf(context) === 0;
1106 };
1107 
1108 
1109 /*
1110  This class is only used for CPContentBinding and CPContentValuesBinding.
1111 */
1112 @implementation _CPComboBoxContentBinder : CPBinder
1113 {
1114  id __doxygen__;
1115 }
1116 
1117 - (void)setValueFor:(CPString)aBinding
1118 {
1119  var destination = [_info objectForKey:CPObservedObjectKey],
1120  keyPath = [_info objectForKey:CPObservedKeyPathKey],
1121  options = [_info objectForKey:CPOptionsKey],
1122  newValue = [destination valueForKeyPath:keyPath],
1123  isPlaceholder = CPIsControllerMarker(newValue);
1124 
1125  [_source removeAllItems];
1126 
1127  if (isPlaceholder)
1128  {
1129  // By default the placeholders will all result in an empty list
1130  switch (newValue)
1131  {
1132  case CPMultipleValuesMarker:
1133  newValue = [options objectForKey:CPMultipleValuesPlaceholderBindingOption] || [];
1134  break;
1135 
1136  case CPNoSelectionMarker:
1137  newValue = [options objectForKey:CPNoSelectionPlaceholderBindingOption] || [];
1138  break;
1139 
1140  case CPNotApplicableMarker:
1141  if ([options objectForKey:CPRaisesForNotApplicableKeysBindingOption])
1142  [CPException raise:CPGenericException
1143  reason:@"can't transform non applicable key on: " + _source + " value: " + newValue];
1144 
1145  newValue = [options objectForKey:CPNotApplicablePlaceholderBindingOption] || [];
1146  break;
1147 
1148  case CPNullMarker:
1149  newValue = [options objectForKey:CPNullPlaceholderBindingOption] || [];
1150  break;
1151  }
1152 
1153  if (![newValue isKindOfClass:[CPArray class]])
1154  newValue = [];
1155  }
1156  else
1157  newValue = [self transformValue:newValue withOptions:options];
1158 
1159  switch (aBinding)
1160  {
1161  case CPContentBinding:
1162  [_source setContent:newValue];
1163  break;
1164 
1165  case CPContentValuesBinding:
1166  [_source setContentValues:newValue];
1167  break;
1168  }
1169 }
1170 
1171 @end
1172 
1173 @implementation _CPComboBoxPopUpButton : CPView
1174 {
1175  CPComboBox _comboBox;
1176 }
1177 
1178 - (id)initWithFrame:(CGRect)aFrame comboBox:(CPComboBox)aComboBox
1179 {
1180  self = [super initWithFrame:aFrame];
1181 
1182  if (self)
1183  _comboBox = aComboBox;
1184 
1185  return self;
1186 }
1187 
1188 - (void)mouseDown:(CPEvent)theEvent
1189 {
1190  [_comboBox _popUpButtonWasClicked];
1191 }
1192 
1193 - (BOOL)acceptsFirstResponder
1194 {
1195  return NO;
1196 }
1197 
1198 @end