API  1.0.0
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
26 @protocol CPComboBoxDelegate <CPObject>
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 
36 @protocol CPComboBoxDataSource <CPObject>
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 
216 - (id <CPComboBoxDelegate>)delegate
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 
844  [[CPRunLoop mainRunLoop] performBlock:function()
845  {
846  // This will prevent to jump to the focused element
847  var previousScrollingOrigin = [self _scrollToVisibleRectAndReturnPreviousOrigin];
848 
849  element.focus();
850 
851  [self _restorePreviousScrollingOrigin:previousScrollingOrigin];
852 
853  } argument:nil order:0 modes:[CPDefaultRunLoopMode]];
854 #endif
855 
856  return NO;
857  }
858 
859  // The list was not clicked, we need to close it now
860  [_listDelegate close];
861 
862  // If the field is empty, allow it to remain empty.
863  // Otherwise restore the most recently selected value if forcing selection.
864  var value = [self stringValue];
865 
866  if (value)
867  {
868  if (_forceSelection && ![value isEqual:_selectedStringValue])
869  [self setStringValue:_selectedStringValue];
870  }
871  else
872  _selectedStringValue = @"";
873 
874  return [super resignFirstResponder];
875 }
876 
877 - (void)setFont:(CPFont)aFont
878 {
879  [super setFont:aFont];
880 
881  if (_listDelegate)
882  [_listDelegate setFont:aFont];
883 }
884 
885 - (void)setAlignment:(CPTextAlignment)alignment
886 {
887  [super setAlignment:alignment];
888 
889  if (_listDelegate)
890  [_listDelegate setAlignment:alignment];
891 }
892 
893 #pragma mark Pop Up Button Layout
894 
895 - (CGRect)popupButtonRectForBounds:(CGRect)bounds
896 {
897  var borderInset = [self currentValueForThemeAttribute:@"border-inset"],
898  buttonSize = [self currentValueForThemeAttribute:@"popup-button-size"];
899 
900  bounds.origin.x = CGRectGetMaxX(bounds) - borderInset.right - buttonSize.width;
901  bounds.origin.y += borderInset.top;
902 
903  bounds.size.width = buttonSize.width;
904  bounds.size.height = buttonSize.height;
905 
906  return bounds;
907 }
908 
909 - (CGRect)rectForEphemeralSubviewNamed:(CPString)aName
910 {
911  if (aName === "popup-button-view")
912  return [self popupButtonRectForBounds:[self bounds]];
913 
914  return [super rectForEphemeralSubviewNamed:aName];
915 }
916 
917 - (CPView)createEphemeralSubviewNamed:(CPString)aName
918 {
919  if (aName === "popup-button-view")
920  {
921  var view = [[_CPComboBoxPopUpButton alloc] initWithFrame:CGRectMakeZero() comboBox:self];
922 
923  return view;
924  }
925 
926  return [super createEphemeralSubviewNamed:aName];
927 }
928 
929 - (void)layoutSubviews
930 {
931  [super layoutSubviews];
932 
933  var popupButtonView = [self layoutEphemeralSubviewNamed:@"popup-button-view"
934  positioned:CPWindowAbove
935  relativeToEphemeralSubviewNamed:@"content-view"];
936 }
937 
938 #pragma mark Internal Helpers
939 
941 - (void)_dataSourceWarningForMethod:(SEL)cmd condition:(CPString)flag
942 {
943  CPLog.warn("-[%s %s] should not be called when usesDataSource is set to %s", [self className], cmd, flag ? "YES" : "NO");
944 }
945 
950 - (void)_selectMatchingItem
951 {
952  var index = CPNotFound,
953  stringValue = [self stringValue];
954 
955  if (_usesDataSource)
956  {
957  if (_dataSource && [_dataSource respondsToSelector:@selector(comboBox:indexOfItemWithStringValue:)])
958  index = [_dataSource comboBox:self indexOfItemWithStringValue:stringValue]
959  }
960  else
961  {
962  index = [self indexOfItemWithObjectValue:stringValue];
963  }
964 
965  [_listDelegate selectRow:index];
966 
967  // selectRow scrolls the row to visible, if a row is selected scroll it to the top
968  if (index !== CPNotFound)
969  {
970  [_listDelegate scrollItemAtIndexToTop:index];
971  _selectedStringValue = stringValue;
972  }
973 }
974 
979 - (CGRect)_borderFrame
980 {
981  var inset = [self currentValueForThemeAttribute:@"border-inset"],
982  frame = [self bounds];
983 
984  frame.origin.x += inset.left;
985  frame.origin.y += inset.top;
986  frame.size.width -= inset.left + inset.right;
987  frame.size.height -= inset.top + inset.bottom;
988 
989  return frame;
990 }
991 
992 /* @ignore */
993 - (void)_popUpButtonWasClicked
994 {
995  if (![self isEnabled])
996  return;
997 
998  // If we are currently the first responder, we will be asked to resign when the list pops up.
999  // Set a flag to let resignResponder know that the button was clicked and we should not resign.
1000  var firstResponder = [[self window] firstResponder];
1001 
1002  _popUpButtonCausedResign = firstResponder === self;
1003 
1004  if ([self listIsVisible])
1005  [_listDelegate close];
1006  else
1007  {
1008  if (firstResponder !== self)
1009  [[self window] makeFirstResponder:self];
1010 
1011  [self popUpList];
1012  }
1013 }
1014 
1015 
1016 #pragma mark -
1017 #pragma mark Observers method
1018 
1019 - (void)_addObservers
1020 {
1021  if (_isObserving)
1022  return;
1023 
1024  [super _addObservers];
1025  [self _addObserversForListDelegate:_listDelegate];
1026 }
1027 
1028 - (void)_removeObservers
1029 {
1030  if (!_isObserving)
1031  return;
1032 
1033  [super _removeObservers];
1034  [self _removeObserversForListDelegate:_listDelegate];
1035 }
1036 
1037 @end
1038 
1040 
1042 - (void)comboBoxSelectionIsChanging:(CPNotification)aNotification
1043 {
1044  if (_implementedDelegateComboBoxMethods & CPComboBoxDelegate_comboBoxSelectionIsChanging_)
1045  [_delegate comboBoxSelectionIsChanging:[[CPNotification alloc] initWithName:CPComboBoxSelectionIsChangingNotification object:self userInfo:nil]];
1046 
1047  [[CPNotificationCenter defaultCenter] postNotificationName:CPComboBoxSelectionIsChangingNotification object:self];
1048 }
1049 
1051 - (void)comboBoxSelectionDidChange:(CPNotification)aNotification
1052 {
1053  if (_implementedDelegateComboBoxMethods & CPComboBoxDelegate_comboBoxSelectionDidChange_)
1054  [_delegate comboBoxSelectionDidChange:[[CPNotification alloc] initWithName:CPComboBoxSelectionDidChangeNotification object:self userInfo:nil]];
1055 
1056  [[CPNotificationCenter defaultCenter] postNotificationName:CPComboBoxSelectionDidChangeNotification object:self];
1057 }
1058 
1060 - (void)comboBoxWillPopUp:(CPNotification)aNotification
1061 {
1062  if (_implementedDelegateComboBoxMethods & CPComboBoxDelegate_comboBoxWillPopUp_)
1063  [_delegate comboBoxWillPopUp:[[CPNotification alloc] initWithName:CPComboBoxWillPopUpNotification object:self userInfo:nil]];
1064 
1065  [[CPNotificationCenter defaultCenter] postNotificationName:CPComboBoxWillPopUpNotification object:self];
1066 }
1067 
1069 - (void)comboBoxWillDismiss:(CPNotification)aNotification
1070 {
1071  if (_implementedDelegateComboBoxMethods & CPComboBoxDelegate_comboBoxWillDismiss_)
1072  [_delegate comboBoxWillDismiss:[[CPNotification alloc] initWithName:CPComboBoxWillDismissNotification object:self userInfo:nil]];
1073 
1074  [[CPNotificationCenter defaultCenter] postNotificationName:CPComboBoxWillDismissNotification object:self];
1075 }
1076 
1077 @end
1078 
1080 
1082 - (CPString)comboBoxCompletedString:(CPString)uncompletedString
1083 {
1084  if ([_dataSource respondsToSelector:@selector(comboBox:completedString:)])
1085  return [_dataSource comboBox:self completedString:uncompletedString];
1086  else
1087  return nil;
1088 }
1089 
1090 @end
1091 
1092 @implementation CPComboBox (_CPPopUpListDataSource)
1093 
1094 - (int)numberOfItemsInList:(_CPPopUpList)aList
1095 {
1096  return [self numberOfItems];
1097 }
1098 
1099 - (int)numberOfVisibleItemsInList:(_CPPopUpList)aList
1100 {
1101  return [self numberOfVisibleItems];
1102 }
1103 
1104 - (id)list:(_CPPopUpList)aList objectValueForItemAtIndex:(int)index
1105 {
1106  if (_usesDataSource)
1107  return [_dataSource comboBox:self objectValueForItemAtIndex:index];
1108  else
1109  return _items[index];
1110 }
1111 
1112 - (id)list:(_CPPopUpList)aList displayValueForObjectValue:(id)aValue
1113 {
1114  return aValue || @"";
1115 }
1116 
1117 - (CPString)list:(_CPPopUpList)aList stringValueForObjectValue:(id)aValue
1118 {
1119  return String(aValue);
1120 }
1121 
1122 @end
1123 
1124 @implementation CPComboBox (Bindings)
1125 
1127 - (void)setContentValues:(CPArray)anArray
1128 {
1129  [self setUsesDataSource:NO];
1130  [self removeAllItems];
1131  [self addItemsWithObjectValues:anArray];
1132 }
1133 
1135 - (void)setContent:(CPArray)anArray
1136 {
1137  [self setUsesDataSource:NO];
1138 
1139  // Directly nuke _items, [_items removeAll] will trigger an extra call to setContent
1140  _items = [];
1141 
1142  var values = [anArray arrayByApplyingBlock:function(object)
1143  {
1144  return [object description];
1145  }];
1146 
1147  [self addItemsWithObjectValues:values];
1148 }
1149 
1150 @end
1151 
1152 var CPComboBoxItemsKey = @"CPComboBoxItemsKey",
1153  CPComboBoxListKey = @"CPComboBoxListKey",
1154  CPComboBoxDelegateKey = @"CPComboBoxDelegateKey",
1155  CPComboBoxDataSourceKey = @"CPComboBoxDataSourceKey",
1156  CPComboBoxUsesDataSourceKey = @"CPComboBoxUsesDataSourceKey",
1157  CPComboBoxCompletesKey = @"CPComboBoxCompletesKey",
1158  CPComboBoxNumberOfVisibleItemsKey = @"CPComboBoxNumberOfVisibleItemsKey",
1159  CPComboBoxHasVerticalScrollerKey = @"CPComboBoxHasVerticalScrollerKey",
1160  CPComboBoxButtonBorderedKey = @"CPComboBoxButtonBorderedKey";
1161 
1162 @implementation CPComboBox (CPCoding)
1163 
1164 - (id)initWithCoder:(CPCoder)aCoder
1165 {
1166  self = [super initWithCoder:aCoder];
1167 
1168  if (self)
1169  {
1170  [self _initComboBox];
1171 
1172  _items = [aCoder decodeObjectForKey:CPComboBoxItemsKey];
1173  _listDelegate = [aCoder decodeObjectForKey:CPComboBoxListKey];
1174  _delegate = [aCoder decodeObjectForKey:CPComboBoxDelegateKey];
1175  _dataSource = [aCoder decodeObjectForKey:CPComboBoxDataSourceKey];
1176  _usesDataSource = [aCoder decodeBoolForKey:CPComboBoxUsesDataSourceKey];
1177  _completes = [aCoder decodeBoolForKey:CPComboBoxCompletesKey];
1178  _numberOfVisibleItems = [aCoder decodeIntForKey:CPComboBoxNumberOfVisibleItemsKey];
1179  _hasVerticalScroller = [aCoder decodeBoolForKey:CPComboBoxHasVerticalScrollerKey];
1180  [self setButtonBordered:[aCoder decodeBoolForKey:CPComboBoxButtonBorderedKey]];
1181  }
1182 
1183  return self;
1184 }
1185 
1186 - (void)encodeWithCoder:(CPCoder)aCoder
1187 {
1188  [super encodeWithCoder:aCoder];
1189 
1190  [aCoder encodeObject:_items forKey:CPComboBoxItemsKey];
1191  [aCoder encodeObject:_listDelegate forKey:CPComboBoxListKey];
1192  [aCoder encodeObject:_delegate forKey:CPComboBoxDelegateKey];
1193  [aCoder encodeObject:_dataSource forKey:CPComboBoxDataSourceKey];
1194  [aCoder encodeBool:_usesDataSource forKey:CPComboBoxUsesDataSourceKey];
1195  [aCoder encodeBool:_completes forKey:CPComboBoxCompletesKey];
1196  [aCoder encodeInt:_numberOfVisibleItems forKey:CPComboBoxNumberOfVisibleItemsKey];
1197  [aCoder encodeBool:_hasVerticalScroller forKey:CPComboBoxHasVerticalScrollerKey];
1198  [aCoder encodeBool:[self isButtonBordered] forKey:CPComboBoxButtonBorderedKey];
1199 }
1200 
1201 @end
1202 
1203 
1204 var CPComboBoxCompletionTest = function(object, index, context)
1205 {
1206  return object.toString().indexOf(context) === 0;
1207 };
1208 
1209 
1210 /*
1211  This class is only used for CPContentBinding and CPContentValuesBinding.
1212 */
1213 @implementation _CPComboBoxContentBinder : CPBinder
1214 {
1215  id __doxygen__;
1216 }
1217 
1218 - (void)setValueFor:(CPString)aBinding
1219 {
1220  var destination = [_info objectForKey:CPObservedObjectKey],
1221  keyPath = [_info objectForKey:CPObservedKeyPathKey],
1222  options = [_info objectForKey:CPOptionsKey],
1223  newValue = [destination valueForKeyPath:keyPath],
1224  isPlaceholder = CPIsControllerMarker(newValue);
1225 
1226  [_source removeAllItems];
1227 
1228  if (isPlaceholder)
1229  {
1230  // By default the placeholders will all result in an empty list
1231  switch (newValue)
1232  {
1233  case CPMultipleValuesMarker:
1234  newValue = [options objectForKey:CPMultipleValuesPlaceholderBindingOption] || [];
1235  break;
1236 
1237  case CPNoSelectionMarker:
1238  newValue = [options objectForKey:CPNoSelectionPlaceholderBindingOption] || [];
1239  break;
1240 
1241  case CPNotApplicableMarker:
1242  if ([options objectForKey:CPRaisesForNotApplicableKeysBindingOption])
1243  [CPException raise:CPGenericException
1244  reason:@"can't transform non applicable key on: " + _source + " value: " + newValue];
1245 
1246  newValue = [options objectForKey:CPNotApplicablePlaceholderBindingOption] || [];
1247  break;
1248 
1249  case CPNullMarker:
1250  newValue = [options objectForKey:CPNullPlaceholderBindingOption] || [];
1251  break;
1252  }
1253 
1254  if (![newValue isKindOfClass:[CPArray class]])
1255  newValue = [];
1256  }
1257  else
1258  newValue = [self transformValue:newValue withOptions:options];
1259 
1260  switch (aBinding)
1261  {
1262  case CPContentBinding:
1263  [_source setContent:newValue];
1264  break;
1265 
1266  case CPContentValuesBinding:
1267  [_source setContentValues:newValue];
1268  break;
1269  }
1270 }
1271 
1272 @end
1273 
1274 @implementation _CPComboBoxPopUpButton : CPView
1275 {
1276  CPComboBox _comboBox;
1277 }
1278 
1279 - (id)initWithFrame:(CGRect)aFrame comboBox:(CPComboBox)aComboBox
1280 {
1281  self = [super initWithFrame:aFrame];
1282 
1283  if (self)
1284  _comboBox = aComboBox;
1285 
1286  return self;
1287 }
1288 
1289 - (void)mouseDown:(CPEvent)theEvent
1290 {
1291  [_comboBox _popUpButtonWasClicked];
1292 }
1293 
1294 - (BOOL)acceptsFirstResponder
1295 {
1296  return NO;
1297 }
1298 
1299 @end
Used to implement exception handling (creating & raising).
Definition: CPException.h:2
Definition: CPFont.h:2
BOOL makeFirstResponder:(CPResponder aResponder)
Definition: CPWindow.j:1632
void setFont:(CPFont aFont)
Definition: CPControl.j:891
var CPComboBoxButtonSubview
Definition: CPComboBox.j:59
CPString completedString:(CPString substring)
Definition: CPComboBox.j:658
CPView createEphemeralSubviewNamed:(CPString aName)
Definition: CPTextField.j:1833
var CPComboBoxDataSourceKey
Definition: CPComboBox.j:1155
void encodeWithCoder:(CPCoder aCoder)
Definition: CPTextField.j:2090
void textDidChange:(CPNotification note)
Definition: CPTextField.j:1218
BOOL setThemeState:(ThemeState aState)
Definition: CPView.j:3255
CGRect frame
var CPComboBoxListKey
Definition: CPComboBox.j:1153
var isEqual
id< CPComboBoxDelegate > delegate()
Definition: CPComboBox.j:216
void addItemsWithObjectValues:(CPArray objects)
Definition: CPComboBox.j:307
CPComboBoxStateButtonBordered
Definition: CPComboBox.j:51
void setSelectedRange:(CPRange aRange)
Definition: CPTextField.j:1642
void setButtonBordered:(BOOL flag)
Definition: CPComboBox.j:174
var CPComboBoxHasVerticalScrollerKey
Definition: CPComboBox.j:1159
The main run loop for the application.
Definition: CPRunLoop.h:2
var CPComboBoxCompletionTest
Definition: CPComboBox.j:1204
BOOL isButtonBordered()
Definition: CPComboBox.j:169
CPFont font()
Definition: CPControl.j:899
id delegate()
Definition: CALayer.j:965
var CPComboBoxDelegate_comboBoxWillPopUp_
Definition: CPComboBox.j:55
CGRect popupButtonRectForBounds:(CGRect bounds)
Definition: CPComboBox.j:895
CGRect bounds()
Definition: CPView.j:1326
void postNotificationName:object:(CPString aNotificationName, [object] id anObject)
void raise:reason:(CPString aName, [reason] CPString aReason)
Definition: CPException.j:66
CPComboBoxSelectionDidChangeNotification
Definition: CPComboBox.j:46
A collection of unique integers.
Definition: CPIndexSet.h:2
CPString charactersIgnoringModifiers()
Definition: CPEvent.j:392
var CPComboBoxButtonBorderedKey
Definition: CPComboBox.j:1160
void selectItemAtIndex:(int index)
Definition: CPComboBox.j:627
CPNotificationCenter defaultCenter()
A mutable key-value pair collection.
Definition: CPDictionary.h:2
CGRect bounds()
Definition: CALayer.j:203
void paste:(id sender)
Definition: CPTextField.j:1569
CPWindow window()
Definition: CPView.j:527
CPRange selectedRange()
Definition: CPTextField.j:1602
var CPComboBoxNumberOfVisibleItemsKey
Definition: CPComboBox.j:1158
CPComboBoxWillPopUpNotification
Definition: CPComboBox.j:49
var CPComboBoxUsesDataSourceKey
Definition: CPComboBox.j:1156
id initWithName:object:userInfo:(CPString aNotificationName, [object] id anObject, [userInfo] CPDictionary aUserInfo)
function CPMaxRange(aRange)
Definition: CPRange.j:70
An immutable string (collection of characters).
Definition: CPString.h:2
CPRunLoop mainRunLoop()
Definition: CPRunLoop.j:240
BOOL sendAction:to:(SEL anAction, [to] id anObject)
Definition: CPControl.j:319
SEL action()
Definition: CPControl.j:290
id initWithFrame:(CGRect aFrame)
Definition: CPTextField.j:350
CPEscapeFunctionKey
void performBlock:argument:order:modes:(Function aBlock, [argument] id anArgument, [order] int anOrder, [modes] CPArray modes)
Definition: CPRunLoop.j:270
void reloadData()
Definition: CPComboBox.j:513
BOOL performKeyEquivalent:(CPEvent anEvent)
Definition: CPView.j:3054
CPTableView tableView
Definition: CPTableView.j:6205
CGRect rectForEphemeralSubviewNamed:(CPString aName)
Definition: CPTextField.j:1822
CPTextAlignment alignment()
Definition: CPControl.j:784
int length()
Definition: CPString.j:186
void setStringValue:(CPString aString)
Definition: CPControl.j:629
A notification that can be posted to a CPNotificationCenter.
Definition: CPNotification.h:2
var CPComboBoxDelegate_comboBoxSelectionDidChange_
Definition: CPComboBox.j:54
var CPComboBoxCompletesKey
Definition: CPComboBox.j:1157
var CPComboBoxDelegate_comboBoxSelectionIsChanging_
Definition: CPComboBox.j:53
CPTheme defaultTheme()
Definition: CPTheme.j:44
void setUsesDataSource:(BOOL flag)
Definition: CPComboBox.j:289
BOOL takeStringValueFromList()
Definition: CPComboBox.j:557
void setObjectValue:(id aValue)
Definition: CPTextField.j:1310
id target()
Definition: CPControl.j:308
CPString stringValue()
Definition: CPControl.j:613
var CPComboBoxTextSubview
Definition: CPComboBox.j:58
Defines methods for use when archiving & restoring (enc/decoding).
Definition: CPCoder.h:2
CPNotFound
Definition: CPObjJRuntime.j:62
void setAlignment:(CPTextAlignment alignment)
Definition: CPControl.j:776
BOOL unsetThemeState:(ThemeState aState)
Definition: CPView.j:3268
CPString comboBoxCompletedString:(CPString uncompletedString)
Definition: CPComboBox.j:1082
void reloadDataSourceForSelector:(SEL cmd)
Definition: CPComboBox.j:544
Sends messages (CPNotification) between objects.
CPComboBoxSelectionIsChangingNotification
Definition: CPComboBox.j:47
void removeAllItems()
Definition: CPComboBox.j:346
Definition: CPTheme.h:2
CPDownArrowFunctionKey
BOOL resignFirstResponder()
Definition: CPTextField.j:792
BOOL sendAction:to:(SEL anAction, [to] id anObject)
Definition: CPComboBox.j:696
var CPComboBoxDelegate_comboBoxWillDismiss_
Definition: CPComboBox.j:56
Definition: CPEvent.h:2
var CPComboBoxDefaultNumberOfVisibleItems
Definition: CPComboBox.j:60
var CPComboBoxFocusRingWidth
Definition: CPComboBox.j:61
CPComboBoxWillDismissNotification
Definition: CPComboBox.j:48
void popUpList()
Definition: CPComboBox.j:519
CPDeleteCharacter
Definition: CPText.j:56
void layoutSubviews()
Definition: CPTextField.j:1855
id initWithCoder:(CPCoder aCoder)
Definition: CPTextField.j:2060
id indexSetWithIndex:(int anIndex)
Definition: CPIndexSet.j:51
int indexOfItemWithObjectValue:(id anObject)
Definition: CPComboBox.j:482
void setDelegate:(id< CPTextFieldDelegate > aDelegate)
Definition: CPTextField.j:1776
void setListDelegate:(_CPPopUpList aDelegate)
Definition: CPComboBox.j:393
var CPComboBoxDelegateKey
Definition: CPComboBox.j:1154
CPView layoutEphemeralSubviewNamed:positioned:relativeToEphemeralSubviewNamed:(CPString aViewName, [positioned] CPWindowOrderingMode anOrderingMode, [relativeToEphemeralSubviewNamed] CPString relativeToViewName)
Definition: CPView.j:3407
var CPComboBoxItemsKey
Definition: CPComboBox.j:1152
id alloc()
Definition: CPObject.j:130
Definition: CPView.j:137
FrameUpdater prototype description