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