/* * CPComboBox.j * AppKit * * Created by Aparajita Fishman. * Copyright (c) 2012, The Cappuccino Foundation * * 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 "CPText.j" @import "CPTextField.j" @import "_CPPopUpList.j" // TODO : should conform to protocol CPTextFieldDelegate @protocol CPComboBoxDelegate @optional - (void)comboBoxSelectionIsChanging:(CPNotification)aNotification; - (void)comboBoxSelectionDidChange:(CPNotification)aNotification; - (void)comboBoxWillPopUp:(CPNotification)aNotification; - (void)comboBoxWillDismiss:(CPNotification)aNotification; @end @protocol CPComboBoxDataSource @optional - (CPString)comboBox:(CPComboBox)aComboBox completedString:(CPString)uncompletedString; - (id)comboBox:(CPComboBox)aComboBox objectValueForItemAtIndex:(int)index; - (int)comboBox:(CPComboBox)aComboBox indexOfItemWithStringValue:(CPString)stringValue; - (int)numberOfItemsInComboBox:(CPComboBox)aComboBox; @end CPComboBoxSelectionDidChangeNotification = @"CPComboBoxSelectionDidChangeNotification"; CPComboBoxSelectionIsChangingNotification = @"CPComboBoxSelectionIsChangingNotification"; CPComboBoxWillDismissNotification = @"CPComboBoxWillDismissNotification"; CPComboBoxWillPopUpNotification = @"CPComboBoxWillPopUpNotification"; CPComboBoxStateButtonBordered = CPThemeState("button-bordered"); var CPComboBoxDelegate_comboBoxSelectionIsChanging_ = 1 << 0, CPComboBoxDelegate_comboBoxSelectionDidChange_ = 1 << 1, CPComboBoxDelegate_comboBoxWillPopUp_ = 1 << 2, CPComboBoxDelegate_comboBoxWillDismiss_ = 1 << 3; var CPComboBoxTextSubview = @"text", CPComboBoxButtonSubview = @"button", CPComboBoxDefaultNumberOfVisibleItems = 5, CPComboBoxFocusRingWidth = -1; @implementation CPComboBox : CPTextField { BOOL _canComplete; BOOL _completes; BOOL _forceSelection; BOOL _hasVerticalScroller; BOOL _popUpButtonCausedResign; BOOL _usesDataSource; CGSize _intercellSpacing; CPArray _items; id _dataSource; CPInteger _implementedDelegateMethods; CPString _selectedStringValue; float _itemHeight; int _numberOfVisibleItems; _CPPopUpList _listDelegate; } + (CPString)defaultThemeClass { return "combobox"; } + (CPDictionary)themeAttributes { return @{ @"popup-button-size": CGSizeMake(21.0, 29.0), @"border-inset": CGInsetMake(3.0, 3.0, 3.0, 3.0), }; } + (Class)_binderClassForBinding:(CPString)aBinding { if (aBinding === CPContentBinding || aBinding === CPContentValuesBinding) return [_CPComboBoxContentBinder class]; return [super _binderClassForBinding:aBinding]; } - (id)initWithFrame:(CGRect)aFrame { self = [super initWithFrame:aFrame]; if (self) [self _initComboBox]; return self; } - (void)_initComboBox { _items = [CPArray array]; // _listClass = [_CPPopUpList class]; _usesDataSource = NO; _completes = NO; _canComplete = NO; _numberOfVisibleItems = CPComboBoxDefaultNumberOfVisibleItems; _forceSelection = NO; _hasVerticalScroller = YES; _selectedStringValue = @""; _popUpButtonCausedResign = NO; [self setTheme:[CPTheme defaultTheme]]; [self setBordered:YES]; [self setBezeled:YES]; [self setEditable:YES]; [self setThemeState:CPComboBoxStateButtonBordered]; } #pragma mark Setting Display Attributes - (BOOL)hasVerticalScroller { return _hasVerticalScroller; } - (void)setHasVerticalScroller:(BOOL)flag { flag = !!flag; if (_hasVerticalScroller === flag) return; _hasVerticalScroller = flag; if (_listDelegate) [[_listDelegate scrollView] setHasVerticalScroller:_hasVerticalScroller]; } - (CGSize)intercellSpacing { return [[_listDelegate tableView] intercellSpacing]; } - (void)setIntercellSpacing:(CGSize)aSize { if (!aSize || (_intercellSpacing && CGSizeEqualToSize(aSize, _intercellSpacing))) return; _intercellSpacing = aSize; if (_listDelegate) [[_listDelegate tableView] setIntercellSpacing:_intercellSpacing]; } - (BOOL)isButtonBordered { return [self hasThemeState:CPComboBoxStateButtonBordered]; } - (void)setButtonBordered:(BOOL)flag { if (!!flag) [self setThemeState:CPComboBoxStateButtonBordered]; else [self unsetThemeState:CPComboBoxStateButtonBordered]; } - (float)itemHeight { return [[_listDelegate tableView] rowHeight]; } - (void)setItemHeight:(float)itemHeight { if (itemHeight === _itemHeight) return; _itemHeight = itemHeight; if (_listDelegate) { [[_listDelegate tableView] setRowHeight:_itemHeight]; // FIXME: This shouldn't be necessary, but CPTableView does not tile after setRowHeight [[_listDelegate tableView] reloadData]; } } - (int)numberOfVisibleItems { return _numberOfVisibleItems; } - (void)setNumberOfVisibleItems:(int)visibleItems { // There should always be at least 1 visible item! _numberOfVisibleItems = MAX(visibleItems, 1); } #pragma mark Setting a Delegate - (id )delegate { return [super delegate]; } /*! Sets the CPComboBox delegate. Note that although the Cocoa docs say that the delegate must conform to the NSComboBoxDelegate protocol, in actual fact it doesn't. Also note that the same delegate may conform to the NSTextFieldDelegate protocol. */ - (void)setDelegate:(id )aDelegate { var delegate = [self delegate]; if (aDelegate === delegate) return; if (aDelegate) { if ([aDelegate respondsToSelector:@selector(comboBoxSelectionIsChanging:)]) _implementedDelegateMethods |= CPComboBoxDelegate_comboBoxSelectionIsChanging_ if ([aDelegate respondsToSelector:@selector(comboBoxSelectionDidChange:)]) _implementedDelegateMethods |= CPComboBoxDelegate_comboBoxSelectionDidChange_ if ([aDelegate respondsToSelector:@selector(comboBoxWillPopUp:)]) _implementedDelegateMethods |= CPComboBoxDelegate_comboBoxWillPopUp_ if ([aDelegate respondsToSelector:@selector(comboBoxWillDismiss:)]) _implementedDelegateMethods |= CPComboBoxDelegate_comboBoxWillDismiss_ } [super setDelegate:aDelegate]; } #pragma mark Setting a Data Source - (id )dataSource { if (!_usesDataSource) [self _dataSourceWarningForMethod:_cmd condition:NO]; return _dataSource; } - (void)setDataSource:(id )aSource { if (!_usesDataSource) { [self _dataSourceWarningForMethod:_cmd condition:NO]; } else if (_dataSource !== aSource) { if (![aSource respondsToSelector:@selector(numberOfItemsInComboBox:)] || ![aSource respondsToSelector:@selector(comboBox:objectValueForItemAtIndex:)]) { CPLog.warn("Illegal %s data source (%s). Must implement numberOfItemsInComboBox: and comboBox:objectValueForItemAtIndex:", [self className], [aSource description]); } else { _dataSource = aSource; } } } - (BOOL)usesDataSource { return _usesDataSource; } - (void)setUsesDataSource:(BOOL)flag { flag = !!flag; if (_usesDataSource === flag) return; _usesDataSource = flag; // Cocoa empties the internal item list if usesDataSource is YES if (_usesDataSource) [_items removeAllObjects]; [self reloadData]; } #pragma mark Working with an Internal List - (void)addItemsWithObjectValues:(CPArray)objects { [_items addObjectsFromArray:objects]; [self reloadDataSourceForSelector:_cmd]; } - (void)addItemWithObjectValue:(id)anObject { [_items addObject:anObject]; [self reloadDataSourceForSelector:_cmd]; } - (void)insertItemWithObjectValue:(id)anObject atIndex:(int)anIndex { // Issue the warning first, because removeObjectAtIndex may raise if (_usesDataSource) [self _dataSourceWarningForMethod:_cmd condition:YES]; [_items insertObject:anObject atIndex:anIndex]; [self reloadData]; } /*! Returns the internal array of items. NOTE: Unlike Cocoa the array is mutable, since all arrays in Objective-J are mutable. But you should treat it as an immutable array. Do NOT attempt to change the returned array in any way. If usesDataSource is YES, a warning is logged and an empty array is returned. */ - (CPArray)objectValues { if (_usesDataSource) [self _dataSourceWarningForMethod:_cmd condition:YES]; return _items; } - (void)removeAllItems { [_items removeAllObjects]; [self reloadDataSourceForSelector:_cmd]; } - (void)removeItemAtIndex:(int)index { // Issue the warning first, because removeObjectAtIndex may raise if (_usesDataSource) [self _dataSourceWarningForMethod:_cmd condition:YES]; [_items removeObjectAtIndex:index]; [self reloadData]; } - (void)removeItemWithObjectValue:(id)anObject { [_items removeObject:anObject]; [self reloadDataSourceForSelector:_cmd]; } - (int)numberOfItems { if (_usesDataSource) return [_dataSource numberOfItemsInComboBox:self]; else return _items.length; } #pragma mark Manipulating the Displayed List /*! Returns the delegate to be used when creating the pop up list. */ - (_CPPopUpList)listDelegate { return _listDelegate; } /*! Sets the delegate to be used when creating the pop up list. By default this is _CPPopUpList. If you are using a subclass of _CPPopUpList, call this method with your subclass. */ - (void)setListDelegate:(_CPPopUpList)aDelegate { if (_listDelegate === aDelegate) return; [self _removeObserversForListDelegate:_listDelegate]; _listDelegate = aDelegate; // We only add the observers if the CPComboBox is displayed if ([self window]) [self _addObserversForListDelegate:_listDelegate] // Apply our text style to the list [_listDelegate setFont:[self font]]; [_listDelegate setAlignment:[self alignment]]; [self setHasVerticalScroller:_hasVerticalScroller]; [self setIntercellSpacing:_intercellSpacing]; [self setItemHeight:_itemHeight]; } - (void)_addObserversForListDelegate:(_CPPopUpList)aDelegate { if (!aDelegate) return; var defaultCenter = [CPNotificationCenter defaultCenter]; [defaultCenter addObserver:self selector:@selector(comboBoxWillPopUp:) name:_CPPopUpListWillPopUpNotification object:aDelegate]; [defaultCenter addObserver:self selector:@selector(comboBoxWillDismiss:) name:_CPPopUpListWillDismissNotification object:aDelegate]; [defaultCenter addObserver:self selector:@selector(listDidDismiss:) name:_CPPopUpListDidDismissNotification object:aDelegate]; [defaultCenter addObserver:self selector:@selector(itemWasClicked:) name:_CPPopUpListItemWasClickedNotification object:aDelegate]; [[aDelegate scrollView] setHasVerticalScroller:_hasVerticalScroller]; var tableView = [aDelegate tableView]; [defaultCenter addObserver:self selector:@selector(comboBoxSelectionIsChanging:) name:CPTableViewSelectionIsChangingNotification object:tableView]; [defaultCenter addObserver:self selector:@selector(comboBoxSelectionDidChange:) name:CPTableViewSelectionDidChangeNotification object:tableView]; } - (void)_removeObserversForListDelegate:(_CPPopUpList)aDelegate { if (!aDelegate) return; var defaultCenter = [CPNotificationCenter defaultCenter]; [defaultCenter removeObserver:self name:_CPPopUpListWillPopUpNotification object:aDelegate]; [defaultCenter removeObserver:self name:_CPPopUpListWillDismissNotification object:aDelegate]; [defaultCenter removeObserver:self name:_CPPopUpListDidDismissNotification object:aDelegate]; [defaultCenter removeObserver:self name:_CPPopUpListItemWasClickedNotification object:aDelegate]; var oldTableView = [aDelegate tableView]; if (oldTableView) { [defaultCenter removeObserver:self name:CPTableViewSelectionIsChangingNotification object:oldTableView]; [defaultCenter removeObserver:self name:CPTableViewSelectionDidChangeNotification object:oldTableView]; } } - (int)indexOfItemWithObjectValue:(id)anObject { if (_usesDataSource) [self _dataSourceWarningForMethod:_cmd condition:YES]; return [_items indexOfObject:anObject]; } - (id)itemObjectValueAtIndex:(int)index { if (_usesDataSource) [self _dataSourceWarningForMethod:_cmd condition:YES]; return [_items objectAtIndex:index]; } - (void)noteNumberOfItemsChanged { [[_listDelegate tableView] noteNumberOfRowsChanged]; } - (void)scrollItemAtIndexToTop:(int)index { [_listDelegate scrollItemAtIndexToTop:index]; } - (void)scrollItemAtIndexToVisible:(int)index { [[_listDelegate tableView] scrollRowToVisible:index]; } - (void)reloadData { [[_listDelegate tableView] reloadData]; } /*! @ignore */ - (void)popUpList { if (!_listDelegate) [self setListDelegate:[[_CPPopUpList alloc] initWithDataSource:self]]; // Note the offset here is 1 less than the focus ring width because the outer edge // of the focus ring is very transparent and it looks better if the list is closer. if (CPComboBoxFocusRingWidth < 0) { var inset = [self currentValueForThemeAttribute:@"border-inset"]; CPComboBoxFocusRingWidth = inset.bottom; } [_listDelegate popUpRelativeToRect:[self _borderFrame] view:self offset:CPComboBoxFocusRingWidth - 1]; [self _selectMatchingItem]; } /*! @ignore */ - (BOOL)listIsVisible { return _listDelegate ? [_listDelegate isVisible] : NO; } /*! @ignore */ - (void)reloadDataSourceForSelector:(SEL)cmd { if (_usesDataSource) [self _dataSourceWarningForMethod:cmd condition:YES] else [self reloadData]; } /*! If the list is non-empty, sets the value of the field from the currently selected value of the list and returns YES. If the list is empty or the list has no selected item, returns NO. @ignore */ - (BOOL)takeStringValueFromList { if (_usesDataSource && _dataSource && [_dataSource numberOfItemsInComboBox:self] === 0) return NO; var selectedStringValue = [_listDelegate selectedStringValue]; if (selectedStringValue === nil) return NO; else _selectedStringValue = selectedStringValue; [self setStringValue:_selectedStringValue]; [self _reverseSetBinding]; return YES; } /*! The receiver receives this notification when the list is closed. @ignore */ - (void)listDidDismiss:(CPNotification)aNotification { [[self window] makeFirstResponder:self]; } /*! The receiver receives this notification when an item in the list is clicked. @ignore */ - (void)itemWasClicked:(CPNotification)aNotification { [self takeStringValueFromList]; [self sendAction:[self action] to:[self target]]; } #pragma mark Manipulating the Selection - (void)deselectItemAtIndex:(int)index { var table = [_listDelegate tableView], row = [table selectedRow]; if (row !== index) return; [table deselectRow:index]; } - (int)indexOfSelectedItem { return [[_listDelegate tableView] selectedRow]; } - (id)objectValueOfSelectedItem { var row = [[_listDelegate tableView] selectedRow]; if (row >= 0) { if (_usesDataSource) [self _dataSourceWarningForMethod:_cmd condition:YES]; return _items[row]; } return nil; } - (void)selectItemAtIndex:(int)index { var table = [_listDelegate tableView], row = [table selectedRow]; if (row === index) return; [table selectRowIndexes:[CPIndexSet indexSetWithIndex:index] byExtendingSelection:NO]; } - (void)selectItemWithObjectValue:(id)anObject { var index = [self indexOfItemWithObjectValue:anObject]; if (index !== CPNotFound) [self selectItemAtIndex:index]; } #pragma mark Completing the Text Field - (BOOL)completes { return _completes; } - (void)setCompletes:(BOOL)flag { _completes = !!flag; } - (CPString)completedString:(CPString)substring { if (_usesDataSource) return [self comboBoxCompletedString:substring]; else { var index = [_items indexOfObjectPassingTest:CPComboBoxCompletionTest context:substring]; return index !== CPNotFound ? _items[index] : nil; } } /*! Returns whether the combo box forces the user to enter or select an item that is in the item list. */ - (BOOL)forceSelection { return _forceSelection; } /*! Sets whether the combo box forces the user to enter or select an item that is in the item list. If \c flag is \c YES and the user enters a value that is not in the list, when the field loses focus it will revert to the previous value. If \c flag is \c NO, the user can enter any value they wish. Note that this flag is ignored if \ref setStringValue or \ref setObjectValue are called directly. */ - (void)setForceSelection:(BOOL)flag { _forceSelection = !!flag; } #pragma mark CPTextField Delegate Methods and Overrides /*! @ignore */ - (BOOL)sendAction:(SEL)anAction to:(id)anObject { // When the action is sent, be sure to get the value and close the list. // This covers the case where the action is triggered by pressing a key // that triggers the text field action. if ([self listIsVisible]) { [self takeStringValueFromList]; [_listDelegate close]; } return [super sendAction:anAction to:anObject]; } /*! @ignore */ - (void)setObjectValue:(id)object { [super setObjectValue:object]; _selectedStringValue = [self stringValue]; } /*! @ignore */ - (void)interpretKeyEvents:(CPArray)events { var theEvent = events[0]; // Only if characters are added at the end of the value can completion occur _canComplete = NO; if (_completes) { if (![theEvent _couldBeKeyEquivalent] && [theEvent characters].charAt(0) !== CPDeleteCharacter) { var value = [self _inputElement].value, selectedRange = [self selectedRange]; _canComplete = CPMaxRange(selectedRange) === value.length; } } [super interpretKeyEvents:events]; } /*! @ignore */ - (void)paste:(id)sender { if (_completes) { // Completion can occur only if pasting at the end of the value var value = [self _inputElement].value, selectedRange = [self selectedRange]; _canComplete = CPMaxRange(selectedRange) === value.length; } else _canComplete = NO; [super paste:sender]; } /*! @ignore */ - (void)textDidChange:(CPNotification)aNotification { /* Completion is attempted iff: - _completes is YES - Characters were added at the end of the value */ var uncompletedString = [self stringValue], newString = uncompletedString; if (_completes && _canComplete) { newString = [self completedString:uncompletedString]; if (newString && newString.length > uncompletedString.length) { [self setStringValue:newString]; [self setSelectedRange:CPMakeRange(uncompletedString.length, newString.length - uncompletedString.length)]; } } [self _selectMatchingItem]; _canComplete = NO; [super textDidChange:aNotification]; } /*! Override of CPView -performKeyEquivalent @ignore */ - (BOOL)performKeyEquivalent:(CPEvent)anEvent { if ([[self window] firstResponder] === self) { var key = [anEvent charactersIgnoringModifiers]; switch (key) { case CPDownArrowFunctionKey: if (![self listIsVisible]) { [self popUpList]; return YES; } break; case CPEscapeFunctionKey: if ([self listIsVisible]) { // If we are forcing a selection and the user has entered a value which is not // in the list, revert to the most recent valid value. if (_forceSelection && ([self _inputElement].value !== _selectedStringValue)) [self setStringValue:_selectedStringValue]; } break; } if ([_listDelegate performKeyEquivalent:anEvent]) return YES; } return [super performKeyEquivalent:anEvent]; } /*! @ignore */ - (BOOL)resignFirstResponder { var buttonCausedResign = _popUpButtonCausedResign; _popUpButtonCausedResign = NO; /* If the list or popup button is clicked, we lose focus. The list will refuse first responder, and we refuse to resign. But we still have to manually restore the focus to the input element. */ var shouldResign = !buttonCausedResign && (!_listDelegate || [_listDelegate controllingViewShouldResign]); if (!shouldResign) { #if PLATFORM(DOM) // In FireFox this needs to be done in setTimeout, otherwise there is no caret // We have to save the input element now, when we lose focus it will change. var element = [self _inputElement]; window.setTimeout(function() { element.focus(); }, 0); #endif return NO; } // The list was not clicked, we need to close it now [_listDelegate close]; // If the field is empty, allow it to remain empty. // Otherwise restore the most recently selected value if forcing selection. var value = [self stringValue]; if (value) { if (_forceSelection && ![value isEqual:_selectedStringValue]) [self setStringValue:_selectedStringValue]; } else _selectedStringValue = @""; return [super resignFirstResponder]; } - (void)setFont:(CPFont)aFont { [super setFont:aFont]; if (_listDelegate) [_listDelegate setFont:aFont]; } - (void)setAlignment:(CPTextAlignment)alignment { [super setAlignment:alignment]; if (_listDelegate) [_listDelegate setAlignment:alignment]; } #pragma mark Pop Up Button Layout - (CGRect)popupButtonRectForBounds:(CGRect)bounds { var borderInset = [self currentValueForThemeAttribute:@"border-inset"], buttonSize = [self currentValueForThemeAttribute:@"popup-button-size"]; bounds.origin.x = CGRectGetMaxX(bounds) - borderInset.right - buttonSize.width; bounds.origin.y += borderInset.top; bounds.size.width = buttonSize.width; bounds.size.height = buttonSize.height; return bounds; } - (CGRect)rectForEphemeralSubviewNamed:(CPString)aName { if (aName === "popup-button-view") return [self popupButtonRectForBounds:[self bounds]]; return [super rectForEphemeralSubviewNamed:aName]; } - (CPView)createEphemeralSubviewNamed:(CPString)aName { if (aName === "popup-button-view") { var view = [[_CPComboBoxPopUpButton alloc] initWithFrame:CGRectMakeZero() comboBox:self]; return view; } return [super createEphemeralSubviewNamed:aName]; } - (void)layoutSubviews { [super layoutSubviews]; var popupButtonView = [self layoutEphemeralSubviewNamed:@"popup-button-view" positioned:CPWindowAbove relativeToEphemeralSubviewNamed:@"content-view"]; } #pragma mark Internal Helpers /*! @ignore */ - (void)_dataSourceWarningForMethod:(SEL)cmd condition:(CPString)flag { CPLog.warn("-[%s %s] should not be called when usesDataSource is set to %s", [self className], cmd, flag ? "YES" : "NO"); } /*! Select the item that matches the current value of the combobox. @ignore */ - (void)_selectMatchingItem { var index = CPNotFound, stringValue = [self stringValue]; if (_usesDataSource) { if (_dataSource && [_dataSource respondsToSelector:@selector(comboBox:indexOfItemWithStringValue:)]) index = [_dataSource comboBox:self indexOfItemWithStringValue:stringValue] } else { index = [self indexOfItemWithObjectValue:stringValue]; } [_listDelegate selectRow:index]; // selectRow scrolls the row to visible, if a row is selected scroll it to the top if (index !== CPNotFound) { [_listDelegate scrollItemAtIndexToTop:index]; _selectedStringValue = stringValue; } } /*! Calculate the frame in base coordinates that will nestle just below the visible border of the text field. @ignore */ - (CGRect)_borderFrame { var inset = [self currentValueForThemeAttribute:@"border-inset"], frame = [self bounds]; frame.origin.x += inset.left; frame.origin.y += inset.top; frame.size.width -= inset.left + inset.right; frame.size.height -= inset.top + inset.bottom; return frame; } /* @ignore */ - (void)_popUpButtonWasClicked { if (![self isEnabled]) return; // If we are currently the first responder, we will be asked to resign when the list pops up. // Set a flag to let resignResponder know that the button was clicked and we should not resign. var firstResponder = [[self window] firstResponder]; _popUpButtonCausedResign = firstResponder === self; if ([self listIsVisible]) [_listDelegate close]; else { if (firstResponder !== self) [[self window] makeFirstResponder:self]; [self popUpList]; } } #pragma mark - #pragma mark Observers method - (void)_addObservers { if (_isObserving) return; [super _addObservers]; [self _addObserversForListDelegate:_listDelegate]; } - (void)_removeObservers { if (!_isObserving) return; [super _removeObservers]; [self _removeObserversForListDelegate:_listDelegate]; } @end @implementation CPComboBox (CPComboBoxDelegate) /*! @ignore */ - (void)comboBoxSelectionIsChanging:(CPNotification)aNotification { if (_implementedDelegateMethods & CPComboBoxDelegate_comboBoxSelectionIsChanging_) [_delegate comboBoxSelectionIsChanging:[[CPNotification alloc] initWithName:CPComboBoxSelectionIsChangingNotification object:self userInfo:nil]]; [[CPNotificationCenter defaultCenter] postNotificationName:CPComboBoxSelectionIsChangingNotification object:self]; } /*! @ignore */ - (void)comboBoxSelectionDidChange:(CPNotification)aNotification { if (_implementedDelegateMethods & CPComboBoxDelegate_comboBoxSelectionDidChange_) [_delegate comboBoxSelectionDidChange:[[CPNotification alloc] initWithName:CPComboBoxSelectionDidChangeNotification object:self userInfo:nil]]; [[CPNotificationCenter defaultCenter] postNotificationName:CPComboBoxSelectionDidChangeNotification object:self]; } /*! @ignore */ - (void)comboBoxWillPopUp:(CPNotification)aNotification { if (_implementedDelegateMethods & CPComboBoxDelegate_comboBoxWillPopUp_) [_delegate comboBoxWillPopUp:[[CPNotification alloc] initWithName:CPComboBoxWillPopUpNotification object:self userInfo:nil]]; [[CPNotificationCenter defaultCenter] postNotificationName:CPComboBoxWillPopUpNotification object:self]; } /*! @ignore */ - (void)comboBoxWillDismiss:(CPNotification)aNotification { if (_implementedDelegateMethods & CPComboBoxDelegate_comboBoxWillDismiss_) [_delegate comboBoxWillDismiss:[[CPNotification alloc] initWithName:CPComboBoxWillDismissNotification object:self userInfo:nil]]; [[CPNotificationCenter defaultCenter] postNotificationName:CPComboBoxWillDismissNotification object:self]; } @end @implementation CPComboBox (CPComboBoxDataSource) /*! @ignore */ - (CPString)comboBoxCompletedString:(CPString)uncompletedString { if ([_dataSource respondsToSelector:@selector(comboBox:completedString:)]) return [_dataSource comboBox:self completedString:uncompletedString]; else return nil; } @end @implementation CPComboBox (_CPPopUpListDataSource) - (int)numberOfItemsInList:(_CPPopUpList)aList { return [self numberOfItems]; } - (int)numberOfVisibleItemsInList:(_CPPopUpList)aList { return [self numberOfVisibleItems]; } - (id)list:(_CPPopUpList)aList objectValueForItemAtIndex:(int)index { if (_usesDataSource) return [_dataSource comboBox:self objectValueForItemAtIndex:index]; else return _items[index]; } - (id)list:(_CPPopUpList)aList displayValueForObjectValue:(id)aValue { return aValue || @""; } - (CPString)list:(_CPPopUpList)aList stringValueForObjectValue:(id)aValue { return String(aValue); } @end @implementation CPComboBox (Bindings) /*! @ignore */ - (void)setContentValues:(CPArray)anArray { [self setUsesDataSource:NO]; [self removeAllItems]; [self addItemsWithObjectValues:anArray]; } /*! @ignore */ - (void)setContent:(CPArray)anArray { [self setUsesDataSource:NO]; // Directly nuke _items, [_items removeAll] will trigger an extra call to setContent _items = []; var values = []; [anArray enumerateObjectsUsingBlock:function(object) { values.push([object description]); }]; [self addItemsWithObjectValues:values]; } @end var CPComboBoxItemsKey = @"CPComboBoxItemsKey", CPComboBoxListKey = @"CPComboBoxListKey", CPComboBoxDelegateKey = @"CPComboBoxDelegateKey", CPComboBoxDataSourceKey = @"CPComboBoxDataSourceKey", CPComboBoxUsesDataSourceKey = @"CPComboBoxUsesDataSourceKey", CPComboBoxCompletesKey = @"CPComboBoxCompletesKey", CPComboBoxNumberOfVisibleItemsKey = @"CPComboBoxNumberOfVisibleItemsKey", CPComboBoxHasVerticalScrollerKey = @"CPComboBoxHasVerticalScrollerKey", CPComboBoxButtonBorderedKey = @"CPComboBoxButtonBorderedKey"; @implementation CPComboBox (CPCoding) - (id)initWithCoder:(CPCoder)aCoder { self = [super initWithCoder:aCoder]; if (self) { [self _initComboBox]; _items = [aCoder decodeObjectForKey:CPComboBoxItemsKey]; _listDelegate = [aCoder decodeObjectForKey:CPComboBoxListKey]; _delegate = [aCoder decodeObjectForKey:CPComboBoxDelegateKey]; _dataSource = [aCoder decodeObjectForKey:CPComboBoxDataSourceKey]; _usesDataSource = [aCoder decodeBoolForKey:CPComboBoxUsesDataSourceKey]; _completes = [aCoder decodeBoolForKey:CPComboBoxCompletesKey]; _numberOfVisibleItems = [aCoder decodeIntForKey:CPComboBoxNumberOfVisibleItemsKey]; _hasVerticalScroller = [aCoder decodeBoolForKey:CPComboBoxHasVerticalScrollerKey]; [self setButtonBordered:[aCoder decodeBoolForKey:CPComboBoxButtonBorderedKey]]; } return self; } - (void)encodeWithCoder:(CPCoder)aCoder { [super encodeWithCoder:aCoder]; [aCoder encodeObject:_items forKey:CPComboBoxItemsKey]; [aCoder encodeObject:_listDelegate forKey:CPComboBoxListKey]; [aCoder encodeObject:_delegate forKey:CPComboBoxDelegateKey]; [aCoder encodeObject:_dataSource forKey:CPComboBoxDataSourceKey]; [aCoder encodeBool:_usesDataSource forKey:CPComboBoxUsesDataSourceKey]; [aCoder encodeBool:_completes forKey:CPComboBoxCompletesKey]; [aCoder encodeInt:_numberOfVisibleItems forKey:CPComboBoxNumberOfVisibleItemsKey]; [aCoder encodeBool:_hasVerticalScroller forKey:CPComboBoxHasVerticalScrollerKey]; [aCoder encodeBool:[self isButtonBordered] forKey:CPComboBoxButtonBorderedKey]; } @end var CPComboBoxCompletionTest = function(object, index, context) { return object.toString().indexOf(context) === 0; }; /* This class is only used for CPContentBinding and CPContentValuesBinding. */ @implementation _CPComboBoxContentBinder : CPBinder - (void)setValueFor:(CPString)aBinding { var destination = [_info objectForKey:CPObservedObjectKey], keyPath = [_info objectForKey:CPObservedKeyPathKey], options = [_info objectForKey:CPOptionsKey], newValue = [destination valueForKeyPath:keyPath], isPlaceholder = CPIsControllerMarker(newValue); [_source removeAllItems]; if (isPlaceholder) { // By default the placeholders will all result in an empty list switch (newValue) { case CPMultipleValuesMarker: newValue = [options objectForKey:CPMultipleValuesPlaceholderBindingOption] || []; break; case CPNoSelectionMarker: newValue = [options objectForKey:CPNoSelectionPlaceholderBindingOption] || []; break; case CPNotApplicableMarker: if ([options objectForKey:CPRaisesForNotApplicableKeysBindingOption]) [CPException raise:CPGenericException reason:@"can't transform non applicable key on: " + _source + " value: " + newValue]; newValue = [options objectForKey:CPNotApplicablePlaceholderBindingOption] || []; break; case CPNullMarker: newValue = [options objectForKey:CPNullPlaceholderBindingOption] || []; break; } if (![newValue isKindOfClass:[CPArray class]]) newValue = []; } else newValue = [self transformValue:newValue withOptions:options]; switch (aBinding) { case CPContentBinding: [_source setContent:newValue]; break; case CPContentValuesBinding: [_source setContentValues:newValue]; break; } } @end @implementation _CPComboBoxPopUpButton : CPView { CPComboBox _comboBox; } - (id)initWithFrame:(CGRect)aFrame comboBox:(CPComboBox)aComboBox { self = [super initWithFrame:aFrame]; if (self) _comboBox = aComboBox; return self; } - (void)mouseDown:(CPEvent)theEvent { [_comboBox _popUpButtonWasClicked]; } - (BOOL)acceptsFirstResponder { return NO; } @end