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