API  0.9.6
 All Classes Files Functions Variables Macros Groups Pages
CPPopUpButton.j
Go to the documentation of this file.
1 /*
2  * CPPopUpButton.j
3  * AppKit
4  *
5  * Created by Francisco Tolmasky.
6  * Copyright 2008, 280 North, Inc.
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 var VISIBLE_MARGIN = 7.0;
26 
28 
35 @implementation CPPopUpButton : CPButton
36 {
37  CPUInteger _selectedIndex;
38  CPRectEdge _preferredEdge;
39 }
40 
41 + (CPString)defaultThemeClass
42 {
43  return "popup-button";
44 }
45 
46 + (CPSet)keyPathsForValuesAffectingSelectedIndex
47 {
48  return [CPSet setWithObject:@"objectValue"];
49 }
50 
51 + (CPSet)keyPathsForValuesAffectingSelectedTag
52 {
53  return [CPSet setWithObject:@"objectValue"];
54 }
55 
56 + (CPSet)keyPathsForValuesAffectingSelectedItem
57 {
58  return [CPSet setWithObject:@"objectValue"];
59 }
60 
67 - (id)initWithFrame:(CGRect)aFrame pullsDown:(BOOL)shouldPullDown
68 {
69  self = [super initWithFrame:aFrame];
70 
71  if (self)
72  {
73  [self selectItemAtIndex:CPNotFound];
74 
75  _preferredEdge = CPMaxYEdge;
76 
77  [self setValue:CPImageLeft forThemeAttribute:@"image-position"];
78  [self setValue:CPLeftTextAlignment forThemeAttribute:@"alignment"];
79  [self setValue:CPLineBreakByTruncatingTail forThemeAttribute:@"line-break-mode"];
80 
81  [self setMenu:[[CPMenu alloc] initWithTitle:@""]];
82 
83  [self setPullsDown:shouldPullDown];
84 
85  var options = CPKeyValueObservingOptionNew | CPKeyValueObservingOptionOld; // | CPKeyValueObservingOptionInitial;
86  [self addObserver:self forKeyPath:@"menu.items" options:options context:nil];
87  [self addObserver:self forKeyPath:@"_firstItem.changeCount" options:options context:nil];
88  [self addObserver:self forKeyPath:@"selectedItem.changeCount" options:options context:nil];
89  }
90 
91  return self;
92 }
93 
94 - (id)initWithFrame:(CGRect)aFrame
95 {
96  return [self initWithFrame:aFrame pullsDown:NO];
97 }
98 
99 // Setting the Type of Menu
100 
109 - (void)setPullsDown:(BOOL)shouldPullDown
110 {
111  if (shouldPullDown)
112  var changed = [self setThemeState:CPPopUpButtonStatePullsDown];
113  else
114  var changed = [self unsetThemeState:CPPopUpButtonStatePullsDown];
115 
116  if (!changed)
117  return;
118 
119  var items = [[self menu] itemArray];
120 
121  if ([items count] <= 0)
122  return;
123 
124  [items[0] setHidden:[self pullsDown]];
125 
127 }
128 
132 - (BOOL)pullsDown
133 {
134  return [self hasThemeState:CPPopUpButtonStatePullsDown];
135 }
136 
137 // Inserting and Deleting Items
138 
142 - (void)addItem:(CPMenuItem)anItem
143 {
144  [[self menu] addItem:anItem];
145 }
146 
151 - (void)addItemWithTitle:(CPString)aTitle
152 {
153  [[self menu] addItemWithTitle:aTitle action:NULL keyEquivalent:nil];
154 }
155 
160 - (void)addItemsWithTitles:(CPArray)titles
161 {
162  var index = 0,
163  count = [titles count];
164 
165  for (; index < count; ++index)
166  [self addItemWithTitle:titles[index]];
167 }
168 
174 - (void)insertItemWithTitle:(CPString)aTitle atIndex:(int)anIndex
175 {
176  var items = [self itemArray],
177  count = [items count];
178 
179  while (count--)
180  if ([items[count] title] == aTitle)
181  [self removeItemAtIndex:count];
182 
183  [[self menu] insertItemWithTitle:aTitle action:NULL keyEquivalent:nil atIndex:anIndex];
184 }
185 
189 - (void)removeAllItems
190 {
191  [[self menu] removeAllItems];
193 }
194 
199 - (void)removeItemWithTitle:(CPString)aTitle
200 {
201  [self removeItemAtIndex:[self indexOfItemWithTitle:aTitle]];
203 }
204 
209 - (void)removeItemAtIndex:(int)anIndex
210 {
211  [[self menu] removeItemAtIndex:anIndex];
213 }
214 
215 // Getting the User's Selection
219 - (CPMenuItem)selectedItem
220 {
221  var indexOfSelectedItem = [self indexOfSelectedItem];
222 
223  if (indexOfSelectedItem < 0 || indexOfSelectedItem > [self numberOfItems] - 1)
224  return nil;
225 
226  return [[self menu] itemAtIndex:indexOfSelectedItem];
227 }
228 
232 - (CPString)titleOfSelectedItem
233 {
234  return [[self selectedItem] title];
235 }
236 
240 - (int)indexOfSelectedItem
241 {
242  return _selectedIndex;
243 }
244 
245 // Setting the Current Selection
250 - (void)selectItem:(CPMenuItem)aMenuItem
251 {
252  [self selectItemAtIndex:[self indexOfItem:aMenuItem]];
253 }
254 
259 - (void)selectItemAtIndex:(CPUInteger)anIndex
260 {
261  [self setObjectValue:anIndex];
262 }
263 
264 - (void)setSelectedIndex:(CPUInteger)anIndex
265 {
266  [self setObjectValue:anIndex];
267 }
268 
269 - (CPUInteger)selectedIndex
270 {
271  return [self objectValue];
272 }
273 
278 - (void)setObjectValue:(int)anIndex
279 {
280  var indexOfSelectedItem = [self objectValue];
281 
282  anIndex = parseInt(+anIndex, 10);
283 
284  if (indexOfSelectedItem === anIndex)
285  return;
286 
287  if (indexOfSelectedItem >= 0 && ![self pullsDown])
288  [[self selectedItem] setState:CPOffState];
289 
290  _selectedIndex = anIndex;
291 
292  if (indexOfSelectedItem >= 0 && ![self pullsDown])
293  [[self selectedItem] setState:CPOnState];
294 
296 }
297 
298 - (id)objectValue
299 {
300  return _selectedIndex;
301 }
302 
307 - (void)selectItemWithTag:(int)aTag
308 {
309  [self selectItemAtIndex:[self indexOfItemWithTag:aTag]];
310 }
311 
316 - (void)selectItemWithTitle:(CPString)aTitle
317 {
318  [self selectItemAtIndex:[self indexOfItemWithTitle:aTitle]];
319 }
320 
321 // Getting Menu Items
322 
326 - (int)numberOfItems
327 {
328  return [[self menu] numberOfItems];
329 }
330 
334 - (CPArray)itemArray
335 {
336  return [[self menu] itemArray];
337 }
338 
343 - (CPMenuItem)itemAtIndex:(unsigned)anIndex
344 {
345  return [[self menu] itemAtIndex:anIndex];
346 }
347 
352 - (CPString)itemTitleAtIndex:(unsigned)anIndex
353 {
354  return [[[self menu] itemAtIndex:anIndex] title];
355 }
356 
360 - (CPArray)itemTitles
361 {
362  var titles = [],
363  items = [self itemArray],
364  index = 0,
365  count = [items count];
366 
367  for (; index < count; ++index)
368  titles.push([items[index] title]);
369 
370  return titles;
371 }
372 
377 - (CPMenuItem)itemWithTitle:(CPString)aTitle
378 {
379  var menu = [self menu],
380  itemIndex = [menu indexOfItemWithTitle:aTitle];
381 
382  if (itemIndex === CPNotFound)
383  return nil;
384 
385  return [menu itemAtIndex:itemIndex];
386 }
387 
391 - (CPMenuItem)lastItem
392 {
393  return [[[self menu] itemArray] lastObject];
394 }
395 
396 // Getting the Indices of Menu Items
401 - (int)indexOfItem:(CPMenuItem)aMenuItem
402 {
403  return [[self menu] indexOfItem:aMenuItem];
404 }
405 
410 - (int)indexOfItemWithTag:(int)aTag
411 {
412  return [[self menu] indexOfItemWithTag:aTag];
413 }
414 
419 - (int)indexOfItemWithTitle:(CPString)aTitle
420 {
421  return [[self menu] indexOfItemWithTitle:aTitle];
422 }
423 
430 - (int)indexOfItemWithRepresentedObject:(id)anObject
431 {
432  return [[self menu] indexOfItemWithRepresentedObject:anObject];
433 }
434 
442 - (int)indexOfItemWithTarget:(id)aTarget action:(SEL)anAction
443 {
444  return [[self menu] indexOfItemWithTarget:aTarget action:anAction];
445 }
446 
447 // Setting the Cell Edge to Pop out in Restricted Situations
453 - (CPRectEdge)preferredEdge
454 {
455  return _preferredEdge;
456 }
457 
463 - (void)setPreferredEdge:(CPRectEdge)aRectEdge
464 {
465  _preferredEdge = aRectEdge;
466 }
467 
468 // Setting the Title
473 - (void)setTitle:(CPString)aTitle
474 {
475  if ([self title] === aTitle)
476  return;
477 
478  if ([self pullsDown])
479  {
480  var items = [[self menu] itemArray];
481 
482  if ([items count] <= 0)
483  [self addItemWithTitle:aTitle];
484 
485  else
486  {
487  [items[0] setTitle:aTitle];
489  }
490  }
491  else
492  {
493  var index = [self indexOfItemWithTitle:aTitle];
494 
495  if (index < 0)
496  {
497  [self addItemWithTitle:aTitle];
498 
499  index = [self numberOfItems] - 1;
500  }
501 
502  [self selectItemAtIndex:index];
503  }
504 }
505 
506 // Setting the Image
512 - (void)setImage:(CPImage)anImage
513 {
514  // The Image is set by the currently selected item.
515 }
516 
517 // Setting the State
522 - (void)synchronizeTitleAndSelectedItem
523 {
524  var item = nil;
525 
526  if ([self pullsDown])
527  {
528  var items = [[self menu] itemArray];
529 
530  if ([items count] > 0)
531  item = items[0];
532  }
533  else
534  item = [self selectedItem];
535 
536  [super setImage:[item image]];
537  [super setTitle:[item title]];
538 }
539 
540 - (void)observeValueForKeyPath:(CPString)aKeyPath ofObject:(id)anObject change:(CPDictionary)changes context:(id)aContext
541 {
542  var pullsDown = [self pullsDown];
543 
544  if (!pullsDown && aKeyPath === @"selectedItem.changeCount" ||
545  pullsDown && (aKeyPath === @"_firstItem" || aKeyPath === @"_firstItem.changeCount"))
547 
548  // FIXME: This is due to a bug in KVO, we should never get it for "menu".
549  if (aKeyPath === @"menu")
550  {
551  aKeyPath = @"menu.items";
552 
553  [changes setObject:CPKeyValueChangeSetting forKey:CPKeyValueChangeKindKey];
554  [changes setObject:[[self menu] itemArray] forKey:CPKeyValueChangeNewKey];
555  }
556 
557  if (aKeyPath === @"menu.items")
558  {
559  var changeKind = [changes objectForKey:CPKeyValueChangeKindKey],
560  indexOfSelectedItem = [self indexOfSelectedItem];
561 
562  if (changeKind === CPKeyValueChangeRemoval)
563  {
564  var index = CPNotFound,
565  indexes = [changes objectForKey:CPKeyValueChangeIndexesKey];
566 
567  if ([indexes containsIndex:0] && [self pullsDown])
568  [self _firstItemDidChange];
569 
570  if (![self pullsDown] && [indexes containsIndex:indexOfSelectedItem])
571  {
572  // If the selected item is removed the first item becomes selected.
573  indexOfSelectedItem = 0;
574  }
575  else
576  {
577  // See whether the index has changed, despite the actual item not changing.
578  while ((index = [indexes indexGreaterThanIndex:index]) !== CPNotFound &&
579  index <= indexOfSelectedItem)
580  --indexOfSelectedItem;
581  }
582 
583  [self selectItemAtIndex:indexOfSelectedItem];
584  }
585 
586  else if (changeKind === CPKeyValueChangeReplacement)
587  {
588  var indexes = [changes objectForKey:CPKeyValueChangeIndexesKey];
589 
590  if (pullsDown && [indexes containsIndex:0] ||
591  !pullsDown && [indexes containsIndex:indexOfSelectedItem])
593  }
594 
595  else
596  {
597  // No matter what, we want to prepare the new items.
598  var newItems = [changes objectForKey:CPKeyValueChangeNewKey];
599 
600  [newItems enumerateObjectsUsingBlock:function(aMenuItem)
601  {
602  var action = [aMenuItem action];
603 
604  if (!action)
605  [aMenuItem setAction:action = @selector(_popUpItemAction:)];
606 
607  if (action === @selector(_popUpItemAction:))
608  [aMenuItem setTarget:self];
609  }];
610 
611  if (changeKind === CPKeyValueChangeSetting)
612  {
613  [self _firstItemDidChange];
614 
615  [self selectItemAtIndex:CPNotFound];
616  [self selectItemAtIndex:MIN([newItems count] - 1, indexOfSelectedItem)];
617  }
618 
619  else //if (changeKind === CPKeyValueChangeInsertion)
620  {
621  var indexes = [changes objectForKey:CPKeyValueChangeIndexesKey];
622 
623  if ([self pullsDown] && [indexes containsIndex:0])
624  {
625  [self _firstItemDidChange];
626 
627  if ([self numberOfItems] > 1)
628  {
629  var index = CPNotFound,
630  originalIndex = 0;
631 
632  while ((index = [indexes indexGreaterThanIndex:index]) !== CPNotFound &&
633  index <= originalIndex)
634  ++originalIndex;
635 
636  [[self itemAtIndex:originalIndex] setHidden:NO];
637  }
638  }
639 
640  if (indexOfSelectedItem < 0)
641  [self selectItemAtIndex:0];
642 
643  else
644  {
645  var index = CPNotFound;
646 
647  // See whether the index has changed, despite the actual item not changing.
648  while ((index = [indexes indexGreaterThanIndex:index]) !== CPNotFound &&
649  index <= indexOfSelectedItem)
650  ++indexOfSelectedItem;
651 
652  [self selectItemAtIndex:indexOfSelectedItem];
653  }
654  }
655  }
656  }
657 
658 // [super observeValueForKeyPath:aKeyPath ofObject:anObject change:changes context:aContext];
659 }
660 
661 - (void)mouseDown:(CPEvent)anEvent
662 {
663  if (![self isEnabled] || ![self numberOfItems])
664  return;
665 
666  [self highlight:YES];
667 
668  var menu = [self menu],
669  bounds = [self bounds],
670  minimumWidth = CGRectGetWidth(bounds);
671 
672  // FIXME: setFont: should set the font on the menu.
673  [menu setFont:[self font]];
674 
675  if ([self pullsDown])
676  {
677  var positionedItem = nil,
678  location = CGPointMake(0.0, CGRectGetMaxY(bounds));
679  }
680  else
681  {
682  var contentRect = [self contentRectForBounds:bounds],
683  positionedItem = [self selectedItem],
684  standardLeftMargin = [_CPMenuWindow _standardLeftMargin] + [_CPMenuItemStandardView _standardLeftMargin],
685  location = CGPointMake(CGRectGetMinX(contentRect) - standardLeftMargin, 0.0);
686 
687  minimumWidth += standardLeftMargin;
688 
689  // To ensure the selected item is highlighted correctly, unset the highlighted item
690  [menu _highlightItemAtIndex:CPNotFound];
691  }
692 
693  [menu setMinimumWidth:minimumWidth];
694 
695  [menu
696  _popUpMenuPositioningItem:positionedItem
697  atLocation:location
698  topY:CGRectGetMinY(bounds)
699  bottomY:CGRectGetMaxY(bounds)
700  inView:self
701  callback:function(aMenu)
702  {
703  [self highlight:NO];
704 
705  var highlightedItem = [aMenu highlightedItem];
706 
707  if ([highlightedItem _isSelectable])
708  [self selectItem:highlightedItem];
709  }];
710 /*
711  else
712  {
713  // This is confusing, I KNOW, so let me explain it to you.
714  // We want the *content* of the selected menu item to overlap the *content* of our pop up.
715  // 1. So calculate where our content is, then calculate where the menu item is.
716  // 2. Move LEFT by whatever indentation we have (offsetWidths, aka, window margin, item margin, etc).
717  // 3. MOVE UP by the difference in sizes of the content and menu item, this will only work if the content is vertically centered.
718  var contentRect = [self convertRect:[self contentRectForBounds:bounds] toView:nil],
719  menuOrigin = [theWindow convertBaseToGlobal:contentRect.origin],
720  menuItemRect = [menuWindow rectForItemAtIndex:_selectedIndex];
721 
722  menuOrigin.x -= CGRectGetMinX(menuItemRect) + [menuWindow overlapOffsetWidth] + [[[menu itemAtIndex:_selectedIndex] _menuItemView] overlapOffsetWidth];
723  menuOrigin.y -= CGRectGetMinY(menuItemRect) + (CGRectGetHeight(menuItemRect) - CGRectGetHeight(contentRect)) / 2.0;
724  }
725 */
726 }
727 
728 - (void)rightMouseDown:(CPEvent)anEvent
729 {
730  // Disable standard CPView behavior which incorrectly displays the menu as a 'context menu'.
731 }
732 
733 - (void)_popUpItemAction:(id)aSender
734 {
735  [self sendAction:[self action] to:[self target]];
736 }
737 
738 - (void)_firstItemDidChange
739 {
740  [self willChangeValueForKey:@"_firstItem"];
741  [self didChangeValueForKey:@"_firstItem"];
742 
743  [[self _firstItem] setHidden:YES];
744 }
745 
746 - (CPMenuItem)_firstItem
747 {
748  if ([self numberOfItems] <= 0)
749  return nil;
750 
751  return [[self menu] itemAtIndex:0];
752 }
753 
754 - (void)takeValueFromKeyPath:(CPString)aKeyPath ofObjects:(CPArray)objects
755 {
756  var count = objects.length,
757  value = [objects[0] valueForKeyPath:aKeyPath];
758 
759  [self selectItemWithTag:value];
760  [self setEnabled:YES];
761 
762  while (count-- > 1)
763  if (value !== [objects[count] valueForKeyPath:aKeyPath])
764  [[self selectedItem] setState:CPOffState];
765 }
766 
767 - (void)_reverseSetBinding
768 {
769  [_CPPopUpButtonSelectionBinder reverseSetValueForObject:self];
770 
771  [super _reverseSetBinding];
772 }
773 
774 @end
775 
777 
778 + (Class)_binderClassForBinding:(CPString)aBinding
779 {
780  if (aBinding == CPSelectedIndexBinding ||
781  aBinding == CPSelectedObjectBinding ||
782  aBinding == CPSelectedTagBinding ||
783  aBinding == CPSelectedValueBinding ||
784  aBinding == CPContentBinding ||
785  aBinding == CPContentObjectsBinding ||
786  aBinding == CPContentValuesBinding)
787  {
788  var capitalizedBinding = aBinding.charAt(0).toUpperCase() + aBinding.substr(1);
789 
790  return [CPClassFromString(@"_CPPopUpButton" + capitalizedBinding + "Binder") class];
791  }
792 
793  return [super _binderClassForBinding:aBinding];
794 }
795 
796 @end
797 @implementation _CPPopUpButtonContentBinder : CPBinder
798 {
799  id __doxygen__;
800 }
801 
802 - (CPInteger)_getInsertNullOffset
803 {
804  var options = [_info objectForKey:CPOptionsKey];
805 
806  return [options objectForKey:CPInsertsNullPlaceholderBindingOption] ? 1 : 0;
807 }
808 
809 - (CPString)_getNullPlaceholder
810 {
811  var options = [_info objectForKey:CPOptionsKey],
812  placeholder = [options objectForKey:CPNullPlaceholderBindingOption] || @"";
813 
814  if (placeholder === [CPNull null])
815  placeholder = @"";
816 
817  return placeholder;
818 }
819 
820 - (id)transformValue:(CPArray)contentArray withOptions:(CPDictionary)options
821 {
822  // Desactivate the full array transformation forced by super because we don't want this. We want individual transformations (see below).
823  return contentArray;
824 }
825 
826 - (void)setValue:(CPArray)contentArray forBinding:(CPString)aBinding
827 {
828  [self _setContent:contentArray];
829  [self _setContentValuesIfNeeded:contentArray];
830 }
831 
832 - (void)valueForBinding:(CPString)aBinding
833 {
834  return [self _content];
835 }
836 
837 - (void)_setContent:(CPArray)aValue
838 {
839  var count = [aValue count],
840  options = [_info objectForKey:CPOptionsKey],
841  offset = [self _getInsertNullOffset];
842 
843  if (count + offset != [_source numberOfItems])
844  {
845  [_source removeAllItems];
846 
847  if (offset)
848  [_source addItemWithTitle:[self _getNullPlaceholder]];
849 
850  for (var i = 0; i < count; i++)
851  {
852  var item = [[CPMenuItem alloc] initWithTitle:@"" action:NULL keyEquivalent:nil];
853  [self _setValue:[aValue objectAtIndex:i] forItem:item withOptions:options];
854  [_source addItem:item];
855  }
856  }
857  else
858  {
859  for (var i = 0; i < count; i++)
860  {
861  [self _setValue:[aValue objectAtIndex:i] forItem:[_source itemAtIndex:i + offset] withOptions:options];
862  }
863  }
864 }
865 
866 - (void)_setContentValuesIfNeeded:(CPArray)values
867 {
868  var offset = [self _getInsertNullOffset];
869 
870  if (![_source infoForBinding:CPContentValuesBinding])
871  {
872  if (offset)
873  [[_source itemAtIndex:0] setTitle:[self _getNullPlaceholder]];
874 
875  var count = [values count];
876 
877  for (var i = 0; i < count; i++)
878  [[_source itemAtIndex:i + offset] setTitle:[[values objectAtIndex:i] description]];
879  }
880 }
881 
882 - (void)_setValue:(id)aValue forItem:(CPMenuItem)aMenuItem withOptions:(CPDictionary)options
883 {
884  var value = [self _transformValue:aValue withOptions:options];
885  [aMenuItem setRepresentedObject:value];
886 }
887 
888 - (id)_transformValue:(id)aValue withOptions:(CPDictionary)options
889 {
890  return [super transformValue:aValue withOptions:options];
891 }
892 
893 - (CPArray)_content
894 {
895  return [_source valueForKeyPath:@"itemArray.representedObject"];
896 }
897 
898 @end
899 @implementation _CPPopUpButtonContentValuesBinder : _CPPopUpButtonContentBinder
900 {
901  id __doxygen__;
902 }
903 
904 - (void)setValue:(id)aValue forBinding:(CPString)aBinding
905 {
906  [super _setContent:aValue];
907 }
908 
909 - (void)_setValue:(id)aValue forItem:(CPMenuItem)aMenuItem withOptions:(CPDictionary)options
910 {
911  if (aValue === [CPNull null])
912  aValue = nil;
913 
914  var value = [self _transformValue:aValue withOptions:options];
915  [aMenuItem setTitle:value];
916 }
917 
918 - (CPArray)_content
919 {
920  return [_source valueForKeyPath:@"itemArray.title"];
921 }
922 
923 @end
924 
926 
927 @implementation _CPPopUpButtonSelectionBinder : CPBinder
928 {
929  CPString _selectionBinding;
930 }
931 
932 - (id)initWithBinding:(CPString)aBinding name:(CPString)aName to:(id)aDestination keyPath:(CPString)aKeyPath options:(CPDictionary)options from:(id)aSource
933 {
934  self = [super initWithBinding:aBinding name:aName to:aDestination keyPath:aKeyPath options:options from:aSource];
935 
936  if (self)
937  {
938  binderForObject[[aSource UID]] = self;
939  _selectionBinding = aName;
940  }
941 
942  return self;
943 }
944 
945 + (void)reverseSetValueForObject:(id)aSource
946 {
947  var binder = binderForObject[[aSource UID]];
948  [binder reverseSetValueFor:[binder _selectionBinding]];
949 }
950 
951 - (void)setPlaceholderValue:(id)aValue withMarker:(CPString)aMarker forBinding:(CPString)aBinding
952 {
953  [self setValue:aValue forBinding:aBinding];
954 }
955 
956 - (CPInteger)_getInsertNullOffset
957 {
958  var options = [[CPBinder infoForBinding:CPContentBinding forObject:_source] objectForKey:CPOptionsKey];
959 
960  return [options objectForKey:CPInsertsNullPlaceholderBindingOption] ? 1 : 0;
961 }
962 
963 @end
964 @implementation _CPPopUpButtonSelectedIndexBinder : _CPPopUpButtonSelectionBinder
965 {
966  id __doxygen__;
967 }
968 
969 - (void)setValue:(id)aValue forBinding:(CPString)aBinding
970 {
971  [_source selectItemAtIndex:aValue + [self _getInsertNullOffset]];
972 }
973 
974 - (id)valueForBinding:(CPString)aBinding
975 {
976  return [_source indexOfSelectedItem] - [self _getInsertNullOffset];
977 }
978 
979 @end
980 @implementation _CPPopUpButtonSelectedObjectBinder : _CPPopUpButtonSelectionBinder
981 {
982  id __doxygen__;
983 }
984 
985 - (void)setValue:(id)aValue forBinding:(CPString)aBinding
986 {
987  var index = [_source indexOfItemWithRepresentedObject:aValue],
988  offset = [self _getInsertNullOffset];
989 
990  // If the content binding has the option CPNullPlaceholderBindingOption and the object to select is nil, select the first item (i.e., the placeholder).
991  // Other cases to consider:
992  // 1. no binding:
993  // 1.1 there's no item with a represented object matching the object to select.
994  // 1.2 the object to select is nil/CPNull
995  // 2. there's a binding:
996  // 2.1 there's a CPNullPlaceholderBindingOption:
997  // 2.1.1 there's no item with a represented object matching the object to select?
998  // 2.1.2 the object to select is nil/CPNull
999  // 2.2 there's no CPNullPlaceholderBindingOption:
1000  // 2.2.1 there's no item with a represented object matching the object to select?
1001  // 2.2.2 the object to select is nil/CPNull
1002  // More cases? Behaviour that depends on array controller settings?
1003 
1004  if (offset === 1 && index === CPNotFound)
1005  index = 0;
1006 
1007  [_source selectItemAtIndex:index];
1008 }
1009 
1010 - (id)valueForBinding:(CPString)aBinding
1011 {
1012  return [[_source selectedItem] representedObject];
1013 }
1014 
1015 @end
1016 @implementation _CPPopUpButtonSelectedTagBinder : _CPPopUpButtonSelectionBinder
1017 {
1018  id __doxygen__;
1019 }
1020 
1021 - (void)setValue:(id)aValue forBinding:(CPString)aBinding
1022 {
1023  [_source selectItemWithTag:aValue];
1024 }
1025 
1026 - (id)valueForBinding:(CPString)aBinding
1027 {
1028  return [[_source selectedItem] tag];
1029 }
1030 
1031 @end
1032 @implementation _CPPopUpButtonSelectedValueBinder : _CPPopUpButtonSelectionBinder
1033 {
1034  id __doxygen__;
1035 }
1036 
1037 - (void)setValue:(id)aValue forBinding:(CPString)aBinding
1038 {
1039  [_source selectItemWithTitle:aValue];
1040 }
1041 
1042 - (id)valueForBinding:(CPString)aBinding
1043 {
1044  return [_source titleOfSelectedItem];
1045 }
1046 
1047 @end
1048 
1049 var DEPRECATED_CPPopUpButtonMenuKey = @"CPPopUpButtonMenuKey",
1050  DEPRECATED_CPPopUpButtonSelectedIndexKey = @"CPPopUpButtonSelectedIndexKey";
1051 
1060 - (id)initWithCoder:(CPCoder)aCoder
1061 {
1062  self = [super initWithCoder:aCoder];
1063 
1064  if (self)
1065  {
1066  // FIXME: (or not?) _title is nulled in - [CPButton initWithCoder:],
1067  // so we need to do this again.
1069 
1070  // FIXME: Remove deprecation leniency for 1.0
1071  if ([aCoder containsValueForKey:DEPRECATED_CPPopUpButtonMenuKey])
1072  {
1073  CPLog.warn(self + " was encoded with an older version of Cappuccino. Please nib2cib the original nib again or open and re-save in Atlas.");
1074 
1075  [self setMenu:[aCoder decodeObjectForKey:DEPRECATED_CPPopUpButtonMenuKey]];
1076  [self setObjectValue:[aCoder decodeObjectForKey:DEPRECATED_CPPopUpButtonSelectedIndexKey]];
1077  }
1078 
1079  var options = CPKeyValueObservingOptionNew | CPKeyValueObservingOptionOld;/* | CPKeyValueObservingOptionInitial */
1080 
1081  [self addObserver:self forKeyPath:@"menu.items" options:options context:nil];
1082  [self addObserver:self forKeyPath:@"_firstItem.changeCount" options:options context:nil];
1083  [self addObserver:self forKeyPath:@"selectedItem.changeCount" options:options context:nil];
1084  }
1085 
1086  return self;
1087 }
1088 
1089 @end