blob: cdd48415a100bc5708a1aeed5e610ac702124bd7 [file] [log] [blame]
////////////////////////////////////////////////////////////////////////////////
//
// Licensed to the Apache Software Foundation (ASF) under one or more
// contributor license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright ownership.
// The ASF licenses this file to You under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance with
// the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////////
package spark.components.supportClasses
{
import flash.events.Event;
import flash.events.EventPhase;
import flash.events.FocusEvent;
import flash.events.KeyboardEvent;
import flash.events.MouseEvent;
import flash.ui.Keyboard;
import mx.collections.IList;
import mx.core.EventPriority;
import mx.core.IFactory;
import mx.core.ISelectableList;
import mx.core.IVisualElement;
import mx.core.mx_internal;
import mx.events.FlexEvent;
import mx.events.IndexChangedEvent;
import mx.managers.IFocusManagerComponent;
import spark.components.ButtonBarButton;
import spark.components.IItemRenderer;
import spark.components.supportClasses.ButtonBase;
import spark.events.IndexChangeEvent;
import spark.events.RendererExistenceEvent;
use namespace mx_internal; // use of ListBase/setCurrentCaretIndex(index);
//--------------------------------------
// Styles
//--------------------------------------
/**
* Orientation of the icon in relation to the label.
* Valid MXML values are <code>right</code>, <code>left</code>,
* <code>bottom</code>, and <code>top</code>.
*
* <p>In ActionScript, you can use the following constants
* to set this property:
* <code>IconPlacement.RIGHT</code>,
* <code>IconPlacement.LEFT</code>,
* <code>IconPlacement.BOTTOM</code>, and
* <code>IconPlacement.TOP</code>.</p>
*
* @default IconPlacement.LEFT
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
[Style(name="iconPlacement", type="String", enumeration="top,bottom,right,left", inherit="no")]
//--------------------------------------
// Other metadata
//--------------------------------------
[AccessibilityClass(implementation="spark.accessibility.ButtonBarBaseAccImpl")]
/**
* The ButtonBarBase class defines the common behavior for the ButtonBar, TabBar and similar subclasses.
* This class does not add any new API however it refines selection, keyboard focus and keyboard navigation
* behavior for the control's ItemRenderer elements.
* This base class is not intended to be instantiated directly.
*
* <p>Clicking on an ItemRenderer selects it by setting the <code>selectedIndex</code> and the
* <code>caretIndex</code> properties. If <code>requireSelection</code> is <code>false</code>, then clicking
* again deselects it. If the data provider is an <code>ISelectableList</code> object, then its
* <code>selectedIndex</code> is set as well.</p>
*
* <p>Arrow key events are handled by adjusting the <code>caretIndex</code>.
* If <code>arrowKeysWrapFocus</code> is <code>true</code>, then the <code>caretIndex</code> wraps.
* Pressing the Space key selects the ItemRenderer at the <code>caretIndex</code>.</p>
*
* <p>The <code>showsCaret</code> property of the ItemRenderer at <code>caretIndex</code>
* is set to <code>true</code> when the ButtonBarBase object has focus and
* the <code>caretIndex</code> was reached as a consequence
* of a keyboard gesture.
* If the <code>caretIndex</code> was set as a side effect of responding to a
* mouse click, then <code>showsCaret</code> is not set.</p>
*
* <p>The <code>allowDeselection</code> property of <code>ButtonBarButton</code>
* ItemRenderers is set to <code>!requireSelection</code>.</p>
*
* @mxml
*
* <p>The <code>&lt;s:ButtonBarBase&gt;</code> tag inherits all of the tag
* attributes of its superclass and adds no new tag attributes:</p>
*
* <pre>
* &lt;s:ButtonBarBase/&gt;
* </pre>
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public class ButtonBarBase extends ListBase
{
include "../../core/Version.as";
//--------------------------------------------------------------------------
//
// Class mixins
//
//--------------------------------------------------------------------------
/**
* @private
* Placeholder for mixin by ButtonBarBaseAccImpl.
*/
mx_internal static var createAccessibilityImplementation:Function;
/**
* Constructor.
*
* <p>Initializes tab processing: tabbing to this component will give it the focus, but not
* clicking on it with the mouse. Tabbing to the children is disabled.</p>
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public function ButtonBarBase()
{
super();
tabChildren = false;
tabEnabled = true;
tabFocusEnabled = true;
setCurrentCaretIndex(0);
}
//--------------------------------------------------------------------------
//
// Variables
//
//--------------------------------------------------------------------------
/**
* @private
* If false, don't show the focusRing for the tab at caretIndex, see
* itemShowingCaret() below.
*
* If the caret index changes because of something other than a arrow
* or space keypress then we don't show the focus ring, i.e. we do not
* set showsCaret=true for the item renderer at caretIndex.
*
* This flag is valid at commitProperties() time. It's set to false
* if at least one selectedIndex change (see item_clickHandler()) occurred
* because of a mouse click.
*/
private var enableFocusHighlight:Boolean = true;
/**
* @private
*/
private var inCollectionChangeHandler:Boolean = false;
/**
* @private
* Used to distinguish item_clickHandler() calls initiated by the mouse, from calls
* initiated by pressing the space bar.
*/
private var inKeyUpHandler:Boolean = false;
/**
* @private
* Index of item that is currently pressed by the
* spacebar.
*/
private var pressedIndex:Number;
//--------------------------------------------------------------------------
//
// Overridden Properties
//
//--------------------------------------------------------------------------
//----------------------------------
// dataProvider
//----------------------------------
[Inspectable(category="Data")]
/**
* @private
*/
override public function set dataProvider(value:IList):void
{
if (dataProvider is ISelectableList)
{
dataProvider.removeEventListener(FlexEvent.VALUE_COMMIT, dataProvider_changeHandler);
dataProvider.removeEventListener(IndexChangedEvent.CHANGE, dataProvider_changeHandler);
}
if (value is ISelectableList)
{
value.addEventListener(FlexEvent.VALUE_COMMIT, dataProvider_changeHandler, false, 0, true);
value.addEventListener(IndexChangedEvent.CHANGE, dataProvider_changeHandler, false, 0, true);
}
super.dataProvider = value;
if (value is ISelectableList)
selectedIndex = ISelectableList(dataProvider).selectedIndex;
}
//----------------------------------
// iconField
//----------------------------------
/**
* @private
*/
private var _iconField:String = "icon";
/**
* @private
*/
private var iconFieldOrFunctionChanged:Boolean;
/**
* The name of the field in the data provider items which serves
* as the icon to display.
*
* The <code>iconFunction</code> property overrides this property.
*
* @default null
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4.5
*/
public function get iconField():String
{
return _iconField;
}
/**
* @private
*/
public function set iconField(value:String):void
{
if (value == _iconField)
return;
_iconField = value;
iconFieldOrFunctionChanged = true;
invalidateProperties();
}
//----------------------------------
// iconFunction
//----------------------------------
/**
* @private
*/
private var _iconFunction:Function;
/**
* A user-supplied function to run on each item to determine its icon.
* The <code>iconFunction</code> property overrides
* the <code>iconField</code> property.
*
* <p>You can supply an <code>iconFunction</code> that finds the
* appropriate fields and returns a displayable icon. </p>
*
* <p>The icon function takes a single argument which is the item in
* the data provider and returns a valid BitmapImage source.</p>
* <pre>
* myIconFunction(item:Object):Object</pre>
*
* @default null
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4.5
*/
public function get iconFunction():Function
{
return _iconFunction;
}
/**
* @private
*/
public function set iconFunction(value:Function):void
{
if (value == _iconFunction)
return;
_iconFunction = value;
iconFieldOrFunctionChanged = true;
invalidateProperties();
}
//----------------------------------
// requireSelection
//----------------------------------
private var requireSelectionChanged:Boolean;
[Inspectable(category="General", defaultValue="false")]
/**
* @private
* See commitProperties().
*/
override public function set requireSelection(value:Boolean):void
{
if (value == requireSelection)
return;
super.requireSelection = value;
requireSelectionChanged = true;
invalidateProperties();
}
//--------------------------------------------------------------------------
//
// Overridden Methods
//
//--------------------------------------------------------------------------
/**
* @private
* Called by the initialize() method of UIComponent
* to hook in the accessibility code.
*/
override protected function initializeAccessibility():void
{
if (ButtonBarBase.createAccessibilityImplementation != null)
ButtonBarBase.createAccessibilityImplementation(this);
}
/**
* @private
* if the collection is changing and the collection is a viewstack
* the viewstack will also adjust the selection and notify us
* via dataProvider_changeHandler so we want to
* ignore calls to adjustSelection that the base class
* calls so we don't increment or decrement
* selectedIndex twice
*/
override protected function dataProvider_collectionChangeHandler(event:Event):void
{
inCollectionChangeHandler = true;
super.dataProvider_collectionChangeHandler(event);
inCollectionChangeHandler = false;
}
/**
* @private
*/
override protected function adjustSelection(newIndex:int, add:Boolean=false):void
{
// see comment in dataProvider_collectionChangeHandler
if (inCollectionChangeHandler && dataProvider is ISelectableList)
return;
super.adjustSelection(newIndex, add);
}
/**
* @private
*/
override protected function commitProperties():void
{
super.commitProperties();
if (requireSelectionChanged && dataGroup)
{
requireSelectionChanged = false;
const n:int = dataGroup.numElements;
for (var i:int = 0; i < n; i++)
{
var renderer:ButtonBarButton = dataGroup.getElementAt(i) as ButtonBarButton;
if (renderer)
renderer.allowDeselection = !requireSelection;
}
}
if (iconFieldOrFunctionChanged)
{
updateRendererIcons();
iconFieldOrFunctionChanged = false;
}
enableFocusHighlight = true;
}
/**
* @private
* Return the item renderer at the specified index, or null.
*/
private function getItemRenderer(index:int):IVisualElement
{
if (!dataGroup || (index < 0) || (index >= dataGroup.numElements))
return null;
return dataGroup.getElementAt(index);
}
/**
* @private
* Called when setCurrentCaretIndex() moves the caret, or when it's moved as a consequence
* of items being moved/removed. See ListBase/caretIndexAdjusted.
*/
override protected function itemShowingCaret(index:int, showsCaret:Boolean):void
{
super.itemShowingCaret(index, showsCaret);
const renderer:IVisualElement = getItemRenderer(index);
if (renderer)
{
const hasFocus:Boolean = focusManager && (focusManager.getFocus() == this);
renderer.depth = (showsCaret) ? 1 : 0;
if (renderer is IItemRenderer)
IItemRenderer(renderer).showsCaret = showsCaret && enableFocusHighlight && hasFocus;
}
}
/**
* @private
* Called when the focus is gained/lost by tabbing in or out.
*/
override public function drawFocus(isFocused:Boolean):void
{
const renderer:IVisualElement = getItemRenderer(caretIndex);
if (renderer)
{
renderer.depth = (isFocused) ? 1 : 0;
if (renderer is IItemRenderer)
IItemRenderer(renderer).showsCaret = isFocused;
}
}
/**
* @private
*/
override protected function itemSelected(index:int, selected:Boolean):void
{
super.itemSelected(index, selected);
const renderer:IItemRenderer = getItemRenderer(index) as IItemRenderer;
if (renderer)
{
if (selected)
setCurrentCaretIndex(index); // causes itemShowingCaret() call
renderer.selected = selected;
}
if ((dataProvider is ISelectableList) && selected)
ISelectableList(dataProvider).selectedIndex = index;
}
/**
* @private
* Detected changes to iconPlacement and update as necessary.
*/
override public function styleChanged(styleProp:String):void
{
if (!styleProp ||
styleProp == "styleName" || styleProp == "iconPlacement")
{
// Cause icon association to reoccur, taking into consideration
// the new placement.
iconFieldOrFunctionChanged = true;
invalidateProperties();
}
super.styleChanged(styleProp);
}
/**
* @private
*/
override public function updateRenderer(renderer:IVisualElement, itemIndex:int, data:Object):void
{
itemToIcon(renderer, data);
super.updateRenderer(renderer, itemIndex, data);
}
//--------------------------------------------------------------------------
//
// Methods
//
//--------------------------------------------------------------------------
/**
* Gets the index for several of a ButtonBar's Buttons, referencing them by the ButtonBarbutton's <code>label</code>.
*
* <p>The method takes an array of ButtonBarButtons label and an optional field name.</p>
* <pre>myButtonBar.getButtonIndices(["My Button Label1", "My Label2"])</pre>
*
* @param labelValues Are the ButtonBarButton labels to find.
* @param fieldName Field used for comparing the label (optional)
*
* @langversion 3.0
* @playerversion Flash 11.1
* @playerversion AIR 3.4
* @productversion Flex 4.10
*/
public function getButtonIndices(labelValues:Array, fieldName:String = ""):Array
{
var buttonIndices:Array;
if (!dataGroup || labelValues.length < 1 || labelValues == null)
{
return [];
}
if (fieldName == "" || fieldName == null)
{
return findRowIndices(labelField, labelValues);
}
else
{
return findRowIndices(fieldName, labelValues);
}
}
/**
* Allows changing the <code>enabled</code> property of a the child ButtonBarbutton's.
* It identifies the button given its label field (default) or an different optional field name may be passed.
*
* <p>The method takes a single ButtonBarButton label, a new <code>enabled</code> property value, and an optional field name to use as the comparison field.</p>
* <pre>
* myButtonBar.setButtonEnabled("My Button Label", false)</pre>
*
* @param labelValue Is the ButtonBarButton label.
* @param enabledValue The buttons new enabled value.
* @param fieldName Field used to compare the label value against.
*
* @langversion 3.0
* @playerversion Flash 11.1
* @playerversion AIR 3.4
* @productversion Flex 4.10
*/
public function setButtonEnabled(labelValue:String, enabledValue:Boolean, fieldName:String = ""):void
{
setButtonsEnabled([labelValue], enabledValue, fieldName);
}
/**
* Allows changing the <code>enabled</code> property of several child ButtonBarbutton's.
* It identifies the buttons given their label fields (default) or an different optional field name may be passed.
*
* <p>The method takes an array of ButtonBarButton labels, a new <code>enabled</code> property value, and an optional field name to use as the comparison field.</p>
* <pre>
* myButtonBar.setButtonsEnabled(["My Button Label1", "My Label2"], false)</pre>
*
* @param labelValues Is an array of ButtonBarButton labels.
* @param enabledValue The buttons new enabled value.
* @param fieldName Field used to compare the label value against.
*
* @langversion 3.0
* @playerversion Flash 11.1
* @playerversion AIR 3.4
* @productversion Flex 4.10
*/
public function setButtonsEnabled(labelValues:Array, enabledValue:Boolean, fieldName:String = ""):void
{
var btnCurrent:ButtonBarButton = null;
var buttonIndices:Array;
var indicesTotal:uint = 0;
var loopingIndex:uint = 0;
buttonIndices = getButtonIndices(labelValues, fieldName);
indicesTotal = buttonIndices.length;
if (indicesTotal == 0)
{
return;
}
for (loopingIndex; loopingIndex < indicesTotal; loopingIndex++)
{
btnCurrent = dataGroup.getElementAt(buttonIndices[loopingIndex]) as ButtonBarButton;
btnCurrent.enabled = enabledValue;
}
}
/**
* @private
*/
private function itemToIcon(renderer:IVisualElement, item:Object):void
{
if (!(renderer is ButtonBase))
return;
// Assign iconPlacement if appropriate.
var iconPlacement:String = getStyle("iconPlacement");
if (iconPlacement)
ButtonBase(renderer).setStyle("iconPlacement", iconPlacement);
// iconFunction takes precedence.
if (_iconFunction != null)
{
ButtonBase(renderer).setStyle("icon", _iconFunction(item));
return;
}
// iconField
if (_iconField &&
_iconField.length > 0 &&
item is Object)
{
try
{
if (item[_iconField] != null)
{
ButtonBase(renderer).setStyle("icon", item[_iconField]);
return;
}
}
catch(e:Error)
{
}
}
ButtonBase(renderer).clearStyle("icon");
}
/**
* @private
*/
private function updateRendererIcons():void
{
if (!dataGroup)
return;
const count:int = dataGroup.numElements;
for (var i:int = 0; i < count; i++)
{
var renderer:IItemRenderer = dataGroup.getElementAt(i) as IItemRenderer;
if (renderer && renderer.data)
itemToIcon(renderer, renderer.data);
}
}
//--------------------------------------------------------------------------
//
// Event Handlers
//
//--------------------------------------------------------------------------
/**
* @private
*/
private function dataProvider_changeHandler(event:Event):void
{
var newSelectedIndex:int = ISelectableList(dataProvider).selectedIndex;
if (selectedIndex != newSelectedIndex)
{
selectedIndex = newSelectedIndex;
commitSelection(false);
dispatchEvent(new FlexEvent(FlexEvent.VALUE_COMMIT));
}
}
/**
* @private
*/
override protected function dataGroup_rendererAddHandler(event:RendererExistenceEvent):void
{
super.dataGroup_rendererAddHandler(event);
const renderer:IVisualElement = event.renderer;
if (renderer)
{
renderer.addEventListener(MouseEvent.CLICK, item_clickHandler);
if (renderer is IFocusManagerComponent)
IFocusManagerComponent(renderer).focusEnabled = false;
if (renderer is ButtonBarButton)
ButtonBarButton(renderer).allowDeselection = !requireSelection;
}
}
/**
* @private
*/
override protected function dataGroup_rendererRemoveHandler(event:RendererExistenceEvent):void
{
super.dataGroup_rendererRemoveHandler(event);
const renderer:IVisualElement = event.renderer;
if (renderer)
renderer.removeEventListener(MouseEvent.CLICK, item_clickHandler);
}
/**
* @private
* Called synchronously when the space bar is pressed or the mouse is clicked.
*/
private function item_clickHandler(event:MouseEvent):void
{
var newIndex:int;
if (event.currentTarget is IItemRenderer)
newIndex = IItemRenderer(event.currentTarget).itemIndex;
else
newIndex = dataGroup.getElementIndex(event.currentTarget as IVisualElement);
var oldSelectedIndex:int = selectedIndex;
if (newIndex == selectedIndex)
{
if (!requireSelection)
setSelectedIndex(NO_SELECTION, true);
}
else
setSelectedIndex(newIndex, true);
// Changing the selectedIndex typically causes a call to itemSelected() at
// commitProperties() time. We'll update the caretIndex then. If this
// method was -not- called as a consequence of a keypress, we will not show
// the focus highlight at caretIndex. See itemShowingCaret().
if (enableFocusHighlight && (selectedIndex != oldSelectedIndex))
enableFocusHighlight = inKeyUpHandler;
}
/**
* @private
* Increment or decrement the caretIndex. Wrap if arrowKeysWrapFocus=true.
*/
private function adjustCaretIndex(delta:int):void
{
if (!dataGroup || (caretIndex < 0))
return;
const oldCaretIndex:int = caretIndex;
const length:int = dataGroup.numElements;
if (arrowKeysWrapFocus)
setCurrentCaretIndex((caretIndex + delta + length) % length);
else
setCurrentCaretIndex(Math.min(length - 1, Math.max(0, caretIndex + delta)));
if (oldCaretIndex != caretIndex)
dispatchEvent(new IndexChangeEvent(IndexChangeEvent.CARET_CHANGE, false, false, oldCaretIndex, caretIndex));
}
/**
* @private
*/
override protected function keyDownHandler(event:KeyboardEvent):void
{
if (event.eventPhase == EventPhase.BUBBLING_PHASE)
return;
if (!enabled || !dataGroup || event.isDefaultPrevented())
return;
// Block input if space bar is being held down.
if (!isNaN(pressedIndex))
{
event.preventDefault();
return;
}
super.keyDownHandler(event);
// If rtl layout, need to swap LEFT/UP and RIGHT/DOWN so correct action
// is done.
var keyCode:uint = mapKeycodeForLayoutDirection(event, true);
switch (keyCode)
{
case Keyboard.UP:
case Keyboard.LEFT:
{
adjustCaretIndex(-1);
event.preventDefault();
break;
}
case Keyboard.DOWN:
case Keyboard.RIGHT:
{
adjustCaretIndex(+1);
event.preventDefault();
break;
}
case Keyboard.SPACE:
{
const renderer:IItemRenderer = getItemRenderer(caretIndex) as IItemRenderer;
if (renderer && ((!renderer.selected && requireSelection) || !requireSelection))
{
renderer.dispatchEvent(event);
pressedIndex = caretIndex;
}
break;
}
}
}
/**
* @private
*/
override protected function keyUpHandler(event:KeyboardEvent):void
{
if (event.eventPhase == EventPhase.BUBBLING_PHASE)
return;
if (!enabled)
return;
super.keyUpHandler(event);
switch (event.keyCode)
{
case Keyboard.SPACE:
{
inKeyUpHandler = true;
// Need to check pressedIndex for NaN for the case when key up
// happens on an already selected renderer and under the condition
// that requireSelection=true.
if (!isNaN(pressedIndex))
{
// Dispatch key up to the previously pressed item in case focus was lost
// through other interaction (e.g. mouse clicks, etc...)
const renderer:IItemRenderer = getItemRenderer(pressedIndex) as IItemRenderer;
if (renderer && ((!renderer.selected && requireSelection) || !requireSelection))
{
renderer.dispatchEvent(event);
pressedIndex = NaN;
}
}
inKeyUpHandler = false;
break;
}
}
}
}
}