blob: 1f1c063a328fc736d23418f8ca6fb3c49d849b5c [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
{
import flash.display.DisplayObject;
import flash.display.DisplayObjectContainer;
import flash.display.InteractiveObject;
import flash.events.Event;
import flash.events.EventDispatcher;
import mx.core.FlexGlobals;
import mx.core.IFlexDisplayObject;
import mx.core.IMXMLObject;
import mx.core.IVisualElement;
import mx.core.IVisualElementContainer;
import mx.core.UIComponent;
import mx.core.mx_internal;
import mx.events.FlexEvent;
import mx.events.ItemClickEvent;
import mx.utils.NameUtil;
use namespace mx_internal;
//--------------------------------------
// Events
//--------------------------------------
/**
* Dispatched when the value of the selected RadioButton component in
* this group changes.
*
* @eventType flash.events.Event.CHANGE
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
[Event(name="change", type="flash.events.Event")]
/**
* Dispatched when a user selects a RadioButton component in the group.
* You can also set a handler for individual RadioButton components.
*
* This event is dispatched only when the
* user interacts with the radio buttons by using the mouse.
*
* @eventType mx.events.ItemClickEvent.ITEM_CLICK
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
[Event(name="itemClick", type="mx.events.ItemClickEvent")]
//--------------------------------------
// Validation events
//--------------------------------------
/**
* Dispatched when values are changed programmatically
* or by user interaction.
*
* <p>Because a programmatic change triggers this event, make sure
* that any <code>valueCommit</code> event handler does not change
* a value that causes another <code>valueCommit</code> event.
* For example, do not change the<code>selectedValue</code>
* property or <code>selection</code> property in a <code>valueCommit</code>
* event handler. </p>
*
* @eventType mx.events.FlexEvent.VALUE_COMMIT
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
[Event(name="valueCommit", type="mx.events.FlexEvent")]
//--------------------------------------
// Other metadata
//--------------------------------------
[IconFile("RadioButtonGroup.png")]
[DefaultTriggerEvent("change")]
/**
* The RadioButtonGroup component defines a group of RadioButton components
* that act as a single mutually exclusive component; therefore,
* a user can select only one RadioButton component at a time.
* The <code>id</code> property is required when you use the
* <code>&lt;s:RadioButtonGroup&gt;</code> tag to define the group name. Any
* <code>&lt;s:RadioButton&gt;</code> component added to this group will
* have this group name.
*
* <p>Notice that the RadioButtonGroup component is a subclass of EventDispatcher,
* not UIComponent, and implements the IMXMLObject interface.
* All other Flex visual components implement the IVisualElement interface.
* The RadioButtonGroup component declaration must
* be contained within the <code>&lt;Declarations&gt;</code> tag since it is
* not assignable to IVisualElement.</p>
*
* <p>To use this component in a list-based component, such as a List or DataGrid,
* create an item renderer.
* For information about creating an item renderer, see
* <a href="http://help.adobe.com/en_US/flex/using/WS4bebcd66a74275c3-fc6548e124e49b51c4-8000.html">
* Custom Spark item renderers</a>. </p>
*
* @mxml
*
* <p>The <code>&lt;s:RadioButtonGroup&gt;</code> tag inherits all of the
* tag attributes of its superclass, and adds the following tag attributes:</p>
*
* <pre>
* &lt;s:RadioButtonGroup
* <strong>Properties</strong>
* enabled="true"
* selectedValue="null"
* selection="null"
*
* <strong>Events</strong>
* change="<i>No default</i>"
* itemClick="<i>No default</i>"
* valueCommit="<i>No default</i>"
* /&gt;
* </pre>
*
* @see spark.components.RadioButton
* @includeExample examples/RadioButtonGroupExample.mxml
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public class RadioButtonGroup extends EventDispatcher implements IMXMLObject
{
include "../core/Version.as";
//--------------------------------------------------------------------------
//
// Constructor
//
//--------------------------------------------------------------------------
/**
* Constructor.
*
* @param document In simple cases where a class extends EventDispatcher,
* the <code>document</code> parameter should not be used.
*
* @see flash.events.EventDispatcher
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public function RadioButtonGroup(document:IFlexDisplayObject = null)
{
super();
}
//--------------------------------------------------------------------------
//
// Variables
//
//--------------------------------------------------------------------------
/**
* @private
* Since there is no id, generate one, if needed.
*/
private var _name:String;
/**
* @private
* The document containing a reference to this RadioButtonGroup.
*/
private var document:IFlexDisplayObject;
/**
* @private
* An Array of the RadioButtons that belong to this group.
*/
private var radioButtons:Array /* of RadioButton */ = [];
/**
* @private
* Whether the group is enabled. This can be different than the individual
* radio buttons in the group.
*/
private var _enabled:Boolean = true;
//--------------------------------------------------------------------------
//
// Properties
//
//--------------------------------------------------------------------------
//----------------------------------
// enabled
//----------------------------------
[Inspectable(category="General", defaultValue="true")]
/**
* Determines whether selection is allowed. Note that the value returned
* only reflects the value that was explicitly set on the
* <code>RadioButtonGroup</code> and does not reflect any values explicitly
* set on the individual RadioButtons.
*
* @default true
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public function get enabled():Boolean
{
return _enabled;
}
/**
* @private
*/
public function set enabled(value:Boolean):void
{
if (_enabled == value)
return;
_enabled = value;
// The group state changed. Invalidate all the radio buttons. The
// radio button skin most likely will change.
for (var i:int = 0; i < numRadioButtons; i++)
getRadioButtonAt(i).invalidateSkinState();
}
//----------------------------------
// numRadioButtons
//----------------------------------
[Bindable("numRadioButtonsChanged")]
/**
* The number of RadioButtons that belong to this RadioButtonGroup.
*
* @default "0"
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public function get numRadioButtons():int
{
return radioButtons.length;
}
//----------------------------------
// selectedIndex
//----------------------------------
/**
* @private
*/
private var _selectedIndex:int = -1;
[Bindable("change")]
[Bindable("valueCommit")]
[Inspectable(category="General")]
/**
* The index of the selected RadioButton component in the group.
* If a RadioButton is not selected, this property is <code>-1</code>.
*
* @default -1
*
* @langversion 3.0
* @playerversion Flash 11.1
* @playerversion AIR 3.4
* @productversion Flex 4.10
*/
public function get selectedIndex():int
{
return _selectedIndex;
}
/**
* @private.
*/
public function set selectedIndex(newValue:int):void
{
if (newValue == _selectedIndex)
{
return;
}
if (newValue == -1)
{
setSelection(null, false);
dispatchEvent(new FlexEvent(FlexEvent.VALUE_COMMIT));
return;
}
changeSelection(newValue, false)
dispatchEvent(new FlexEvent(FlexEvent.VALUE_COMMIT));
}
//----------------------------------
// selectedValue
//----------------------------------
/**
* @private
* Storage for the selectedValue property.
*/
private var _selectedValue:Object;
[Bindable("change")]
[Bindable("valueCommit")]
[Inspectable(category="General")]
/**
* The <code>value</code> property of the selected
* RadioButton component in the group, if it has been set,
* otherwise, the <code>label</code> property of the selected RadioButton.
* If no RadioButton is selected, this property is <code>null</code>.
*
* <p>If you set <code>selectedValue</code>, Flex selects the
* first RadioButton component whose <code>value</code> or
* <code>label</code> property matches this value.</p>
*
* @default null
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public function get selectedValue():Object
{
if (selection)
{
return selection.value != null ?
selection.value :
selection.label;
}
return null;
}
/**
* @private.
*/
public function set selectedValue(value:Object):void
{
// The rbg might set the selectedValue before the radio buttons are
// initialized and inserted in the group. This will hold the selected
// value until it can be put in the group.
_selectedValue = value;
// Clear the exisiting selecton if there is one.
if (value == null)
{
setSelection(null, false);
return;
}
// Find the radio button value specified.
var n:int = numRadioButtons;
for (var i:int = 0; i < n; i++)
{
var radioButton:RadioButton = getRadioButtonAt(i);
if (radioButton.value == value ||
radioButton.label == value)
{
changeSelection(i, false);
_selectedValue = null;
dispatchEvent(new FlexEvent(FlexEvent.VALUE_COMMIT));
break;
}
}
}
//----------------------------------
// selection
//----------------------------------
/**
* @private
* Reference to the selected radio button.
*/
private var _selection:RadioButton;
[Bindable("change")]
[Bindable("valueCommit")]
[Inspectable(category="General")]
/**
* Contains a reference to the currently selected
* RadioButton component in the group.
* You can access this property in ActionScript only;
* it is not settable in MXML.
* Setting this property to <code>null</code> deselects the currently
* selected RadioButton component. A change event is not dispatched.
*
* @default null
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public function get selection():RadioButton
{
return _selection;
}
/**
* @private
*/
public function set selection(value:RadioButton):void
{
if ( _selection == value)
return;
// Going through the selection setter should never fire a change event.
setSelection(value, false);
}
//--------------------------------------------------------------------------
//
// Methods
//
//--------------------------------------------------------------------------
/**
* Implementation of the <code>IMXMLObject.initialized()</code> method
* to support deferred instantiation.
*
* @param document The MXML document that created this object.
*
* @param id The identifier used by document to refer to this object.
* If the object is a deep property on document, <code>id</code> is null.
*
* @see mx.core.IMXMLObject
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public function initialized(document:Object, id:String):void
{
_name = id;
this.document = document ?
IFlexDisplayObject(document) :
IFlexDisplayObject(FlexGlobals.topLevelApplication);
}
/**
* Returns the RadioButton component at the specified index.
*
* @param index The 0-based index of the RadioButton in the
* RadioButtonGroup.
*
* @return The specified RadioButton component if index is between
* 0 and <code>numRadioButtons</code> - 1. Returns
* <code>null</code> if the index is invalid.
*
* @see numRadioButtons
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public function getRadioButtonAt(index:int):RadioButton
{
if (index >= 0 && index < numRadioButtons)
return radioButtons[index];
return null;
}
/**
* @private
* String to uniquely identify this radio button group.
*/
mx_internal function get name():String
{
if (_name == null)
_name = NameUtil.createUniqueName(this);
return _name;
}
/**
* @private
* Add a radio button to the group. This can be called by
* RadioButton or via the addedHandler when applying a state.
*/
mx_internal function addInstance(instance:RadioButton):void
{
// During a state transition, called when rb is removed from
// display list.
instance.addEventListener(Event.REMOVED, radioButton_removedHandler);
radioButtons.push(instance);
// Apply group indices in "tab order" or "breadth-first" order.
radioButtons.sort(readOrderCompare);
for (var i:int = 0; i < radioButtons.length; i++)
radioButtons[i].indexNumber = i;
// There is a pending selectedValue. See if we can set it now.
if (_selectedValue != null)
selectedValue = _selectedValue;
// If this radio button is selected, then it becomes the selection
// for the group.
if (instance.selected == true)
selection = instance;
instance.radioButtonGroup = this;
instance.invalidateSkinState();
dispatchEvent(new Event("numRadioButtonsChanged"));
}
/**
* @private
* Remove a radio button from the group. This can be called by
* RadioButton or via the removedHandler when removing a state.
*/
private function removeInstance(instance:RadioButton):void
{
if (instance)
{
var foundInstance:Boolean = false;
for (var i:int = 0; i < numRadioButtons; i++)
{
var rb:RadioButton = getRadioButtonAt(i);
if (foundInstance)
{
// Decrement the indexNumber for each button after the removed button.
rb.indexNumber = rb.indexNumber - 1;
}
else if (rb == instance)
{
// During a state transition, called when rb is added back
// to display list.
instance.addEventListener(Event.ADDED, radioButton_addedHandler);
// Don't set the group to null. If this is being removed
// because the state changed, the group will be needed
// if the radio button is readded later because of another
// state transition.
//rb.group = null;
// If the rb is selected, leave the button itself selected
// but clear the selection for the group.
if (instance == _selection)
_selection = null;
instance.radioButtonGroup = null;
instance.invalidateSkinState();
// Remove the radio button from the internal array.
radioButtons.splice(i,1);
foundInstance = true;
// redo the same index because we removed the previous item at this index
i--;
}
}
if (foundInstance)
dispatchEvent(new Event("numRadioButtonsChanged"));
}
}
/**
* @private
*/
mx_internal function setSelection(value:RadioButton, fireChange:Boolean = true):void
{
if (_selection == value)
return;
if (value == null)
{
if (selection != null)
{
_selection.selected = false;
_selection = null;
_selectedIndex = -1;
if (fireChange)
dispatchEvent(new Event(Event.CHANGE));
}
}
else
{
var n:int = numRadioButtons;
for (var i:int = 0; i < n; i++)
{
if (value == getRadioButtonAt(i))
{
changeSelection(i, fireChange);
break;
}
}
}
dispatchEvent(new FlexEvent(FlexEvent.VALUE_COMMIT));
}
/**
* @private
*/
private function changeSelection(index:int, fireChange:Boolean = true):void
{
var rb:RadioButton = getRadioButtonAt(index);
if (rb && rb != _selection)
{
// Unselect the currently selected radio
if (_selection)
_selection.selected = false;
// Change the focus to the new radio.
// Set the state of the new radio to true.
// Fire a click event for the new radio.
// Fire a click event for the radio group.
_selection = rb;
_selection.selected = true;
_selectedIndex = index;
if (fireChange)
dispatchEvent(new Event(Event.CHANGE));
}
}
/**
* @private
* Sandbox root of RadioButton "a" in readOrderCompare().
*/
private var aSbRoot:DisplayObject;
/**
* @private
* Sandbox root of RadioButton "b" in readOrderCompare().
*/
private var bSbRoot:DisplayObject;
/**
* @private
* Comparison function used to sort items as they are added to the radioButtons array.
* Compares by tabIndex or if neither item defines a tabIndex, by "breadthOrder."
*
* Returns -1 if a is before b in sort order, 0 if a and b have same
* sort order and 1 if a after b in sort order.
*/
private function readOrderCompare(a:DisplayObject, b:DisplayObject):Number
{
var aParent:DisplayObjectContainer = a.parent;
var bParent:DisplayObjectContainer = b.parent;
if (!aParent || !bParent)
return 0;
// Only set when a is the radio button. The sandbox root should be the
// same for the parents.
if (a is RadioButton)
aSbRoot = RadioButton(a).systemManager.getSandboxRoot();
// Only set when b is the radio button. The sandbox root should be the
// same for the parents.
if (b is RadioButton)
bSbRoot = RadioButton(b).systemManager.getSandboxRoot();
// If reached the sandbox root of either then done.
if (aParent == aSbRoot || bParent == bSbRoot)
return 0;
// first check to see if we can compare by tabIndex
var aTabIndex:int = (a is InteractiveObject) ? InteractiveObject(a).tabIndex : -1;
var bTabIndex:int = (b is InteractiveObject) ? InteractiveObject(b).tabIndex : -1;
// if one of the items being compared has a defined tabIndex, compare by tabIndex
if(aTabIndex > -1 || bTabIndex > -1)
{
if (aTabIndex > bTabIndex)
return (bTabIndex == -1) ? -1 : 1; // items with assigned tabIndex come before those without
if (aTabIndex < bTabIndex)
return (aTabIndex == -1) ? 1 : -1; // items without assigned tabIndex come after those without
if (a == b)
return 0;
}
// if neither item has a defined tabIndex, compare by "breadthOrder"
var aNestLevel:int = (a is UIComponent) ? UIComponent(a).nestLevel : -1;
var bNestLevel:int = (b is UIComponent) ? UIComponent(b).nestLevel : -1;
var aIndex:int = 0;
var bIndex:int = 0;
if (aParent == bParent)
{
if (aParent is IVisualElementContainer && a is IVisualElement)
aIndex = IVisualElementContainer(aParent).getElementIndex(IVisualElement(a));
else
aIndex = DisplayObjectContainer(aParent).getChildIndex(a);
if (bParent is IVisualElementContainer && b is IVisualElement)
bIndex = IVisualElementContainer(bParent).getElementIndex(IVisualElement(b));
else
bIndex = DisplayObjectContainer(bParent).getChildIndex(b);
}
if (aNestLevel > bNestLevel || aIndex > bIndex)
return 1;
else if (aNestLevel < bNestLevel || bIndex > aIndex)
return -1;
else if (a == b)
return 0;
else // Nest levels are identical, compare ancestors.
return readOrderCompare(aParent, bParent);
}
//--------------------------------------------------------------------------
//
// Event Handlers
//
//--------------------------------------------------------------------------
/**
* @private
* Called during a state transition when rb is added back to display list.
*/
private function radioButton_addedHandler(event:Event):void
{
var rb:RadioButton = event.target as RadioButton;
if (rb)
{
//trace("radioButton_addedHandler", rb.id);
rb.removeEventListener(Event.ADDED, radioButton_addedHandler);
addInstance(rb);
}
}
/**
* @private
*/
private function radioButton_removedHandler(event:Event):void
{
var rb:RadioButton = event.target as RadioButton;
if (rb)
{
//trace("radioButton_removedHandler", rb.id);
rb.removeEventListener(Event.REMOVED, radioButton_removedHandler);
removeInstance(rb);
}
}
}
}