blob: 444557cee97d8abb950620b61b3d26be8b6a8d45 [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.events.Event;
import flash.events.KeyboardEvent;
import flash.events.MouseEvent;
import flash.ui.Keyboard;
import spark.components.supportClasses.ToggleButtonBase;
import mx.core.IFlexDisplayObject;
import mx.core.UIComponent;
import mx.core.mx_internal;
import mx.events.FlexEvent;
import mx.events.ItemClickEvent;
import mx.managers.IFocusManager;
import mx.managers.IFocusManagerGroup;
use namespace mx_internal;
//--------------------------------------
// 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", theme="mobile")]
/**
* @copy spark.components.supportClasses.GroupBase#style:symbolColor
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
[Style(name="symbolColor", type="uint", format="Color", inherit="yes", theme="spark, mobile")]
//--------------------------------------
// Excluded APIs
//--------------------------------------
[Exclude(name="cornerRadius", kind="style")]
[Exclude(name="icon", kind="style")]
[Exclude(name="textAlign", kind="style")]
//--------------------------------------
// Other metadata
//--------------------------------------
[AccessibilityClass(implementation="spark.accessibility.RadioButtonAccImpl")]
[IconFile("RadioButton.png")]
/**
* The RadioButton component allows the user make a single choice
* within a set of mutually exclusive choices.
* A RadioButtonGroup is composed of two or more RadioButton components
* with the same <code>groupName</code> property.
* While grouping RadioButton instances in a RadioButtonGroup is optional,
* a group lets you do things like set a single event handler on a group of
* RadioButtons, rather than on each individual RadioButton.
*
* <p>The RadioButton group can refer to the a group created by the
* <code>&lt;s:RadioButtonGroup&gt;</code> tag.
* The user selects only one member of the group at a time.
* Selecting an unselected group member deselects the currently selected
* RadioButton component within that group.</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>
*
* <p>The RadioButton component has the following default characteristics:</p>
* <table class="innertable">
* <tr>
* <th>Characteristic</th>
* <th>Description</th>
* </tr>
* <tr>
* <td>Default size</td>
* <td>Wide enough to display the text label of the component</td>
* </tr>
* <tr>
* <td>Minimum size</td>
* <td>18 pixels wide and 18 pixels high</td>
* </tr>
* <tr>
* <td>Maximum size</td>
* <td>10000 pixels wide and 10000 pixels high</td>
* </tr>
* <tr>
* <td>Default skin class</td>
* <td>spark.skins.spark.RadioButtonSkin</td>
* </tr>
* </table>
*
* @mxml
*
* <p>The <code>&lt;s:RadioButton&gt;</code> tag inherits all of the tag
* attributes of its superclass, and adds the following tag attributes:</p>
*
* <pre>
* &lt;s:RadioButton
* <strong>Properties</strong>
* enabled=""
* group="<i>the automatically created default RadioButtonGroup</i>"
* groupName="radioGroup"
* value="null"
* /&gt;
* </pre>
*
* @see spark.components.RadioButtonGroup
* @see spark.skins.spark.RadioButtonSkin
* @includeExample examples/RadioButtonExample.mxml
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public class RadioButton extends ToggleButtonBase implements IFocusManagerGroup
{
include "../core/Version.as";
//--------------------------------------------------------------------------
//
// Class mixins
//
//--------------------------------------------------------------------------
/**
* @private
* Placeholder for mixin by RadioButtonAccImpl.
*/
mx_internal static var createAccessibilityImplementation:Function;
//--------------------------------------------------------------------------
//
// Constructor
//
//--------------------------------------------------------------------------
/**
* Constructor.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public function RadioButton()
{
super();
// Button variables.
// Start out in the default group. The button is always in a group,
// either explicitly or implicitly.
groupName = "radioGroup";
}
//--------------------------------------------------------------------------
//
// Variables
//
//--------------------------------------------------------------------------
/**
* @private
* Default inital index value
*/
mx_internal var indexNumber:int = 0;
/**
* @private
* The RadioButtonGroup that this radio button is in. The group property
* should not be used to keep track of the radio button group for this radio
* button. During state transitions, the radio button may come
* and go from the group and the group property is not reset. The group
* property, if initially set, is needed when the radio button is readded
* to the group.
*/
mx_internal var radioButtonGroup:RadioButtonGroup = null;
//--------------------------------------------------------------------------
//
// Overridden properties
//
//--------------------------------------------------------------------------
//----------------------------------
// enabled
//----------------------------------
/**
* The RadioButton component is enabled if the
* RadioButtonGroup is enabled and the RadioButton itself is enabled.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
override public function get enabled():Boolean
{
// Is the radio button itself enabled?
if (!super.enabled)
return false;
// The button is enabled so it's enabled if it's not in a group
// or the group is enabled.
return !radioButtonGroup ||
radioButtonGroup.enabled;
}
//----------------------------------
// suggestedFocusSkinExclusions
//----------------------------------
/**
* @private
*/
private static const focusExclusions:Array = ["labelDisplay"];
/**
* @private
*/
override public function get suggestedFocusSkinExclusions():Array
{
return focusExclusions;
}
//--------------------------------------------------------------------------
//
// Properties
//
//--------------------------------------------------------------------------
//----------------------------------
// autoGroupIndex
//----------------------------------
/**
* @private
* automaticRadioButtonGroups is shared with halo radio button groups.
* Spark radio button groups are prefixed with _fx to differentiate the
* Halo groups which are stored in the same table.
*/
private function get autoGroupIndex():String
{
return "_spark_" + groupName;
}
//----------------------------------
// group
//----------------------------------
/**
* @private
* Storage for the group property.
*/
private var _group:RadioButtonGroup;
/**
* The RadioButtonGroup component to which this RadioButton belongs.
* When creating RadioButtons to put in a RadioButtonGroup, you
* should use either the <code>group</code> property
* or the <code>groupName</code> property for all of
* the buttons.
*
* @default the automatically created default RadioButtonGroup
* @see #groupName
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public function get group():RadioButtonGroup
{
// Debugger asks too soon.
if (!document)
return _group;
if (!_group)
{
// If using the default groupName, the button isn't added to the
// group until it is first accessed.
if (groupName && groupName != "")
{
var g:RadioButtonGroup;
try
{
g = RadioButtonGroup(document[groupName]);
}
catch(e:Error)
{
// Special automaticRadioButtonGroup slot to hold generated
// radio button groups. Shared with halo so prefix
// groupName to differentiate.
if (document.automaticRadioButtonGroups &&
document.automaticRadioButtonGroups[autoGroupIndex])
{
g = RadioButtonGroup(
document.automaticRadioButtonGroups[autoGroupIndex]);
}
}
if (!g)
{
g = new RadioButtonGroup(IFlexDisplayObject(document));
if (!document.automaticRadioButtonGroups)
document.automaticRadioButtonGroups = [];
document.automaticRadioButtonGroups[autoGroupIndex] = g;
}
else if (!(g is RadioButtonGroup))
{
return null;
}
_group = g;
}
}
return _group;
}
/**
* @private
*/
public function set group(value:RadioButtonGroup):void
{
if (_group == value)
return;
// If the button was in another group, remove it.
removeFromGroup();
_group = value;
// If the group is set then the groupName is the generated name of
// the rbg. If it's set to null, then set the groupName back to the
// default group so this button will move back to that group.
_groupName = value ? group.name : "radioGroup";
// Make sure this gets added to it's RadioButtonGroup
groupChanged = true;
invalidateProperties();
invalidateDisplayList();
}
//----------------------------------
// groupName
//----------------------------------
/**
* @private
* Storage for groupName property.
*/
private var _groupName:String;
/**
* @private
*/
private var groupChanged:Boolean = false;
[Inspectable(category="General", defaultValue="radioGroup")]
/**
* Specifies the name of the group to which this RadioButton component belongs, or
* specifies the value of the <code>id</code> property of a RadioButtonGroup component
* if this RadioButton is part of a group defined by a RadioButtonGroup component.
* All radio buttons with the same <code>groupName</code> property will be in the same tab group.
*
* <p>When creating
* radio buttons to put in a RadioButtonGroup, it is advisable to
* use either the <code>group</code> property
* or the <code>groupName</code> property for all of the buttons.</p>
*
* @default "radioGroup"
* @see #group
* @see mx.manager.IFocusManagerGroup#groupName
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public function get groupName():String
{
return _groupName;
}
/**
* @private
*/
public function set groupName(value:String):void
{
// A groupName must be non-empty string.
if (!value || value == "")
return;
// If the button was in another group, remove it, before changing the
// groupName.
removeFromGroup();
_groupName = value;
// Make sure get group recalculates the group.
_group = null;
// Make sure this gets added to it's RadioButtonGroup
groupChanged = true;
invalidateProperties();
invalidateDisplayList();
}
//----------------------------------
// selected
//----------------------------------
/**
* @private
*/
override public function set selected(value:Boolean):void
{
super.selected = value;
invalidateDisplayList();
}
//----------------------------------
// value
//----------------------------------
/**
* @private
* Storage for value property.
*/
private var _value:Object;
[Bindable("change")]
[Bindable("valueChanged")]
[Inspectable(category="General", defaultValue="")]
/**
* Optional user-defined value
* that is associated with a RadioButton component.
*
* @default null
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public function get value():Object
{
return _value;
}
/**
* @private
*/
public function set value(value:Object):void
{
if (_value == value)
return;
_value = value;
if (selected && group)
group.dispatchEvent(new FlexEvent(FlexEvent.VALUE_COMMIT));
}
//--------------------------------------------------------------------------
//
// Overridden methods: UIComponent, ButtonBase
//
//--------------------------------------------------------------------------
/**
* @private
*/
override protected function initializeAccessibility():void
{
if (RadioButton.createAccessibilityImplementation != null)
RadioButton.createAccessibilityImplementation(this);
}
/**
* @private
* Update properties before measurement/layout.
*/
override protected function commitProperties():void
{
if (groupChanged)
{
addToGroup();
groupChanged = false;
}
// Do this after radio button is added to the group so when the
// skin state is set, enabled and selected will return the correct values,
// and the correct skin will be used.
super.commitProperties();
}
/**
* @private
*/
override protected function updateDisplayList(unscaledWidth:Number,
unscaledHeight:Number):void
{
super.updateDisplayList(unscaledWidth, unscaledHeight);
// If this rb is selected and in a group, make sure it is the group
// selection. If it is not selected and it's in a group, make sure it
// is not the group selection.
if (group)
{
if (selected)
_group.selection = this;
else if (group.selection == this)
_group.selection = null;
}
}
/**
* @private
* Set radio button to selected and dispatch that there has been a change.
*/
override protected function buttonReleased():void
{
if (!enabled || selected)
return; // prevent a selected button from dispatching "click"
if (!radioButtonGroup)
addToGroup();
// Must call super.buttonReleased() before setting
// the group's selection.
super.buttonReleased();
group.setSelection(this);
// Dispatch an itemClick event from the RadioButtonGroup.
var itemClickEvent:ItemClickEvent =
new ItemClickEvent(ItemClickEvent.ITEM_CLICK);
itemClickEvent.label = label;
itemClickEvent.index = indexNumber;
itemClickEvent.relatedObject = this;
itemClickEvent.item = value;
group.dispatchEvent(itemClickEvent);
}
//--------------------------------------------------------------------------
//
// Methods
//
//--------------------------------------------------------------------------
/**
* @private
* Create radio button group if it does not exist
* and add the instance to the group.
*/
private function addToGroup():RadioButtonGroup
{
var g:RadioButtonGroup = group; // Trigger getting the group
if (g)
g.addInstance(this);
return g;
}
/**
* @private
*/
private function removeFromGroup():void
{
// If the radio button was in a group, remove it.
this.dispatchEvent(new Event(Event.REMOVED));
// It's possible that the radio button was in the auto group. If so,
// delete the group if there are no other radio buttons still in it.
// The radio button also could have been in an explicit RadioButtonGroup
// specified via group or in a RadioButtonGroup that was in the document,
// specified by groupName.
try
{
if (document.automaticRadioButtonGroups[autoGroupIndex].numRadioButtons == 0)
{
delete document.automaticRadioButtonGroups[autoGroupIndex];
}
}
catch(e:Error)
{
}
}
/**
* @private
* Set previous radio button in the group.
*/
private function setPrev(moveSelection:Boolean = true):void
{
var g:RadioButtonGroup = group;
var fm:IFocusManager = focusManager;
if (fm)
fm.showFocusIndicator = true;
for (var i:int = 1; i <= indexNumber; i++)
{
var radioButton:RadioButton =
g.getRadioButtonAt(indexNumber - i);
if (radioButton && isRadioButtonEnabled(radioButton))
{
if (moveSelection)
g.setSelection(radioButton);
radioButton.setFocus();
return;
}
}
if (moveSelection && g.getRadioButtonAt(indexNumber) != g.selection)
g.setSelection(this);
this.drawFocus(true);
}
/**
* @private
* Set the next radio button in the group.
*/
private function setNext(moveSelection:Boolean = true):void
{
var g:RadioButtonGroup = group;
var fm:IFocusManager = focusManager;
if (fm)
fm.showFocusIndicator = true;
for (var i:int = indexNumber + 1; i < g.numRadioButtons; i++)
{
var radioButton:RadioButton = g.getRadioButtonAt(i);
if (radioButton && isRadioButtonEnabled(radioButton))
{
if (moveSelection)
g.setSelection(radioButton);
radioButton.setFocus();
return;
}
}
if (moveSelection && g.getRadioButtonAt(indexNumber) != g.selection)
g.setSelection(this);
this.drawFocus(true);
}
/**
* @private
* When using keyboard navigation, need to make sure we don't move to
* a radio button that's not enabled because it's in a different
* container that isn't enabled.
*/
private function isRadioButtonEnabled(rb:RadioButton):Boolean
{
if (!rb.enabled)
return false;
var sbRoot:DisplayObject = rb.systemManager.getSandboxRoot();
// If it's in another UIComponent like a container, is that enabled?
var p:DisplayObject = rb.parent;
while (p && p != sbRoot)
{
if (p is UIComponent && !UIComponent(p).enabled)
return false;
p = p.parent;
}
return true;
}
/**
* @private
*/
private function setThis():void
{
if (!radioButtonGroup)
addToGroup();
var g:RadioButtonGroup = group;
if (g.selection != this)
g.setSelection(this);
}
//--------------------------------------------------------------------------
//
// Overridden event handlers: UIComponent
//
//--------------------------------------------------------------------------
/**
* @private
* Support the use of keyboard within the group.
*/
override protected function keyDownHandler(event:KeyboardEvent):void
{
// Have to make sure we don't move to a radio button that's not enabled
// because it's in a different container that is not enabled.
if (event.isDefaultPrevented())
return;
// If rtl layout, need to swap LEFT and RIGHT so correct action
// is done.
var keyCode:uint = mapKeycodeForLayoutDirection(event);
switch (keyCode)
{
case Keyboard.DOWN:
{
setNext(!event.ctrlKey);
event.preventDefault();
break;
}
case Keyboard.UP:
{
setPrev(!event.ctrlKey);
event.preventDefault();
break;
}
case Keyboard.LEFT:
{
setPrev(!event.ctrlKey);
event.preventDefault();
break;
}
case Keyboard.RIGHT:
{
setNext(!event.ctrlKey);
event.preventDefault();
break;
}
case Keyboard.SPACE:
{
setThis();
//fall through, no break
}
default:
{
super.keyDownHandler(event);
break;
}
}
}
}
}