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