mirror of
https://github.com/cappuccino/cappuccino.git
synced 2026-09-10 20:57:11 +00:00
954 lines
26 KiB
Plaintext
954 lines
26 KiB
Plaintext
/*
|
|
* CPComboBox.j
|
|
* AppKit
|
|
*
|
|
* Created by Aparajita Fishman.
|
|
* Copyright (c) 2011, Intalio, 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 <AppKit/CPTextField.j>
|
|
@import <AppKit/_CPPopUpList.j>
|
|
|
|
|
|
CPComboBoxSelectionDidChangeNotification = @"CPComboBoxSelectionDidChangeNotification";
|
|
CPComboBoxSelectionIsChangingNotification = @"CPComboBoxSelectionIsChangingNotification";
|
|
CPComboBoxWillDismissNotification = @"CPComboBoxWillDismissNotification";
|
|
CPComboBoxWillPopUpNotification = @"CPComboBoxWillPopUpNotification";
|
|
|
|
var CPComboBoxTextSubview = @"text",
|
|
CPComboBoxButtonSubview = @"button",
|
|
CPComboBoxDefaultNumberOfVisibleItems = 5;
|
|
|
|
|
|
@implementation CPComboBox : CPTextField
|
|
{
|
|
CPArray _items;
|
|
_CPPopUpList _list;
|
|
Class _listClass;
|
|
CPComboBoxDataSource _dataSource;
|
|
BOOL _usesDataSource;
|
|
BOOL _completes;
|
|
BOOL _canComplete;
|
|
int _numberOfVisibleItems;
|
|
BOOL _forceSelection;
|
|
BOOL _hasVerticalScroller;
|
|
CPString _selectedStringValue;
|
|
}
|
|
|
|
- (id)initWithFrame:(CGRect)aFrame
|
|
{
|
|
self = [super initWithFrame:aFrame];
|
|
|
|
if (self)
|
|
[self _init];
|
|
|
|
return self;
|
|
}
|
|
|
|
- (void)_init
|
|
{
|
|
_items = []
|
|
_listClass = [_CPPopUpList class];
|
|
_usesDataSource = NO;
|
|
_completes = NO;
|
|
_canComplete = NO;
|
|
_numberOfVisibleItems = CPComboBoxDefaultNumberOfVisibleItems;
|
|
_forceSelection = NO;
|
|
_hasVerticalScroller = YES;
|
|
_selectedStringValue = @"";
|
|
|
|
[self setTarget:self];
|
|
[self setAction:@selector(textFieldAction:)];
|
|
|
|
[[CPNotificationCenter defaultCenter] addObserver:self selector:@selector(controlTextDidChange:) name:CPControlTextDidChangeNotification object:self];
|
|
}
|
|
|
|
//#pragma mark Setting Display Attributes
|
|
|
|
- (BOOL)hasVerticalScroller
|
|
{
|
|
return _hasVerticalScroller;
|
|
}
|
|
|
|
- (void)setHasVerticalScroller:(BOOL)flag
|
|
{
|
|
flag = !!flag;
|
|
|
|
if (_hasVerticalScroller === flag)
|
|
return;
|
|
|
|
_hasVerticalScroller = flag;
|
|
[[_list scrollView] setHasVerticalScroller:flag];
|
|
}
|
|
|
|
- (CGSize)intercellSpacing
|
|
{
|
|
return [[_list tableView] intercellSpacing];
|
|
}
|
|
|
|
- (void)setIntercellSpacing:(CGSize)aSize
|
|
{
|
|
[[_list tableView] setIntercellSpacing:aSize];
|
|
}
|
|
|
|
- (BOOL)isButtonBordered
|
|
{
|
|
return YES;
|
|
//return [[self ephemeralSubviewNamed:CPComboBoxButtonSubview] isBordered];
|
|
}
|
|
|
|
- (void)setButtonBordered:(BOOL)flag
|
|
{
|
|
//[[self ephemeralSubviewNamed:CPComboBoxButtonSubview] setBordered:flag];
|
|
}
|
|
|
|
- (float)itemHeight
|
|
{
|
|
return [[_list tableView] rowHeight];
|
|
}
|
|
|
|
- (void)setItemHeight:(float)itemHeight
|
|
{
|
|
[[_list tableView] setRowHeight:itemHeight];
|
|
}
|
|
|
|
- (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 < CPComboBoxDelegate >)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 < CPComboBoxDelegate >)aDelegate
|
|
{
|
|
var delegate = [self delegate];
|
|
|
|
if (aDelegate === delegate)
|
|
return;
|
|
|
|
var defaultCenter = [CPNotificationCenter defaultCenter];
|
|
|
|
if (delegate)
|
|
{
|
|
[defaultCenter removeObserver:delegate name:CPComboBoxSelectionIsChangingNotification object:self];
|
|
[defaultCenter removeObserver:delegate name:CPComboBoxSelectionDidChangeNotification object:self];
|
|
[defaultCenter removeObserver:delegate name:CPComboBoxWillDismissNotification object:self];
|
|
[defaultCenter removeObserver:delegate name:CPComboBoxWillPopUpNotification object:self];
|
|
}
|
|
|
|
if (aDelegate)
|
|
{
|
|
if ([aDelegate respondsToSelector:@selector(comboBoxSelectionIsChanging:)])
|
|
[defaultCenter addObserver:delegate
|
|
selector:@selector(comboBoxSelectionIsChanging:)
|
|
name:CPComboBoxSelectionIsChangingNotification
|
|
object:self];
|
|
|
|
if ([aDelegate respondsToSelector:@selector(comboBoxSelectionDidChange:)])
|
|
[defaultCenter addObserver:delegate
|
|
selector:@selector(comboBoxSelectionDidChange:)
|
|
name:CPComboBoxSelectionDidChangeNotification
|
|
object:self];
|
|
|
|
if ([aDelegate respondsToSelector:@selector(comboBoxWillPopUp:)])
|
|
[defaultCenter addObserver:delegate
|
|
selector:@selector(comboBoxWillPopUp:)
|
|
name:CPComboBoxWillPopUpNotification
|
|
object:self];
|
|
|
|
if ([aDelegate respondsToSelector:@selector(comboBoxWillDismiss:)])
|
|
[defaultCenter addObserver:delegate
|
|
selector:@selector(comboBoxWillDissmis:)
|
|
name:CPComboBoxWillDismissNotification
|
|
object:self];
|
|
}
|
|
|
|
[super setDelegate:aDelegate];
|
|
}
|
|
|
|
//#pragma mark Setting a Data Source
|
|
|
|
- (id < CPComboBoxDataSource >)dataSource
|
|
{
|
|
if (_usesDataSource === NO)
|
|
[self dataSourceWarningForMethod:_cmd condition:NO];
|
|
|
|
return _dataSource;
|
|
}
|
|
|
|
- (void)setDataSource:(id < CPComboBoxDataSource >)aSource
|
|
{
|
|
if (_usesDataSource === NO)
|
|
[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 that this does a deep copy of the array,
|
|
effectively returning an immutable array to be consistent with Cocoa.
|
|
|
|
If usesDataSource is YES, a warning is logged and an empty array is returned.
|
|
*/
|
|
- (CPArray)objectValues
|
|
{
|
|
if (_usesDataSource)
|
|
[self dataSourceWarningForMethod:_cmd condition:YES];
|
|
|
|
return [[CPArray alloc] initWithArray:_items copyItems:YES];
|
|
}
|
|
|
|
- (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 helper class to be used when creating the pop up list.
|
|
*/
|
|
- (Class)listClass
|
|
{
|
|
return _listClass;
|
|
}
|
|
|
|
/*!
|
|
Sets the helper class 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)setListClass:(Class)aClass
|
|
{
|
|
_listClass = aClass;
|
|
}
|
|
|
|
- (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
|
|
{
|
|
[[_list tableView] noteNumberOfRowsChanged];
|
|
}
|
|
|
|
- (void)scrollItemAtIndexToTop:(int)index
|
|
{
|
|
[_list scrollItemAtIndexToTop:index];
|
|
}
|
|
|
|
- (void)scrollItemAtIndexToVisible:(int)index
|
|
{
|
|
[[_list tableView] scrollRowToVisible:index];
|
|
}
|
|
|
|
- (void)reloadData
|
|
{
|
|
[[_list tableView] reloadData];
|
|
}
|
|
|
|
/*! @ignore */
|
|
- (void)popUpList
|
|
{
|
|
if (!_list)
|
|
[self makeList];
|
|
|
|
[self selectMatchingItem];
|
|
[_list popUpRelativeTo:[self borderFrame]];
|
|
}
|
|
|
|
/*! @ignore */
|
|
- (_CPPopUpList)makeList
|
|
{
|
|
var defaultCenter = [CPNotificationCenter defaultCenter];
|
|
|
|
if (_list)
|
|
{
|
|
[defaultCenter removeObserver:self name:_CPPopUpListWillPopUpNotification object:_list];
|
|
[defaultCenter removeObserver:self name:_CPPopUpListWillDismissNotification object:_list];
|
|
[defaultCenter removeObserver:self name:_CPPopUpListDidDismissNotification object:_list];
|
|
[defaultCenter removeObserver:self name:_CPPopUpListItemWasClickedNotification object:_list];
|
|
}
|
|
|
|
_list = [[_listClass alloc] initWithDelegate:self];
|
|
|
|
[defaultCenter addObserver:self
|
|
selector:@selector(listWillPopUp:)
|
|
name:_CPPopUpListWillPopUpNotification
|
|
object:_list];
|
|
|
|
[defaultCenter addObserver:self
|
|
selector:@selector(listWillDismiss:)
|
|
name:_CPPopUpListWillDismissNotification
|
|
object:_list];
|
|
|
|
[defaultCenter addObserver:self
|
|
selector:@selector(listDidDismiss:)
|
|
name:_CPPopUpListDidDismissNotification
|
|
object:_list];
|
|
|
|
[defaultCenter addObserver:self
|
|
selector:@selector(itemWasClicked:)
|
|
name:_CPPopUpListItemWasClickedNotification
|
|
object:_list];
|
|
|
|
[[_list scrollView] setHasVerticalScroller:_hasVerticalScroller];
|
|
|
|
var tableView = [_list tableView];
|
|
|
|
[defaultCenter addObserver:self
|
|
selector:@selector(comboBoxSelectionIsChanging:)
|
|
name:CPTableViewSelectionIsChangingNotification
|
|
object:tableView];
|
|
|
|
[defaultCenter addObserver:self
|
|
selector:@selector(comboBoxSelectionDidChange:)
|
|
name:CPTableViewSelectionDidChangeNotification
|
|
object:tableView];
|
|
}
|
|
|
|
/*! @ignore */
|
|
- (BOOL)listIsVisible
|
|
{
|
|
return _list ? [_list 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, immediately returns NO.
|
|
*/
|
|
- (BOOL)takeStringValueFromList
|
|
{
|
|
if ([_dataSource numberOfItemsInComboBox:self] === 0)
|
|
return NO;
|
|
|
|
_selectedStringValue = [_list selectedStringValue] || @"";
|
|
|
|
[self setStringValue:_selectedStringValue];
|
|
return YES;
|
|
}
|
|
|
|
/*!
|
|
The field receives this notification when the list is about to be pop up.
|
|
*/
|
|
- (void)listWillPopUp:(CPNotification)aNotification
|
|
{
|
|
[self comboBoxWillPopUp];
|
|
}
|
|
|
|
/*!
|
|
The field receives this notification when the list is about to be dismissed.
|
|
*/
|
|
- (void)listWillDismiss:(CPNotification)aNotification
|
|
{
|
|
[self comboBoxWillDismiss];
|
|
}
|
|
|
|
/*!
|
|
The field receives this notification when the list is closed.
|
|
*/
|
|
- (void)listDidDismiss:(CPNotification)aNotification
|
|
{
|
|
[[self window] makeFirstResponder:self];
|
|
}
|
|
|
|
/*!
|
|
The field receives this notification when an item in the list is clicked.
|
|
*/
|
|
- (void)itemWasClicked:(CPNotification)aNotification
|
|
{
|
|
[self takeStringValueFromList];
|
|
}
|
|
|
|
//#pragma mark Manipulating the Selection
|
|
|
|
- (void)deselectItemAtIndex:(int)index
|
|
{
|
|
var table = [_list tableView],
|
|
row = [table selectedRow];
|
|
|
|
if (row !== index)
|
|
return;
|
|
|
|
[table deselectRow:index];
|
|
}
|
|
|
|
- (int)indexOfSelectedItem
|
|
{
|
|
return [[_list tableView] selectedRow];
|
|
}
|
|
|
|
- (id)objectValueOfSelectedItem
|
|
{
|
|
var row = [[_list tableView] selectedRow];
|
|
|
|
if (row >= 0)
|
|
{
|
|
if (_usesDataSource)
|
|
[self dataSourceWarningForMethod:_cmd condition:YES];
|
|
|
|
return _items[row];
|
|
}
|
|
|
|
return nil;
|
|
}
|
|
|
|
- (void)selectItemAtIndex:(int)index
|
|
{
|
|
var table = [_list 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.
|
|
*/
|
|
- (void)setForceSelection:(BOOL)flag
|
|
{
|
|
_forceSelection = !!flag;
|
|
}
|
|
|
|
//#pragma mark CPTextField Delegate Methods and Overrides
|
|
|
|
- (void)keyDown:(CPEvent)anEvent
|
|
{
|
|
// Only if characters are added at the end of the value can completion occur
|
|
_canComplete = NO;
|
|
|
|
if (_completes)
|
|
{
|
|
if (![anEvent _couldBeKeyEquivalent] && [anEvent characters].charAt(0) !== CPDeleteCharacter)
|
|
{
|
|
var value = [self _inputElement].value,
|
|
selectedRange = [self selectedRange];
|
|
|
|
_canComplete = CPMaxRange(selectedRange) === value.length;
|
|
}
|
|
}
|
|
|
|
[super keyDown:anEvent];
|
|
}
|
|
|
|
- (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];
|
|
}
|
|
|
|
- (void)controlTextDidChange:(CPNotification)anNotification
|
|
{
|
|
/*
|
|
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;
|
|
}
|
|
|
|
- (BOOL)performKeyEquivalent:(CPEvent)anEvent
|
|
{
|
|
if ([[self window] firstResponder] === self)
|
|
{
|
|
var key = [anEvent charactersIgnoringModifiers];
|
|
|
|
switch (key)
|
|
{
|
|
case CPDownArrowFunctionKey:
|
|
if ([self listIsVisible])
|
|
[_list selectNextItem];
|
|
else
|
|
[self popUpList];
|
|
|
|
return YES;
|
|
|
|
case CPUpArrowFunctionKey:
|
|
if ([self listIsVisible])
|
|
{
|
|
[_list selectPreviousItem];
|
|
return YES;
|
|
}
|
|
break;
|
|
|
|
case CPEscapeFunctionKey:
|
|
if ([self listIsVisible])
|
|
{
|
|
if (_forceSelection)
|
|
[self setStringValue:_selectedStringValue];
|
|
|
|
[_list close];
|
|
return YES;
|
|
}
|
|
break;
|
|
|
|
case CPPageUpFunctionKey:
|
|
if ([self listIsVisible])
|
|
{
|
|
[_list scrollPageUp];
|
|
return YES;
|
|
}
|
|
break;
|
|
|
|
case CPPageDownFunctionKey:
|
|
if ([self listIsVisible])
|
|
{
|
|
[_list scrollPageDown];
|
|
return YES;
|
|
}
|
|
break;
|
|
|
|
case CPHomeFunctionKey:
|
|
if ([self listIsVisible])
|
|
{
|
|
[_list scrollToTop];
|
|
return YES;
|
|
}
|
|
break;
|
|
|
|
case CPEndFunctionKey:
|
|
if ([self listIsVisible])
|
|
{
|
|
[_list scrollToBottom];
|
|
return YES;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
return [super performKeyEquivalent:anEvent];
|
|
}
|
|
|
|
- (BOOL)resignFirstResponder
|
|
{
|
|
/*
|
|
If the list is clicked, we lose focus. In that case the list will refuse first responder,
|
|
and we refuse to resign. But we still have to manually restore the focus to the input element.
|
|
*/
|
|
if ([_list listWasClicked])
|
|
{
|
|
/*
|
|
If an item was not clicked (probably the scrollbar), clear the click flag so that future
|
|
clicks outside the list will allow it to close. It isn't so great doing that here, but
|
|
the sequence of events is such that it has to be done here.
|
|
*/
|
|
if (![_list itemWasClicked])
|
|
[_list setListWasClicked:NO];
|
|
|
|
//#if PLATFORM(DOM)
|
|
[self _inputElement].focus();
|
|
//#endif
|
|
|
|
return NO;
|
|
}
|
|
|
|
// The list was not clicked, we need to close it now
|
|
[_list 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];
|
|
}
|
|
|
|
/*!
|
|
We override this so that we can know when the field is about to lose focus
|
|
because the user pressed tab.
|
|
*/
|
|
- (CPView)nextValidKeyView
|
|
{
|
|
if ([self listIsVisible])
|
|
[self takeStringValueFromList];
|
|
|
|
return [super nextValidKeyView];
|
|
}
|
|
|
|
/*!
|
|
We override this so that we can know when the field is about to lose focus
|
|
because the user pressed shift tab.
|
|
*/
|
|
- (CPView)previousValidKeyView
|
|
{
|
|
if ([self listIsVisible])
|
|
[self takeStringValueFromList];
|
|
|
|
return [super previousValidKeyView];
|
|
}
|
|
|
|
//#pragma mark Internal Helpers
|
|
|
|
- (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");
|
|
}
|
|
|
|
- (id)objectValueForItemAtIndex:(int)index
|
|
{
|
|
return [_dataSource comboBox:self objectValueForItemAtIndex:index];
|
|
}
|
|
|
|
/*!
|
|
Select the item that matches the current value of the combobox.
|
|
*/
|
|
- (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];
|
|
|
|
[_list selectRow:index];
|
|
|
|
// selectRow scrolls the row to visible, if a row is selected scroll it to the top
|
|
if (index !== CPNotFound)
|
|
{
|
|
[_list scrollRowToTop:index];
|
|
_selectedStringValue = stringValue;
|
|
}
|
|
}
|
|
|
|
- (BOOL)textFieldAction:(id)sender
|
|
{
|
|
if ([self listIsVisible])
|
|
{
|
|
[self takeStringValueFromList];
|
|
[_list close];
|
|
}
|
|
|
|
return YES;
|
|
}
|
|
|
|
/*!
|
|
@ignore
|
|
|
|
Calculate the frame in base coordinates that will nestle just below the visible border of the text field.
|
|
*/
|
|
- (CGRect)borderFrame
|
|
{
|
|
// Calculate the focus inset by subtracting the focused bezel inset
|
|
// from the unfocused bezel inset
|
|
var unfocusedInset = [self valueForThemeAttribute:@"bezel-inset" inState:CPThemeStateBezeled],
|
|
focusedInset = [self valueForThemeAttribute:@"bezel-inset" inState:CPThemeStateBezeled | CPThemeStateEditing],
|
|
focusInset = CGInsetDifference(unfocusedInset, focusedInset),
|
|
bounds = [self convertRectToBase:[self bounds]];
|
|
|
|
bounds.origin.x += focusInset.left;
|
|
bounds.origin.y += focusInset.top;
|
|
bounds.size.width -= focusInset.left + focusInset.right;
|
|
bounds.size.height -= focusInset.top + focusInset.bottom;
|
|
|
|
return bounds;
|
|
}
|
|
|
|
@end
|
|
|
|
@implementation CPComboBox (CPComboBoxDelegate)
|
|
|
|
- (void)comboBoxSelectionIsChanging:(CPNotification)aNotification
|
|
{
|
|
[[CPNotificationCenter defaultCenter] postNotificationName:CPComboBoxSelectionIsChangingNotification object:self];
|
|
}
|
|
|
|
- (void)comboBoxSelectionDidChange:(CPNotification)aNotification
|
|
{
|
|
[[CPNotificationCenter defaultCenter] postNotificationName:CPComboBoxSelectionDidChangeNotification object:self];
|
|
}
|
|
|
|
- (void)comboBoxWillPopUp
|
|
{
|
|
[[CPNotificationCenter defaultCenter] postNotificationName:CPComboBoxWillPopUpNotification object:self];
|
|
}
|
|
|
|
- (void)comboBoxWillDismiss
|
|
{
|
|
[[CPNotificationCenter defaultCenter] postNotificationName:CPComboBoxWillDismissNotification object:self];
|
|
}
|
|
|
|
@end
|
|
|
|
@implementation CPComboBox (CPComboBoxDataSource)
|
|
|
|
- (CPString)comboBoxCompletedString:(CPString)uncompletedString
|
|
{
|
|
if ([_dataSource respondsToSelector:@selector(comboBox:completedString:)])
|
|
return [_dataSource comboBox:self completedString:uncompletedString];
|
|
else
|
|
return nil;
|
|
}
|
|
|
|
@end
|
|
|
|
var CPComboBoxItemsKey = @"CPComboBoxItemsKey",
|
|
CPComboBoxListKey = @"CPComboBoxListKey",
|
|
CPComboBoxDelegateKey = @"CPComboBoxDelegateKey",
|
|
CPComboBoxDataSourceKey = @"CPComboBoxDataSourceKey",
|
|
CPComboBoxUsesDataSourceKey = @"CPComboBoxUsesDataSourceKey",
|
|
CPComboBoxCompletesKey = @"CPComboBoxCompletesKey",
|
|
CPComboBoxNumberOfVisibleItemsKey = @"CPComboBoxNumberOfVisibleItemsKey",
|
|
CPComboBoxHasVerticalScrollerKey = @"CPComboBoxHasVerticalScrollerKey";
|
|
|
|
@implementation CPComboBox (CPCoding)
|
|
|
|
- (id)initWithCoder:(CPCoder)aCoder
|
|
{
|
|
self = [super initWithCoder:aCoder];
|
|
|
|
if (self)
|
|
{
|
|
[self _init];
|
|
|
|
_items = [aCoder decodeObjectForKey:CPComboBoxItemsKey];
|
|
_list = [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];
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
- (void)encodeWithCoder:(CPCoder)aCoder
|
|
{
|
|
[super encodeWithCoder:aCoder];
|
|
|
|
[aCoder encodeObject:_items forKey:CPComboBoxItemsKey];
|
|
[aCoder encodeObject:_list 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];
|
|
}
|
|
|
|
@end
|
|
|
|
|
|
var CPComboBoxCompletionTest = function(object, index, context)
|
|
{
|
|
return object.toString().indexOf(context) === 0;
|
|
};
|