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