Files
cappuccino/AppKit/CPPopUpButton.j
T
David Richardson f468d483ff AppKit, Foundation: Fix view archiving and add regression tests
Keyed archiving preserves view hierarchies created in code. Decoded views
retain their content, subviews and state, and each view class is verified
against its own description before archiving.

* CPAccordionView: Add CPCoding to CPAccordionView and CPAccordionViewItem.

* CPBox:
  * Encode the content, title and box views unconditionally.
  * Clear the superviews of the box and title views during decoding.

* CPButton: Initialize the value to CPOffState.

* CPMapTable: Add CPCoding.

* CPPopUpButton: Clear the inherited button value.

* CPSearchField: Update the cancel button when the object value changes.

* CPSplitView: Return nil from dividerColor when the theme defines no divider
  image.

* CPStepper: Discard the decoded buttons before recreating them.

* CPTabView:
  * Encode the current tab view items.
  * Exclude the tabs from the encoded subviews.

* CPTokenField:
  * Discard the decoded scroll view before recreating it.
  * Add the decoded tokens to the new document view.

* CPVisualEffectView: Resolve the backdrop filter property only when a DOM is
  available.

* CPBoxTest: Verify preservation of the content view, hierarchy and title, and
  decoding of an untitled box without warnings.

* CPButtonTest: Verify the initial value of a button.

* CPMapTableTest: Verify preservation of entries through archiving.

* DropInArchiveFixtures: Add populated view fixtures, tree comparison, and
  round trip and saved archive helpers.

* DropInArchiveGenerator: Save cib descriptions and view archives, and write no
  archive data if any view fails its round trip.

* DropInArchiveTest: Verify new archives, saved archives and reference cibs, and
  treat decoding warnings as failures.

* DropInSnapshot:
  * Describe the view hierarchy through private views, with state and content.
  * Add DropInWithFixedScrollerStyle.

* DropInArchives, DropInCibs: Add saved view archives, cib descriptions and
  reference cibs.
2026-10-31 20:51:13 -06:00

1117 lines
30 KiB
Plaintext

/*
* CPPopUpButton.j
* AppKit
*
* Created by Francisco Tolmasky.
* Copyright 2008, 280 North, Inc.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
@import "CGGeometry.j"
@import "CPButton.j"
@import "CPKeyValueBinding.j"
@import "CPMenu.j"
@import "CPMenuItem.j"
CPPopUpButtonStatePullsDown = CPThemeState("pulls-down");
/*!
@ingroup appkit
@class CPPopUpButton
A CPPopUpButton contains a pop-up menu of items that a user can select from.
*/
@implementation CPPopUpButton : CPButton
{
CPUInteger _selectedIndex;
CPRectEdge _preferredEdge;
}
+ (CPString)defaultThemeClass
{
return "popup-button";
}
+ (CPDictionary)themeAttributes
{
return @{
@"menu-offset": CGSizeMake(0, 0)
};
}
+ (CPSet)keyPathsForValuesAffectingSelectedIndex
{
return [CPSet setWithObject:@"objectValue"];
}
+ (CPSet)keyPathsForValuesAffectingSelectedTag
{
return [CPSet setWithObject:@"objectValue"];
}
+ (CPSet)keyPathsForValuesAffectingSelectedItem
{
return [CPSet setWithObject:@"objectValue"];
}
/*!
Initializes the pop-up button to the specified size.
@param aFrame the size for the button
@param shouldPullDown \c YES makes this a pull-down menu, \c NO makes it a pop-up menu.
@return the initialized pop-up button
*/
- (id)initWithFrame:(CGRect)aFrame pullsDown:(BOOL)shouldPullDown
{
self = [super initWithFrame:aFrame];
if (self)
{
// The value of a pop up button is its selected index, and not a button state.
_value = nil;
[self selectItemAtIndex:CPNotFound];
_preferredEdge = CPMaxYEdge;
[self setValue:CPImageLeft forThemeAttribute:@"image-position"];
[self setValue:CPLeftTextAlignment forThemeAttribute:@"alignment"];
[self setValue:CPLineBreakByTruncatingTail forThemeAttribute:@"line-break-mode"];
[self setMenu:[[CPMenu alloc] initWithTitle:@""]];
[self setPullsDown:shouldPullDown];
var options = CPKeyValueObservingOptionNew | CPKeyValueObservingOptionOld; // | CPKeyValueObservingOptionInitial;
[self addObserver:self forKeyPath:@"menu.items" options:options context:nil];
[self addObserver:self forKeyPath:@"_firstItem.changeCount" options:options context:nil];
[self addObserver:self forKeyPath:@"selectedItem.changeCount" options:options context:nil];
}
return self;
}
- (id)initWithFrame:(CGRect)aFrame
{
return [self initWithFrame:aFrame pullsDown:NO];
}
// Setting the Type of Menu
/*!
Specifies whether the object is a pull-down or a pop-up menu.
If the button pulls down the menu items represent actions, not states.
So the text in the button will NOT change when the user selects something different.
@param shouldPullDown \c YES makes the pop-up button
a pull-down menu. \c NO makes it a pop-up menu.
*/
- (void)setPullsDown:(BOOL)shouldPullDown
{
if (shouldPullDown)
var changed = [self setThemeState:CPPopUpButtonStatePullsDown];
else
var changed = [self unsetThemeState:CPPopUpButtonStatePullsDown];
if (!changed)
return;
var items = [[self menu] itemArray];
if ([items count] <= 0)
return;
[items[0] setHidden:[self pullsDown]];
[self synchronizeTitleAndSelectedItem];
}
/*!
Returns \c YES if the button is a pull-down menu. \c NO if the button is a pop-up menu.
*/
- (BOOL)pullsDown
{
return [self hasThemeState:CPPopUpButtonStatePullsDown];
}
// Inserting and Deleting Items
/*!
Adds a new menu item using a CPMenuItem object.
*/
- (void)addItem:(CPMenuItem)anItem
{
[[self menu] addItem:anItem];
}
/*!
Adds a new menu item with the specified title.
@param the new menu item's title
*/
- (void)addItemWithTitle:(CPString)aTitle
{
[[self menu] addItemWithTitle:aTitle action:NULL keyEquivalent:nil];
}
/*!
Adds multiple new menu items with the titles specified in the provided array.
@param titles an array of names for the new items
*/
- (void)addItemsWithTitles:(CPArray)titles
{
var index = 0,
count = [titles count];
for (; index < count; ++index)
[self addItemWithTitle:titles[index]];
}
/*!
Inserts a new item with the specified title and index location.
@param aTitle the new item's title
@param anIndex the item's index in the menu
*/
- (void)insertItemWithTitle:(CPString)aTitle atIndex:(int)anIndex
{
var items = [self itemArray],
count = [items count];
while (count--)
if ([items[count] title] == aTitle)
[self removeItemAtIndex:count];
[[self menu] insertItemWithTitle:aTitle action:NULL keyEquivalent:nil atIndex:anIndex];
}
/*!
Removes all menu items from the pop-up button's menu
*/
- (void)removeAllItems
{
[[self menu] removeAllItems];
[self synchronizeTitleAndSelectedItem];
}
/*!
Removes a menu item with the specified title from the button.
@param aTitle the title of the item to remove
*/
- (void)removeItemWithTitle:(CPString)aTitle
{
[self removeItemAtIndex:[self indexOfItemWithTitle:aTitle]];
[self synchronizeTitleAndSelectedItem];
}
/*!
Removes the menu item at the specified index
@param anIndex the index of the item to remove
*/
- (void)removeItemAtIndex:(int)anIndex
{
[[self menu] removeItemAtIndex:anIndex];
[self synchronizeTitleAndSelectedItem];
}
// Getting the User's Selection
/*!
Returns the selected item or \c nil if no item is selected.
*/
- (CPMenuItem)selectedItem
{
var indexOfSelectedItem = [self indexOfSelectedItem];
if (indexOfSelectedItem < 0 || indexOfSelectedItem > [self numberOfItems] - 1)
return nil;
return [[self menu] itemAtIndex:indexOfSelectedItem];
}
/*!
Returns the title of the selected item or \c nil if no item is selected.
*/
- (CPString)titleOfSelectedItem
{
return [[self selectedItem] title];
}
/*!
Returns the index of the selected item. If no item is selected, it returns CPNotFound.
*/
- (int)indexOfSelectedItem
{
return _selectedIndex;
}
// Setting the Current Selection
/*!
Selects the specified menu item.
@param aMenuItem the item to select
*/
- (void)selectItem:(CPMenuItem)aMenuItem
{
[self selectItemAtIndex:[self indexOfItem:aMenuItem]];
}
/*!
Selects the item at the specified index
@param anIndex the index of the item to select
*/
- (void)selectItemAtIndex:(CPUInteger)anIndex
{
[self setObjectValue:anIndex];
}
- (void)setSelectedIndex:(CPUInteger)anIndex
{
[self setObjectValue:anIndex];
}
- (CPUInteger)selectedIndex
{
return [self objectValue];
}
/*!
Selects the item at the specified index
@param anIndex the index of the item to select
*/
- (void)setObjectValue:(id)anIndex
{
var indexOfSelectedItem = [self objectValue];
anIndex = parseInt(+anIndex, 10);
if (indexOfSelectedItem === anIndex)
return;
if (indexOfSelectedItem >= 0 && ![self pullsDown])
[[self selectedItem] setState:CPOffState];
_selectedIndex = anIndex;
if (indexOfSelectedItem >= 0 && ![self pullsDown])
[[self selectedItem] setState:CPOnState];
[self synchronizeTitleAndSelectedItem];
}
- (id)objectValue
{
return _selectedIndex;
}
/*!
Selects the menu item with the specified tag
@param the tag of the item to select
*/
- (void)selectItemWithTag:(int)aTag
{
[self selectItemAtIndex:[self indexOfItemWithTag:aTag]];
}
/*!
Selects the item with the specified title
@param the title of the item to select
*/
- (void)selectItemWithTitle:(CPString)aTitle
{
[self selectItemAtIndex:[self indexOfItemWithTitle:aTitle]];
}
// Getting Menu Items
/*!
Returns a count of the number of items in the button's menu.
*/
- (int)numberOfItems
{
return [[self menu] numberOfItems];
}
/*!
Returns an array of the items in the menu
*/
- (CPArray)itemArray
{
return [[self menu] itemArray];
}
/*!
Returns the item at the specified index or \c nil if the item does not exist.
@param anIndex the index of the item to obtain
*/
- (CPMenuItem)itemAtIndex:(CPUInteger)anIndex
{
return [[self menu] itemAtIndex:anIndex];
}
/*!
Returns the title of the item at the specified index or \c nil if no item exists.
@param anIndex the index of the item
*/
- (CPString)itemTitleAtIndex:(CPUInteger)anIndex
{
return [[[self menu] itemAtIndex:anIndex] title];
}
/*!
Returns an array of all the menu item titles.
*/
- (CPArray)itemTitles
{
return [[self itemArray] arrayByApplyingBlock:function(item)
{
return [item title];
}];
}
/*!
Returns the menu item with the specified title.
@param aTitle the title of the desired menu item
*/
- (CPMenuItem)itemWithTitle:(CPString)aTitle
{
var menu = [self menu],
itemIndex = [menu indexOfItemWithTitle:aTitle];
if (itemIndex === CPNotFound)
return nil;
return [menu itemAtIndex:itemIndex];
}
/*!
Returns the last menu item
*/
- (CPMenuItem)lastItem
{
return [[[self menu] itemArray] lastObject];
}
// Getting the Indices of Menu Items
/*!
Returns the index of the specified item or CPNotFound if the item is not in the list.
@param aMenuItem the item to obtain the index for
*/
- (int)indexOfItem:(CPMenuItem)aMenuItem
{
return [[self menu] indexOfItem:aMenuItem];
}
/*!
Returns the index of the item with the specified tag or CPNotFound if the item is not in the list.
@param aTag the item's tag
*/
- (int)indexOfItemWithTag:(int)aTag
{
return [[self menu] indexOfItemWithTag:aTag];
}
/*!
Returns the index of the item with the specified title or CPNotFound.
@param aTitle the item's title
*/
- (int)indexOfItemWithTitle:(CPString)aTitle
{
return [[self menu] indexOfItemWithTitle:aTitle];
}
/*!
Returns the index of the item with the specified
represented object or CPNotFound
if a match does not exist.
@param anObject the item's represented object
*/
- (int)indexOfItemWithRepresentedObject:(id)anObject
{
return [[self menu] indexOfItemWithRepresentedObject:anObject];
}
/*!
Returns the index of the item with the specified target
and action. Returns CPNotFound if the no
such item is in the list.
@param aTarget the item's target
@param anAction the item's action
*/
- (int)indexOfItemWithTarget:(id)aTarget action:(SEL)anAction
{
return [[self menu] indexOfItemWithTarget:aTarget action:anAction];
}
// Setting the Cell Edge to Pop out in Restricted Situations
/*!
Returns the button's edge where the pop-up menu will be
displayed when there is not enough room to display directly
above the button.
*/
- (CPRectEdge)preferredEdge
{
return _preferredEdge;
}
/*!
Sets the preferred edge of the button to display the
pop-up when there is a limited amount of screen space.
By default, the pop-up should draw on top of the button.
*/
- (void)setPreferredEdge:(CPRectEdge)aRectEdge
{
_preferredEdge = aRectEdge;
}
// Setting the Title
/*!
Sets the pop-up button's title.
@param aTitle the new title
*/
- (void)setTitle:(CPString)aTitle
{
if ([self title] === aTitle)
return;
if ([self pullsDown])
{
var items = [[self menu] itemArray];
if ([items count] <= 0)
[self addItemWithTitle:aTitle];
else
{
[items[0] setTitle:aTitle];
[self synchronizeTitleAndSelectedItem];
}
}
else
{
var index = [self indexOfItemWithTitle:aTitle];
if (index < 0)
{
// this ist to match cocoa where setting an empty string does not add it but simply clears the title
// and sets objectValue to -1
if (aTitle === '')
[self selectItemAtIndex:-1];
else
{
[self addItemWithTitle:aTitle];
index = [self numberOfItems] - 1;
}
}
[self selectItemAtIndex:index];
}
}
// Setting the Image
/*!
This method has no effect. Because the image is taken
from the currently selected item, this method serves
no purpose.
*/
- (void)setImage:(CPImage)anImage
{
// The Image is set by the currently selected item.
}
// Setting the State
/*!
Makes sure the selected item and the item
being displayed are one and the same.
*/
- (void)synchronizeTitleAndSelectedItem
{
var item = nil;
if ([self pullsDown])
{
var items = [[self menu] itemArray];
if ([items count] > 0)
item = items[0];
}
else
item = [self selectedItem];
[super setImage:[item image]];
[super setTitle:[item title]];
}
- (void)observeValueForKeyPath:(CPString)aKeyPath ofObject:(id)anObject change:(CPDictionary)changes context:(id)aContext
{
var pullsDown = [self pullsDown];
if (!pullsDown && aKeyPath === @"selectedItem.changeCount" ||
pullsDown && (aKeyPath === @"_firstItem" || aKeyPath === @"_firstItem.changeCount"))
[self synchronizeTitleAndSelectedItem];
// FIXME: This is due to a bug in KVO, we should never get it for "menu".
if (aKeyPath === @"menu")
{
aKeyPath = @"menu.items";
[changes setObject:CPKeyValueChangeSetting forKey:CPKeyValueChangeKindKey];
[changes setObject:[[self menu] itemArray] forKey:CPKeyValueChangeNewKey];
}
if (aKeyPath === @"menu.items")
{
var changeKind = [changes objectForKey:CPKeyValueChangeKindKey],
indexOfSelectedItem = [self indexOfSelectedItem];
if (changeKind === CPKeyValueChangeRemoval)
{
var index = CPNotFound,
indexes = [changes objectForKey:CPKeyValueChangeIndexesKey];
if ([indexes containsIndex:0] && [self pullsDown])
[self _firstItemDidChange];
if (![self pullsDown] && [indexes containsIndex:indexOfSelectedItem])
{
// If the selected item is removed the first item becomes selected.
indexOfSelectedItem = 0;
}
else
{
// See whether the index has changed, despite the actual item not changing.
while ((index = [indexes indexGreaterThanIndex:index]) !== CPNotFound &&
index <= indexOfSelectedItem)
--indexOfSelectedItem;
}
[self selectItemAtIndex:indexOfSelectedItem];
}
else if (changeKind === CPKeyValueChangeReplacement)
{
var indexes = [changes objectForKey:CPKeyValueChangeIndexesKey];
if (pullsDown && [indexes containsIndex:0] ||
!pullsDown && [indexes containsIndex:indexOfSelectedItem])
[self synchronizeTitleAndSelectedItem];
}
else
{
// No matter what, we want to prepare the new items.
var newItems = [changes objectForKey:CPKeyValueChangeNewKey];
[newItems enumerateObjectsUsingBlock:function(aMenuItem)
{
var action = [aMenuItem action];
if (!action)
[aMenuItem setAction:action = @selector(_popUpItemAction:)];
if (action === @selector(_popUpItemAction:))
[aMenuItem setTarget:self];
}];
if (changeKind === CPKeyValueChangeSetting)
{
[self _firstItemDidChange];
[self selectItemAtIndex:CPNotFound];
[self selectItemAtIndex:MIN([newItems count] - 1, indexOfSelectedItem)];
}
else //if (changeKind === CPKeyValueChangeInsertion)
{
var indexes = [changes objectForKey:CPKeyValueChangeIndexesKey];
if ([self pullsDown] && [indexes containsIndex:0])
{
[self _firstItemDidChange];
if ([self numberOfItems] > 1)
{
var index = CPNotFound,
originalIndex = 0;
while ((index = [indexes indexGreaterThanIndex:index]) !== CPNotFound &&
index <= originalIndex)
++originalIndex;
[[self itemAtIndex:originalIndex] setHidden:NO];
}
}
if (indexOfSelectedItem < 0)
[self selectItemAtIndex:0];
else
{
var index = CPNotFound;
// See whether the index has changed, despite the actual item not changing.
while ((index = [indexes indexGreaterThanIndex:index]) !== CPNotFound &&
index <= indexOfSelectedItem)
++indexOfSelectedItem;
[self selectItemAtIndex:indexOfSelectedItem];
}
}
}
}
// [super observeValueForKeyPath:aKeyPath ofObject:anObject change:changes context:aContext];
}
- (void)mouseDown:(CPEvent)anEvent
{
if (![self isEnabled] || ![self numberOfItems])
return;
var menu = [self menu];
// Don't reopen the menu based on the same click which caused it to close, e.g. a click on this button.
if (menu._lastCloseEvent === anEvent)
return;
[self highlight:YES];
var bounds = [self bounds],
minimumWidth = CGRectGetWidth(bounds);
// FIXME: setFont: should set the font on the menu.
[menu setFont:[self font]];
if ([self pullsDown])
{
var positionedItem = nil,
menuOffset = [self currentValueForThemeAttribute:@"menu-offset"],
location = CGPointMake(menuOffset.width, CGRectGetMaxY(bounds) + menuOffset.height);
}
else
{
var contentRect = [self contentRectForBounds:bounds],
positionedItem = [self selectedItem],
standardLeftMargin = [_CPMenuWindow _standardLeftMargin] + [_CPMenuItemStandardView _standardLeftMargin],
menuOffset = [self currentValueForThemeAttribute:@"menu-offset"],
location = CGPointMake(CGRectGetMinX(contentRect) - standardLeftMargin + menuOffset.width, menuOffset.height);
minimumWidth += standardLeftMargin;
// To ensure the selected item is highlighted correctly, unset the highlighted item
[menu _highlightItemAtIndex:CPNotFound];
}
[menu setMinimumWidth:minimumWidth];
[menu
_popUpMenuPositioningItem:positionedItem
atLocation:location
topY:CGRectGetMinY(bounds)
bottomY:CGRectGetMaxY(bounds)
inView:self
callback:function(aMenu)
{
[self highlight:NO];
var highlightedItem = [aMenu highlightedItem];
if ([highlightedItem _isSelectable])
[self selectItem:highlightedItem];
}];
/*
else
{
// This is confusing, I KNOW, so let me explain it to you.
// We want the *content* of the selected menu item to overlap the *content* of our pop up.
// 1. So calculate where our content is, then calculate where the menu item is.
// 2. Move LEFT by whatever indentation we have (offsetWidths, aka, window margin, item margin, etc).
// 3. MOVE UP by the difference in sizes of the content and menu item, this will only work if the content is vertically centered.
var contentRect = [self convertRect:[self contentRectForBounds:bounds] toView:nil],
menuOrigin = [theWindow convertBaseToGlobal:contentRect.origin],
menuItemRect = [menuWindow rectForItemAtIndex:_selectedIndex];
menuOrigin.x -= CGRectGetMinX(menuItemRect) + [menuWindow overlapOffsetWidth] + [[[menu itemAtIndex:_selectedIndex] _menuItemView] overlapOffsetWidth];
menuOrigin.y -= CGRectGetMinY(menuItemRect) + (CGRectGetHeight(menuItemRect) - CGRectGetHeight(contentRect)) / 2.0;
}
*/
}
- (void)rightMouseDown:(CPEvent)anEvent
{
// Disable standard CPView behavior which incorrectly displays the menu as a 'context menu'.
}
- (void)_popUpItemAction:(id)aSender
{
[self sendAction:[self action] to:[self target]];
}
- (void)_firstItemDidChange
{
[self willChangeValueForKey:@"_firstItem"];
[self didChangeValueForKey:@"_firstItem"];
[[self _firstItem] setHidden:YES];
}
- (CPMenuItem)_firstItem
{
if ([self numberOfItems] <= 0)
return nil;
return [[self menu] itemAtIndex:0];
}
- (void)takeValueFromKeyPath:(CPString)aKeyPath ofObjects:(CPArray)objects
{
var count = objects.length,
value = [objects[0] valueForKeyPath:aKeyPath];
[self selectItemWithTag:value];
[self setEnabled:YES];
while (count-- > 1)
if (value !== [objects[count] valueForKeyPath:aKeyPath])
[[self selectedItem] setState:CPOffState];
}
@end
@implementation CPPopUpButton (BindingSupport)
+ (Class)_binderClassForBinding:(CPString)aBinding
{
if (aBinding == CPSelectedIndexBinding ||
aBinding == CPSelectedObjectBinding ||
aBinding == CPSelectedTagBinding ||
aBinding == CPSelectedValueBinding ||
aBinding == CPContentBinding ||
aBinding == CPContentObjectsBinding ||
aBinding == CPContentValuesBinding)
{
var capitalizedBinding = aBinding.charAt(0).toUpperCase() + aBinding.substr(1);
return [CPClassFromString(@"_CPPopUpButton" + capitalizedBinding + "Binder") class];
}
return [super _binderClassForBinding:aBinding];
}
+ (BOOL)isBindingExclusive:(CPString)aBinding
{
return (aBinding == CPSelectedIndexBinding ||
aBinding == CPSelectedTagBinding ||
aBinding == CPSelectedValueBinding);
}
- (void)_reverseSetBinding
{
[_CPPopUpButtonSelectionBinder _reverseSetValueFromExclusiveBinderForObject:self];
[super _reverseSetBinding];
}
@end
@implementation _CPPopUpButtonContentBinder : CPBinder
{
}
- (CPInteger)_getInsertNullOffset
{
var options = [_info objectForKey:CPOptionsKey];
return [options objectForKey:CPInsertsNullPlaceholderBindingOption] ? 1 : 0;
}
- (CPString)_getNullPlaceholder
{
var options = [_info objectForKey:CPOptionsKey],
placeholder = [options objectForKey:CPNullPlaceholderBindingOption] || @"";
if (placeholder === [CPNull null])
placeholder = @"";
return placeholder;
}
- (id)transformValue:(CPArray)contentArray withOptions:(CPDictionary)options
{
// Desactivate the full array transformation forced by super because we don't want this. We want individual transformations (see below).
return contentArray;
}
- (void)setValue:(CPArray)contentArray forBinding:(CPString)aBinding
{
[self _setContent:contentArray];
[self _setContentValuesIfNeeded:contentArray];
}
- (id)valueForBinding:(CPString)aBinding
{
return [self _content];
}
- (void)_setContent:(CPArray)aValue
{
var count = [aValue count],
options = [_info objectForKey:CPOptionsKey],
offset = [self _getInsertNullOffset],
selectedBindingInfo = [_source infoForBinding:CPSelectedObjectBinding],
selectedObject = nil;
if (selectedBindingInfo)
{
var destination = [selectedBindingInfo objectForKey:CPObservedObjectKey],
keyPath = [selectedBindingInfo objectForKey:CPObservedKeyPathKey];
selectedObject = [destination valueForKeyPath:keyPath];
}
if (count + offset != [_source numberOfItems])
{
[_source removeAllItems];
if (offset)
[_source addItemWithTitle:[self _getNullPlaceholder]];
for (var i = 0; i < count; i++)
{
var item = [[CPMenuItem alloc] initWithTitle:@"" action:NULL keyEquivalent:nil],
itemValue = [aValue objectAtIndex:i];
[self _setValue:itemValue forItem:item withOptions:options];
[_source addItem:item];
// Select this item if it is the one selected by the selected object binding
// This is needed if the selected object binding is set before the items
// from the content binding
if (itemValue === selectedObject)
{
[_source setSelectedIndex:[_source numberOfItems] - 1];
}
}
}
else
{
for (var i = 0; i < count; i++)
{
[self _setValue:[aValue objectAtIndex:i] forItem:[_source itemAtIndex:i + offset] withOptions:options];
}
}
}
- (void)_setContentValuesIfNeeded:(CPArray)values
{
var offset = [self _getInsertNullOffset];
if (![_source infoForBinding:CPContentValuesBinding])
{
if (offset)
[[_source itemAtIndex:0] setTitle:[self _getNullPlaceholder]];
var count = [values count];
for (var i = 0; i < count; i++)
[[_source itemAtIndex:i + offset] setTitle:[[values objectAtIndex:i] description]];
}
}
- (void)_setValue:(id)aValue forItem:(CPMenuItem)aMenuItem withOptions:(CPDictionary)options
{
var value = [self _transformValue:aValue withOptions:options];
[aMenuItem setRepresentedObject:value];
}
- (id)_transformValue:(id)aValue withOptions:(CPDictionary)options
{
return [super transformValue:aValue withOptions:options];
}
- (CPArray)_content
{
return [_source valueForKeyPath:@"itemArray.representedObject"];
}
@end
@implementation _CPPopUpButtonContentValuesBinder : _CPPopUpButtonContentBinder
{
}
- (void)setValue:(CPArray)aValue forBinding:(CPString)aBinding
{
[super _setContent:aValue];
}
- (void)_setValue:(id)aValue forItem:(CPMenuItem)aMenuItem withOptions:(CPDictionary)options
{
if (aValue === [CPNull null])
aValue = nil;
var value = [self _transformValue:aValue withOptions:options];
[aMenuItem setTitle:value];
}
- (CPArray)_content
{
return [_source valueForKeyPath:@"itemArray.title"];
}
@end
@implementation _CPPopUpButtonSelectionBinder : CPBinder
{
}
- (void)setPlaceholderValue:(id)aValue withMarker:(CPString)aMarker forBinding:(CPString)aBinding
{
[self setValue:aValue forBinding:aBinding];
}
- (CPInteger)_getInsertNullOffset
{
var options = [[CPBinder infoForBinding:CPContentBinding forObject:_source] objectForKey:CPOptionsKey];
return [options objectForKey:CPInsertsNullPlaceholderBindingOption] ? 1 : 0;
}
@end
@implementation _CPPopUpButtonSelectedIndexBinder : _CPPopUpButtonSelectionBinder
{
}
- (void)setValue:(id)aValue forBinding:(CPString)aBinding
{
[_source selectItemAtIndex:aValue + [self _getInsertNullOffset]];
}
- (id)valueForBinding:(CPString)aBinding
{
return [_source indexOfSelectedItem] - [self _getInsertNullOffset];
}
@end
@implementation _CPPopUpButtonSelectedObjectBinder : _CPPopUpButtonSelectionBinder
{
}
- (void)setValue:(id)aValue forBinding:(CPString)aBinding
{
var index = [_source indexOfItemWithRepresentedObject:aValue],
offset = [self _getInsertNullOffset];
// If the content binding has the option CPNullPlaceholderBindingOption and the object to select is nil, select the first item (i.e., the placeholder).
// Other cases to consider:
// 1. no binding:
// 1.1 there's no item with a represented object matching the object to select.
// 1.2 the object to select is nil/CPNull
// 2. there's a binding:
// 2.1 there's a CPNullPlaceholderBindingOption:
// 2.1.1 there's no item with a represented object matching the object to select?
// 2.1.2 the object to select is nil/CPNull
// 2.2 there's no CPNullPlaceholderBindingOption:
// 2.2.1 there's no item with a represented object matching the object to select?
// 2.2.2 the object to select is nil/CPNull
// More cases? Behaviour that depends on array controller settings?
if (offset === 1 && index === CPNotFound)
index = 0;
[_source selectItemAtIndex:index];
}
- (id)valueForBinding:(CPString)aBinding
{
return [[_source selectedItem] representedObject];
}
@end
@implementation _CPPopUpButtonSelectedTagBinder : _CPPopUpButtonSelectionBinder
{
}
- (void)setValue:(id)aValue forBinding:(CPString)aBinding
{
[_source selectItemWithTag:aValue];
}
- (id)valueForBinding:(CPString)aBinding
{
return [[_source selectedItem] tag];
}
@end
@implementation _CPPopUpButtonSelectedValueBinder : _CPPopUpButtonSelectionBinder
{
}
- (void)setValue:(id)aValue forBinding:(CPString)aBinding
{
[_source selectItemWithTitle:aValue];
}
- (id)valueForBinding:(CPString)aBinding
{
return [_source titleOfSelectedItem];
}
@end
var DEPRECATED_CPPopUpButtonMenuKey = @"CPPopUpButtonMenuKey",
DEPRECATED_CPPopUpButtonSelectedIndexKey = @"CPPopUpButtonSelectedIndexKey";
@implementation CPPopUpButton (CPCoding)
/*!
Initializes the pop-up button with data from the
specified coder.
@param aCoder the coder from which to read
the data
@return the initialized pop-up button
*/
- (id)initWithCoder:(CPCoder)aCoder
{
self = [super initWithCoder:aCoder];
if (self)
{
// FIXME: (or not?) _title is nulled in - [CPButton initWithCoder:],
// so we need to do this again.
[self synchronizeTitleAndSelectedItem];
// FIXME: Remove deprecation leniency for 1.0
if ([aCoder containsValueForKey:DEPRECATED_CPPopUpButtonMenuKey])
{
CPLog.warn(self + " was encoded with an older version of Cappuccino. Please nib2cib the original nib again or open and re-save in Atlas.");
[self setMenu:[aCoder decodeObjectForKey:DEPRECATED_CPPopUpButtonMenuKey]];
[self setObjectValue:[aCoder decodeObjectForKey:DEPRECATED_CPPopUpButtonSelectedIndexKey]];
}
var options = CPKeyValueObservingOptionNew | CPKeyValueObservingOptionOld;/* | CPKeyValueObservingOptionInitial */
[self addObserver:self forKeyPath:@"menu.items" options:options context:nil];
[self addObserver:self forKeyPath:@"_firstItem.changeCount" options:options context:nil];
[self addObserver:self forKeyPath:@"selectedItem.changeCount" options:options context:nil];
}
return self;
}
@end