scaffold.libs_as.feathers.controls.PickerList.as Maven / Gradle / Ivy
/*
Feathers
Copyright 2012-2015 Bowler Hat LLC. All Rights Reserved.
This program is free software. You can redistribute and/or modify it in
accordance with the terms of the accompanying license agreement.
*/
package feathers.controls
{
import feathers.controls.popups.CalloutPopUpContentManager;
import feathers.controls.popups.DropDownPopUpContentManager;
import feathers.controls.popups.IPersistentPopUpContentManager;
import feathers.controls.popups.IPopUpContentManager;
import feathers.controls.popups.IPopUpContentManagerWithPrompt;
import feathers.controls.popups.VerticalCenteredPopUpContentManager;
import feathers.core.FeathersControl;
import feathers.core.IFocusDisplayObject;
import feathers.core.IToggle;
import feathers.core.PropertyProxy;
import feathers.data.ListCollection;
import feathers.events.CollectionEventType;
import feathers.events.FeathersEventType;
import feathers.skins.IStyleProvider;
import feathers.system.DeviceCapabilities;
import flash.ui.Keyboard;
import starling.core.Starling;
import starling.events.Event;
import starling.events.EventDispatcher;
import starling.events.KeyboardEvent;
import starling.events.Touch;
import starling.events.TouchEvent;
import starling.events.TouchPhase;
import starling.utils.SystemUtil;
/**
* Dispatched when the pop-up list is opened.
*
* The properties of the event object have the following values:
*
* Property Value
* bubbles
false
* currentTarget
The Object that defines the
* event listener that handles the event. For example, if you use
* myButton.addEventListener()
to register an event listener,
* myButton is the value of the currentTarget
.
* data
null
* target
The Object that dispatched the event;
* it is not always the Object listening for the event. Use the
* currentTarget
property to always access the Object
* listening for the event.
*
*
* @eventType starling.events.Event.OPEN
*/
[Event(name="open",type="starling.events.Event")]
/**
* Dispatched when the pop-up list is closed.
*
* The properties of the event object have the following values:
*
* Property Value
* bubbles
false
* currentTarget
The Object that defines the
* event listener that handles the event. For example, if you use
* myButton.addEventListener()
to register an event listener,
* myButton is the value of the currentTarget
.
* data
null
* target
The Object that dispatched the event;
* it is not always the Object listening for the event. Use the
* currentTarget
property to always access the Object
* listening for the event.
*
*
* @eventType starling.events.Event.CLOSE
*/
[Event(name="close",type="starling.events.Event")]
/**
* Dispatched when the selected item changes.
*
* The properties of the event object have the following values:
*
* Property Value
* bubbles
false
* currentTarget
The Object that defines the
* event listener that handles the event. For example, if you use
* myButton.addEventListener()
to register an event listener,
* myButton is the value of the currentTarget
.
* data
null
* target
The Object that dispatched the event;
* it is not always the Object listening for the event. Use the
* currentTarget
property to always access the Object
* listening for the event.
*
*
* @eventType starling.events.Event.CHANGE
*/
[Event(name="change",type="starling.events.Event")]
/**
* Displays a button that may be triggered to display a pop-up list.
* The list may be customized to display in different ways, such as a
* drop-down, in a Callout
, or as a modal overlay.
*
* The following example creates a picker list, gives it a data provider,
* tells the item renderer how to interpret the data, and listens for when
* the selection changes:
*
*
* var list:PickerList = new PickerList();
*
* list.dataProvider = new ListCollection(
* [
* { text: "Milk", thumbnail: textureAtlas.getTexture( "milk" ) },
* { text: "Eggs", thumbnail: textureAtlas.getTexture( "eggs" ) },
* { text: "Bread", thumbnail: textureAtlas.getTexture( "bread" ) },
* { text: "Chicken", thumbnail: textureAtlas.getTexture( "chicken" ) },
* ]);
*
* list.listProperties.itemRendererFactory = function():IListItemRenderer
* {
* var renderer:DefaultListItemRenderer = new DefaultListItemRenderer();
* renderer.labelField = "text";
* renderer.iconSourceField = "thumbnail";
* return renderer;
* };
*
* list.addEventListener( Event.CHANGE, list_changeHandler );
*
* this.addChild( list );
*
* @see ../../../help/picker-list.html How to use the Feathers PickerList component
*/
public class PickerList extends FeathersControl implements IFocusDisplayObject
{
/**
* @private
*/
protected static const INVALIDATION_FLAG_BUTTON_FACTORY:String = "buttonFactory";
/**
* @private
*/
protected static const INVALIDATION_FLAG_LIST_FACTORY:String = "listFactory";
/**
* The default value added to the styleNameList
of the button.
*
* @see feathers.core.FeathersControl#styleNameList
*/
public static const DEFAULT_CHILD_STYLE_NAME_BUTTON:String = "feathers-picker-list-button";
/**
* The default value added to the styleNameList
of the pop-up
* list.
*
* @see feathers.core.FeathersControl#styleNameList
*/
public static const DEFAULT_CHILD_STYLE_NAME_LIST:String = "feathers-picker-list-list";
/**
* The default IStyleProvider
for all PickerList
* components.
*
* @default null
* @see feathers.core.FeathersControl#styleProvider
*/
public static var globalStyleProvider:IStyleProvider;
/**
* @private
*/
protected static function defaultButtonFactory():Button
{
return new Button();
}
/**
* @private
*/
protected static function defaultListFactory():List
{
return new List();
}
/**
* Constructor.
*/
public function PickerList()
{
super();
}
/**
* The default value added to the styleNameList
of the
* button. This variable is protected
so that sub-classes
* can customize the button style name in their constructors instead of
* using the default style name defined by
* DEFAULT_CHILD_STYLE_NAME_BUTTON
.
*
* To customize the button style name without subclassing, see
* customButtonStyleName
.
*
* @see #customButtonStyleName
* @see feathers.core.FeathersControl#styleNameList
*/
protected var buttonStyleName:String = DEFAULT_CHILD_STYLE_NAME_BUTTON;
/**
* The default value added to the styleNameList
of the
* pop-up list. This variable is protected
so that
* sub-classes can customize the list style name in their constructors
* instead of using the default style name defined by
* DEFAULT_CHILD_STYLE_NAME_LIST
.
*
* To customize the pop-up list name without subclassing, see
* customListStyleName
.
*
* @see #customListStyleName
* @see feathers.core.FeathersControl#styleNameList
*/
protected var listStyleName:String = DEFAULT_CHILD_STYLE_NAME_LIST;
/**
* The button sub-component.
*
* For internal use in subclasses.
*
* @see #buttonFactory
* @see #createButton()
*/
protected var button:Button;
/**
* The list sub-component.
*
* For internal use in subclasses.
*
* @see #listFactory
* @see #createList()
*/
protected var list:List;
/**
* @private
*/
override protected function get defaultStyleProvider():IStyleProvider
{
return PickerList.globalStyleProvider;
}
/**
* @private
*/
protected var _dataProvider:ListCollection;
/**
* The collection of data displayed by the list.
*
* The following example passes in a data provider and tells the item
* renderer how to interpret the data:
*
*
* list.dataProvider = new ListCollection(
* [
* { text: "Milk", thumbnail: textureAtlas.getTexture( "milk" ) },
* { text: "Eggs", thumbnail: textureAtlas.getTexture( "eggs" ) },
* { text: "Bread", thumbnail: textureAtlas.getTexture( "bread" ) },
* { text: "Chicken", thumbnail: textureAtlas.getTexture( "chicken" ) },
* ]);
*
* list.listProperties.itemRendererFactory = function():IListItemRenderer
* {
* var renderer:DefaultListItemRenderer = new DefaultListItemRenderer();
* renderer.labelField = "text";
* renderer.iconSourceField = "thumbnail";
* return renderer;
* };
*
* @default null
*/
public function get dataProvider():ListCollection
{
return this._dataProvider;
}
/**
* @private
*/
public function set dataProvider(value:ListCollection):void
{
if(this._dataProvider == value)
{
return;
}
var oldSelectedIndex:int = this.selectedIndex;
var oldSelectedItem:Object = this.selectedItem;
if(this._dataProvider)
{
this._dataProvider.removeEventListener(CollectionEventType.RESET, dataProvider_multipleEventHandler);
this._dataProvider.removeEventListener(CollectionEventType.ADD_ITEM, dataProvider_multipleEventHandler);
this._dataProvider.removeEventListener(CollectionEventType.REMOVE_ITEM, dataProvider_multipleEventHandler);
this._dataProvider.removeEventListener(CollectionEventType.REPLACE_ITEM, dataProvider_multipleEventHandler);
}
this._dataProvider = value;
if(this._dataProvider)
{
this._dataProvider.addEventListener(CollectionEventType.RESET, dataProvider_multipleEventHandler);
this._dataProvider.addEventListener(CollectionEventType.ADD_ITEM, dataProvider_multipleEventHandler);
this._dataProvider.addEventListener(CollectionEventType.REMOVE_ITEM, dataProvider_multipleEventHandler);
this._dataProvider.addEventListener(CollectionEventType.REPLACE_ITEM, dataProvider_multipleEventHandler);
}
if(!this._dataProvider || this._dataProvider.length == 0)
{
this.selectedIndex = -1;
}
else
{
this.selectedIndex = 0;
}
//this ensures that Event.CHANGE will dispatch for selectedItem
//changing, even if selectedIndex has not changed.
if(this.selectedIndex == oldSelectedIndex && this.selectedItem != oldSelectedItem)
{
this.dispatchEventWith(Event.CHANGE);
}
this.invalidate(INVALIDATION_FLAG_DATA);
}
/**
* @private
*/
protected var _ignoreSelectionChanges:Boolean = false;
/**
* @private
*/
protected var _selectedIndex:int = -1;
/**
* The index of the currently selected item. Returns -1
if
* no item is selected.
*
* The following example selects an item by its index:
*
*
* list.selectedIndex = 2;
*
* The following example clears the selected index:
*
*
* list.selectedIndex = -1;
*
* The following example listens for when selection changes and
* requests the selected index:
*
*
* function list_changeHandler( event:Event ):void
* {
* var list:PickerList = PickerList( event.currentTarget );
* var index:int = list.selectedIndex;
*
* }
* list.addEventListener( Event.CHANGE, list_changeHandler );
*
* @default -1
*
* @see #selectedItem
*/
public function get selectedIndex():int
{
return this._selectedIndex;
}
/**
* @private
*/
public function set selectedIndex(value:int):void
{
if(this._selectedIndex == value)
{
return;
}
this._selectedIndex = value;
this.invalidate(INVALIDATION_FLAG_SELECTED);
this.dispatchEventWith(Event.CHANGE);
}
/**
* The currently selected item. Returns null
if no item is
* selected.
*
* The following example changes the selected item:
*
*
* list.selectedItem = list.dataProvider.getItemAt(0);
*
* The following example clears the selected item:
*
*
* list.selectedItem = null;
*
* The following example listens for when selection changes and
* requests the selected item:
*
*
* function list_changeHandler( event:Event ):void
* {
* var list:PickerList = PickerList( event.currentTarget );
* var item:Object = list.selectedItem;
*
* }
* list.addEventListener( Event.CHANGE, list_changeHandler );
*
* @default null
*
* @see #selectedIndex
*/
public function get selectedItem():Object
{
if(!this._dataProvider || this._selectedIndex < 0 || this._selectedIndex >= this._dataProvider.length)
{
return null;
}
return this._dataProvider.getItemAt(this._selectedIndex);
}
/**
* @private
*/
public function set selectedItem(value:Object):void
{
if(!this._dataProvider)
{
this.selectedIndex = -1;
return;
}
this.selectedIndex = this._dataProvider.getItemIndex(value);
}
/**
* @private
*/
protected var _prompt:String;
/**
* Text displayed by the button sub-component when no items are
* currently selected.
*
* In the following example, a prompt is given to the picker list
* and the selected item is cleared to display the prompt:
*
*
* list.prompt = "Select an Item";
* list.selectedIndex = -1;
*
* @default null
*/
public function get prompt():String
{
return this._prompt;
}
/**
* @private
*/
public function set prompt(value:String):void
{
if(this._prompt == value)
{
return;
}
this._prompt = value;
this.invalidate(INVALIDATION_FLAG_SELECTED);
}
/**
* @private
*/
protected var _labelField:String = "label";
/**
* The field in the selected item that contains the label text to be
* displayed by the picker list's button control. If the selected item
* does not have this field, and a labelFunction
is not
* defined, then the picker list will default to calling
* toString()
on the selected item. To omit the
* label completely, define a labelFunction
that returns an
* empty string.
*
* Important: This value only affects the selected
* item displayed by the picker list's button control. It will not
* affect the label text of the pop-up list's item renderers.
*
* In the following example, the label field is changed:
*
*
* list.labelField = "text";
*
* @default "label"
*
* @see #labelFunction
*/
public function get labelField():String
{
return this._labelField;
}
/**
* @private
*/
public function set labelField(value:String):void
{
if(this._labelField == value)
{
return;
}
this._labelField = value;
this.invalidate(INVALIDATION_FLAG_DATA);
}
/**
* @private
*/
protected var _labelFunction:Function;
/**
* A function used to generate label text for the selected item
* displayed by the picker list's button control. If this
* function is not null, then the labelField
will be
* ignored.
*
* Important: This value only affects the selected
* item displayed by the picker list's button control. It will not
* affect the label text of the pop-up list's item renderers.
*
* The function is expected to have the following signature:
* function( item:Object ):String
*
* All of the label fields and functions, ordered by priority:
*
* labelFunction
* labelField
*
*
* In the following example, the label field is changed:
*
*
* list.labelFunction = function( item:Object ):String
* {
* return item.firstName + " " + item.lastName;
* };
*
* @default null
*
* @see #labelField
*/
public function get labelFunction():Function
{
return this._labelFunction;
}
/**
* @private
*/
public function set labelFunction(value:Function):void
{
this._labelFunction = value;
this.invalidate(INVALIDATION_FLAG_DATA);
}
/**
* @private
*/
protected var _popUpContentManager:IPopUpContentManager;
/**
* A manager that handles the details of how to display the pop-up list.
*
* In the following example, a pop-up content manager is provided:
*
*
* list.popUpContentManager = new CalloutPopUpContentManager();
*
* @default null
*/
public function get popUpContentManager():IPopUpContentManager
{
return this._popUpContentManager;
}
/**
* @private
*/
public function set popUpContentManager(value:IPopUpContentManager):void
{
if(this._popUpContentManager == value)
{
return;
}
if(this._popUpContentManager is EventDispatcher)
{
var dispatcher:EventDispatcher = EventDispatcher(this._popUpContentManager);
dispatcher.removeEventListener(Event.OPEN, popUpContentManager_openHandler);
dispatcher.removeEventListener(Event.CLOSE, popUpContentManager_closeHandler);
}
this._popUpContentManager = value;
if(this._popUpContentManager is EventDispatcher)
{
dispatcher = EventDispatcher(this._popUpContentManager);
dispatcher.addEventListener(Event.OPEN, popUpContentManager_openHandler);
dispatcher.addEventListener(Event.CLOSE, popUpContentManager_closeHandler);
}
this.invalidate(INVALIDATION_FLAG_STYLES);
}
/**
* @private
*/
protected var _typicalItemWidth:Number = NaN;
/**
* @private
*/
protected var _typicalItemHeight:Number = NaN;
/**
* @private
*/
protected var _typicalItem:Object = null;
/**
* Used to auto-size the list. If the list's width or height is NaN, the
* list will try to automatically pick an ideal size. This item is
* used in that process to create a sample item renderer.
*
* The following example provides a typical item:
*
*
* list.typicalItem = { text: "A typical item", thumbnail: texture };
* list.itemRendererProperties.labelField = "text";
* list.itemRendererProperties.iconSourceField = "thumbnail";
*
* @default null
*/
public function get typicalItem():Object
{
return this._typicalItem;
}
/**
* @private
*/
public function set typicalItem(value:Object):void
{
if(this._typicalItem == value)
{
return;
}
this._typicalItem = value;
this._typicalItemWidth = NaN;
this._typicalItemHeight = NaN;
this.invalidate(INVALIDATION_FLAG_STYLES);
}
/**
* @private
*/
protected var _buttonFactory:Function;
/**
* A function used to generate the picker list's button sub-component.
* The button must be an instance of Button
. This factory
* can be used to change properties on the button when it is first
* created. For instance, if you are skinning Feathers components
* without a theme, you might use this factory to set skins and other
* styles on the button.
*
* The function should have the following signature:
* function():Button
*
* In the following example, a custom button factory is passed to the
* picker list:
*
*
* list.buttonFactory = function():Button
* {
* var button:Button = new Button();
* button.defaultSkin = new Image( upTexture );
* button.downSkin = new Image( downTexture );
* return button;
* };
*
* @default null
*
* @see feathers.controls.Button
* @see #buttonProperties
*/
public function get buttonFactory():Function
{
return this._buttonFactory;
}
/**
* @private
*/
public function set buttonFactory(value:Function):void
{
if(this._buttonFactory == value)
{
return;
}
this._buttonFactory = value;
this.invalidate(INVALIDATION_FLAG_BUTTON_FACTORY);
}
/**
* @private
*/
protected var _customButtonStyleName:String;
/**
* A style name to add to the picker list's button sub-component.
* Typically used by a theme to provide different styles to different
* picker lists.
*
* In the following example, a custom button style name is passed to
* the picker list:
*
*
* list.customButtonStyleName = "my-custom-button";
*
* In your theme, you can target this sub-component style name to
* provide different styles than the default:
*
*
* getStyleProviderForClass( Button ).setFunctionForStyleName( "my-custom-button", setCustomButtonStyles );
*
* @default null
*
* @see #DEFAULT_CHILD_STYLE_NAME_BUTTON
* @see feathers.core.FeathersControl#styleNameList
* @see #buttonFactory
* @see #buttonProperties
*/
public function get customButtonStyleName():String
{
return this._customButtonStyleName;
}
/**
* @private
*/
public function set customButtonStyleName(value:String):void
{
if(this._customButtonStyleName == value)
{
return;
}
this._customButtonStyleName = value;
this.invalidate(INVALIDATION_FLAG_BUTTON_FACTORY);
}
/**
* @private
*/
protected var _buttonProperties:PropertyProxy;
/**
* An object that stores properties for the picker's button
* sub-component, and the properties will be passed down to the button
* when the picker validates. For a list of available
* properties, refer to
* feathers.controls.Button
.
*
* If the subcomponent has its own subcomponents, their properties
* can be set too, using attribute @
notation. For example,
* to set the skin on the thumb which is in a SimpleScrollBar
,
* which is in a List
, you can use the following syntax:
* list.verticalScrollBarProperties.@thumbProperties.defaultSkin = new Image(texture);
*
* Setting properties in a buttonFactory
function
* instead of using buttonProperties
will result in better
* performance.
*
* In the following example, the button properties are passed to the
* picker list:
*
*
* list.buttonProperties.defaultSkin = new Image( upTexture );
* list.buttonProperties.downSkin = new Image( downTexture );
*
* @default null
*
* @see #buttonFactory
* @see feathers.controls.Button
*/
public function get buttonProperties():Object
{
if(!this._buttonProperties)
{
this._buttonProperties = new PropertyProxy(childProperties_onChange);
}
return this._buttonProperties;
}
/**
* @private
*/
public function set buttonProperties(value:Object):void
{
if(this._buttonProperties == value)
{
return;
}
if(!value)
{
value = new PropertyProxy();
}
if(!(value is PropertyProxy))
{
var newValue:PropertyProxy = new PropertyProxy();
for(var propertyName:String in value)
{
newValue[propertyName] = value[propertyName];
}
value = newValue;
}
if(this._buttonProperties)
{
this._buttonProperties.removeOnChangeCallback(childProperties_onChange);
}
this._buttonProperties = PropertyProxy(value);
if(this._buttonProperties)
{
this._buttonProperties.addOnChangeCallback(childProperties_onChange);
}
this.invalidate(INVALIDATION_FLAG_STYLES);
}
/**
* @private
*/
protected var _listFactory:Function;
/**
* A function used to generate the picker list's pop-up list
* sub-component. The list must be an instance of List
.
* This factory can be used to change properties on the list when it is
* first created. For instance, if you are skinning Feathers components
* without a theme, you might use this factory to set skins and other
* styles on the list.
*
* The function should have the following signature:
* function():List
*
* In the following example, a custom list factory is passed to the
* picker list:
*
*
* list.listFactory = function():List
* {
* var popUpList:List = new List();
* popUpList.backgroundSkin = new Image( texture );
* return popUpList;
* };
*
* @default null
*
* @see feathers.controls.List
* @see #listProperties
*/
public function get listFactory():Function
{
return this._listFactory;
}
/**
* @private
*/
public function set listFactory(value:Function):void
{
if(this._listFactory == value)
{
return;
}
this._listFactory = value;
this.invalidate(INVALIDATION_FLAG_LIST_FACTORY);
}
/**
* @private
*/
protected var _customListStyleName:String;
/**
* A style name to add to the picker list's list sub-component.
* Typically used by a theme to provide different styles to different
* picker lists.
*
* In the following example, a custom list style name is passed to the
* picker list:
*
*
* list.customListStyleName = "my-custom-list";
*
* In your theme, you can target this sub-component style name to provide
* different styles than the default:
*
*
* getStyleProviderForClass( List ).setFunctionForStyleName( "my-custom-list", setCustomListStyles );
*
* @default null
*
* @see #DEFAULT_CHILD_STYLE_NAME_LIST
* @see feathers.core.FeathersControl#styleNameList
* @see #listFactory
* @see #listProperties
*/
public function get customListStyleName():String
{
return this._customListStyleName;
}
/**
* @private
*/
public function set customListStyleName(value:String):void
{
if(this._customListStyleName == value)
{
return;
}
this._customListStyleName = value;
this.invalidate(INVALIDATION_FLAG_LIST_FACTORY);
}
/**
* @private
*/
protected var _listProperties:PropertyProxy;
/**
* An object that stores properties for the picker's pop-up list
* sub-component, and the properties will be passed down to the pop-up
* list when the picker validates. For a list of available
* properties, refer to
* feathers.controls.List
.
*
* If the subcomponent has its own subcomponents, their properties
* can be set too, using attribute @
notation. For example,
* to set the skin on the thumb which is in a SimpleScrollBar
,
* which is in a List
, you can use the following syntax:
* list.verticalScrollBarProperties.@thumbProperties.defaultSkin = new Image(texture);
*
* Setting properties in a listFactory
function
* instead of using listProperties
will result in better
* performance.
*
* In the following example, the list properties are passed to the
* picker list:
*
*
* list.listProperties.backgroundSkin = new Image( texture );
*
* @default null
*
* @see #listFactory
* @see feathers.controls.List
*/
public function get listProperties():Object
{
if(!this._listProperties)
{
this._listProperties = new PropertyProxy(childProperties_onChange);
}
return this._listProperties;
}
/**
* @private
*/
public function set listProperties(value:Object):void
{
if(this._listProperties == value)
{
return;
}
if(!value)
{
value = new PropertyProxy();
}
if(!(value is PropertyProxy))
{
var newValue:PropertyProxy = new PropertyProxy();
for(var propertyName:String in value)
{
newValue[propertyName] = value[propertyName];
}
value = newValue;
}
if(this._listProperties)
{
this._listProperties.removeOnChangeCallback(childProperties_onChange);
}
this._listProperties = PropertyProxy(value);
if(this._listProperties)
{
this._listProperties.addOnChangeCallback(childProperties_onChange);
}
this.invalidate(INVALIDATION_FLAG_STYLES);
}
/**
* @private
*/
protected var _toggleButtonOnOpenAndClose:Boolean = false;
/**
* Determines if the isSelected
property of the picker
* list's button sub-component is toggled when the list is opened and
* closed, if the class used to create the thumb implements the
* IToggle
interface. Useful for skinning to provide a
* different appearance for the button based on whether the list is open
* or not.
*
* In the following example, the button is toggled on open and close:
*
*
* list.toggleButtonOnOpenAndClose = true;
*
* @default false
*
* @see feathers.core.IToggle
* @see feathers.controls.ToggleButton
*/
public function get toggleButtonOnOpenAndClose():Boolean
{
return this._toggleButtonOnOpenAndClose;
}
/**
* @private
*/
public function set toggleButtonOnOpenAndClose(value:Boolean):void
{
if(this._toggleButtonOnOpenAndClose == value)
{
return;
}
this._toggleButtonOnOpenAndClose = value;
if(this.button is IToggle)
{
if(this._toggleButtonOnOpenAndClose && this._popUpContentManager.isOpen)
{
IToggle(this.button).isSelected = true;
}
else
{
IToggle(this.button).isSelected = false;
}
}
}
/**
* @inheritDoc
*/
public function get baseline():Number
{
if(!this.button)
{
return this.scaledActualHeight;
}
return this.scaleY * (this.button.y + this.button.baseline);
}
/**
* @private
*/
protected var _isOpenListPending:Boolean = false;
/**
* @private
*/
protected var _isCloseListPending:Boolean = false;
/**
* Using labelField
and labelFunction
,
* generates a label from the selected item to be displayed by the
* picker list's button control.
*
* Important: This value only affects the selected
* item displayed by the picker list's button control. It will not
* affect the label text of the pop-up list's item renderers.
*/
public function itemToLabel(item:Object):String
{
if(this._labelFunction != null)
{
var labelResult:Object = this._labelFunction(item);
if(labelResult is String)
{
return labelResult as String;
}
return labelResult.toString();
}
else if(this._labelField != null && item && item.hasOwnProperty(this._labelField))
{
labelResult = item[this._labelField];
if(labelResult is String)
{
return labelResult as String;
}
return labelResult.toString();
}
else if(item is String)
{
return item as String;
}
else if(item)
{
return item.toString();
}
return "";
}
/**
* @private
*/
protected var _buttonHasFocus:Boolean = false;
/**
* @private
*/
protected var _buttonTouchPointID:int = -1;
/**
* @private
*/
protected var _listIsOpenOnTouchBegan:Boolean = false;
/**
* Opens the pop-up list, if it isn't already open.
*/
public function openList():void
{
this._isCloseListPending = false;
if(this._popUpContentManager.isOpen)
{
return;
}
if(!this._isValidating && this.isInvalid())
{
this._isOpenListPending = true;
return;
}
this._isOpenListPending = false;
if(this._popUpContentManager is IPopUpContentManagerWithPrompt)
{
IPopUpContentManagerWithPrompt(this._popUpContentManager).prompt = this._prompt;
}
this._popUpContentManager.open(this.list, this);
this.list.scrollToDisplayIndex(this._selectedIndex);
this.list.validate();
if(this._focusManager)
{
this._focusManager.focus = this.list;
this.stage.addEventListener(KeyboardEvent.KEY_UP, stage_keyUpHandler);
this.list.addEventListener(FeathersEventType.FOCUS_OUT, list_focusOutHandler);
}
}
/**
* Closes the pop-up list, if it is open.
*/
public function closeList():void
{
this._isOpenListPending = false;
if(!this._popUpContentManager.isOpen)
{
return;
}
if(!this._isValidating && this.isInvalid())
{
this._isCloseListPending = true;
return;
}
this._isCloseListPending = false;
this.list.validate();
//don't clean up anything from openList() in closeList(). The list
//may be closed by removing it from the PopUpManager, which would
//result in closeList() never being called.
//instead, clean up in the Event.REMOVED_FROM_STAGE listener.
this._popUpContentManager.close();
}
/**
* @inheritDoc
*/
override public function dispose():void
{
if(this.list)
{
this.closeList();
this.list.dispose();
this.list = null;
}
if(this._popUpContentManager)
{
this._popUpContentManager.dispose();
this._popUpContentManager = null;
}
//clearing selection now so that the data provider setter won't
//cause a selection change that triggers events.
this._selectedIndex = -1;
this.dataProvider = null;
super.dispose();
}
/**
* @private
*/
override public function showFocus():void
{
if(!this.button)
{
return;
}
this.button.showFocus();
}
/**
* @private
*/
override public function hideFocus():void
{
if(!this.button)
{
return;
}
this.button.hideFocus();
}
/**
* @private
*/
override protected function initialize():void
{
if(!this._popUpContentManager)
{
if(SystemUtil.isDesktop)
{
this.popUpContentManager = new DropDownPopUpContentManager();
}
else if(DeviceCapabilities.isTablet(Starling.current.nativeStage))
{
this.popUpContentManager = new CalloutPopUpContentManager();
}
else
{
this.popUpContentManager = new VerticalCenteredPopUpContentManager();
}
}
}
/**
* @private
*/
override protected function draw():void
{
var dataInvalid:Boolean = this.isInvalid(INVALIDATION_FLAG_DATA);
var stylesInvalid:Boolean = this.isInvalid(INVALIDATION_FLAG_STYLES);
var stateInvalid:Boolean = this.isInvalid(INVALIDATION_FLAG_STATE);
var selectionInvalid:Boolean = this.isInvalid(INVALIDATION_FLAG_SELECTED);
var sizeInvalid:Boolean = this.isInvalid(INVALIDATION_FLAG_SIZE);
var buttonFactoryInvalid:Boolean = this.isInvalid(INVALIDATION_FLAG_BUTTON_FACTORY);
var listFactoryInvalid:Boolean = this.isInvalid(INVALIDATION_FLAG_LIST_FACTORY);
if(buttonFactoryInvalid)
{
this.createButton();
}
if(listFactoryInvalid)
{
this.createList();
}
if(buttonFactoryInvalid || stylesInvalid || selectionInvalid)
{
//this section asks the button to auto-size again, if our
//explicit dimensions aren't set.
//set this before buttonProperties is used because it might
//contain width or height changes.
if(this._explicitWidth !== this._explicitWidth) //isNaN
{
this.button.width = NaN;
}
if(this._explicitHeight !== this._explicitHeight) //isNaN
{
this.button.height = NaN;
}
}
if(buttonFactoryInvalid || stylesInvalid)
{
this._typicalItemWidth = NaN;
this._typicalItemHeight = NaN;
this.refreshButtonProperties();
}
if(listFactoryInvalid || stylesInvalid)
{
this.refreshListProperties();
}
if(listFactoryInvalid || dataInvalid)
{
var oldIgnoreSelectionChanges:Boolean = this._ignoreSelectionChanges;
this._ignoreSelectionChanges = true;
this.list.dataProvider = this._dataProvider;
this._ignoreSelectionChanges = oldIgnoreSelectionChanges;
}
if(buttonFactoryInvalid || listFactoryInvalid || stateInvalid)
{
this.button.isEnabled = this._isEnabled;
this.list.isEnabled = this._isEnabled;
}
if(buttonFactoryInvalid || dataInvalid || selectionInvalid)
{
this.refreshButtonLabel();
}
if(listFactoryInvalid || dataInvalid || selectionInvalid)
{
oldIgnoreSelectionChanges = this._ignoreSelectionChanges;
this._ignoreSelectionChanges = true;
this.list.selectedIndex = this._selectedIndex;
this._ignoreSelectionChanges = oldIgnoreSelectionChanges;
}
sizeInvalid = this.autoSizeIfNeeded() || sizeInvalid;
if(buttonFactoryInvalid || stylesInvalid || sizeInvalid || selectionInvalid)
{
this.layout();
}
if(this.list.stage)
{
//final validation to avoid juggler next frame issues
//only validate if it's on the display list, though, because the
//popUpContentManager may need to place restrictions on
//dimensions or make other important changes.
//otherwise, the List may create an item renderer for every item
//in the data provider, which is not good for performance!
this.list.validate();
}
this.handlePendingActions();
}
/**
* If the component's dimensions have not been set explicitly, it will
* measure its content and determine an ideal size for itself. If the
* explicitWidth
or explicitHeight
member
* variables are set, those value will be used without additional
* measurement. If one is set, but not the other, the dimension with the
* explicit value will not be measured, but the other non-explicit
* dimension will still need measurement.
*
* Calls setSizeInternal()
to set up the
* actualWidth
and actualHeight
member
* variables used for layout.
*
* Meant for internal use, and subclasses may override this function
* with a custom implementation.
*/
protected function autoSizeIfNeeded():Boolean
{
var needsWidth:Boolean = this._explicitWidth !== this._explicitWidth; //isNaN
var needsHeight:Boolean = this._explicitHeight !== this._explicitHeight; //isNaN
if(!needsWidth && !needsHeight)
{
return false;
}
var buttonWidth:Number;
var buttonHeight:Number;
if(this._typicalItem)
{
if(this._typicalItemWidth !== this._typicalItemWidth || //isNaN
this._typicalItemHeight !== this._typicalItemHeight) //isNaN
{
var oldWidth:Number = this.button.width;
var oldHeight:Number = this.button.height;
this.button.width = NaN;
this.button.height = NaN;
if(this._typicalItem)
{
this.button.label = this.itemToLabel(this._typicalItem);
}
this.button.validate();
this._typicalItemWidth = this.button.width;
this._typicalItemHeight = this.button.height;
this.refreshButtonLabel();
this.button.width = oldWidth;
this.button.height = oldHeight;
}
buttonWidth = this._typicalItemWidth;
buttonHeight = this._typicalItemHeight;
}
else
{
this.button.validate();
buttonWidth = this.button.width;
buttonHeight = this.button.height;
}
var newWidth:Number = this._explicitWidth;
var newHeight:Number = this._explicitHeight;
if(needsWidth)
{
if(buttonWidth === buttonWidth) //!isNaN
{
newWidth = buttonWidth;
}
else
{
newWidth = 0;
}
}
if(needsHeight)
{
if(buttonHeight === buttonHeight) //!isNaN
{
newHeight = buttonHeight;
}
else
{
newHeight = 0;
}
}
return this.setSizeInternal(newWidth, newHeight, false);
}
/**
* Creates and adds the button
sub-component and
* removes the old instance, if one exists.
*
* Meant for internal use, and subclasses may override this function
* with a custom implementation.
*
* @see #button
* @see #buttonFactory
* @see #customButtonStyleName
*/
protected function createButton():void
{
if(this.button)
{
this.button.removeFromParent(true);
this.button = null;
}
var factory:Function = this._buttonFactory != null ? this._buttonFactory : defaultButtonFactory;
var buttonStyleName:String = this._customButtonStyleName != null ? this._customButtonStyleName : this.buttonStyleName;
this.button = Button(factory());
if(this.button is ToggleButton)
{
//we'll control the value of isSelected manually
ToggleButton(this.button).isToggle = false;
}
this.button.styleNameList.add(buttonStyleName);
this.button.addEventListener(TouchEvent.TOUCH, button_touchHandler);
this.button.addEventListener(Event.TRIGGERED, button_triggeredHandler);
this.addChild(this.button);
}
/**
* Creates and adds the list
sub-component and
* removes the old instance, if one exists.
*
* Meant for internal use, and subclasses may override this function
* with a custom implementation.
*
* @see #list
* @see #listFactory
* @see #customListStyleName
*/
protected function createList():void
{
if(this.list)
{
this.list.removeFromParent(false);
//disposing separately because the list may not have a parent
this.list.dispose();
this.list = null;
}
var factory:Function = this._listFactory != null ? this._listFactory : defaultListFactory;
var listStyleName:String = this._customListStyleName != null ? this._customListStyleName : this.listStyleName;
this.list = List(factory());
this.list.focusOwner = this;
this.list.styleNameList.add(listStyleName);
this.list.addEventListener(Event.CHANGE, list_changeHandler);
this.list.addEventListener(Event.TRIGGERED, list_triggeredHandler);
this.list.addEventListener(Event.REMOVED_FROM_STAGE, list_removedFromStageHandler);
}
/**
* @private
*/
protected function refreshButtonLabel():void
{
if(this._selectedIndex >= 0)
{
this.button.label = this.itemToLabel(this.selectedItem);
}
else
{
this.button.label = this._prompt;
}
}
/**
* @private
*/
protected function refreshButtonProperties():void
{
for(var propertyName:String in this._buttonProperties)
{
var propertyValue:Object = this._buttonProperties[propertyName];
this.button[propertyName] = propertyValue;
}
}
/**
* @private
*/
protected function refreshListProperties():void
{
for(var propertyName:String in this._listProperties)
{
var propertyValue:Object = this._listProperties[propertyName];
this.list[propertyName] = propertyValue;
}
}
/**
* @private
*/
protected function layout():void
{
this.button.width = this.actualWidth;
this.button.height = this.actualHeight;
//final validation to avoid juggler next frame issues
this.button.validate();
}
/**
* @private
*/
protected function handlePendingActions():void
{
if(this._isOpenListPending)
{
this.openList();
}
if(this._isCloseListPending)
{
this.closeList();
}
}
/**
* @private
*/
override protected function focusInHandler(event:Event):void
{
super.focusInHandler(event);
this._buttonHasFocus = true;
this.button.dispatchEventWith(FeathersEventType.FOCUS_IN);
}
/**
* @private
*/
override protected function focusOutHandler(event:Event):void
{
if(this._buttonHasFocus)
{
this.button.dispatchEventWith(FeathersEventType.FOCUS_OUT);
this._buttonHasFocus = false;
}
super.focusOutHandler(event);
}
/**
* @private
*/
protected function childProperties_onChange(proxy:PropertyProxy, name:String):void
{
this.invalidate(INVALIDATION_FLAG_STYLES);
}
/**
* @private
*/
protected function button_touchHandler(event:TouchEvent):void
{
if(this._buttonTouchPointID >= 0)
{
var touch:Touch = event.getTouch(this.button, TouchPhase.ENDED, this._buttonTouchPointID);
if(!touch)
{
return;
}
this._buttonTouchPointID = -1;
//the button will dispatch Event.TRIGGERED before this touch
//listener is called, so it is safe to clear this flag.
//we're clearing it because Event.TRIGGERED may also be
//dispatched after keyboard input.
this._listIsOpenOnTouchBegan = false;
}
else
{
touch = event.getTouch(this.button, TouchPhase.BEGAN);
if(!touch)
{
return;
}
this._buttonTouchPointID = touch.id;
this._listIsOpenOnTouchBegan = this._popUpContentManager.isOpen;
}
}
/**
* @private
*/
protected function button_triggeredHandler(event:Event):void
{
if(this._focusManager && this._listIsOpenOnTouchBegan)
{
return;
}
if(this._popUpContentManager.isOpen)
{
this.closeList();
return;
}
this.openList();
}
/**
* @private
*/
protected function list_changeHandler(event:Event):void
{
if(this._ignoreSelectionChanges ||
this._popUpContentManager is IPersistentPopUpContentManager)
{
return;
}
this.selectedIndex = this.list.selectedIndex;
}
/**
* @private
*/
protected function popUpContentManager_openHandler(event:Event):void
{
if(this._toggleButtonOnOpenAndClose && this.button is IToggle)
{
IToggle(this.button).isSelected = true;
}
this.dispatchEventWith(Event.OPEN);
}
/**
* @private
*/
protected function popUpContentManager_closeHandler(event:Event):void
{
if(this._popUpContentManager is IPersistentPopUpContentManager)
{
this.selectedIndex = this.list.selectedIndex;
}
if(this._toggleButtonOnOpenAndClose && this.button is IToggle)
{
IToggle(this.button).isSelected = false;
}
this.dispatchEventWith(Event.CLOSE);
}
/**
* @private
*/
protected function list_removedFromStageHandler(event:Event):void
{
if(this._focusManager)
{
this.list.stage.removeEventListener(KeyboardEvent.KEY_UP, stage_keyUpHandler);
this.list.removeEventListener(FeathersEventType.FOCUS_OUT, list_focusOutHandler);
}
}
/**
* @private
*/
protected function list_focusOutHandler(event:Event):void
{
if(!this._popUpContentManager.isOpen)
{
return;
}
this.closeList();
}
/**
* @private
*/
protected function list_triggeredHandler(event:Event):void
{
if(!this._isEnabled ||
this._popUpContentManager is IPersistentPopUpContentManager)
{
return;
}
this.closeList();
}
/**
* @private
*/
protected function dataProvider_multipleEventHandler():void
{
//we need to ensure that the pop-up list has received the new
//selected index, or it might update the selected index to an
//incorrect value after an item is added, removed, or replaced.
this.validate();
}
/**
* @private
*/
protected function stage_keyUpHandler(event:KeyboardEvent):void
{
if(!this._popUpContentManager.isOpen)
{
return;
}
if(event.keyCode == Keyboard.ENTER)
{
this.closeList();
}
}
}
}