blob: e74aa7b640fedec7e46b8e07dfb51751591c2e34 [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 mx.containers
{
import flash.display.DisplayObject;
import flash.events.Event;
import mx.containers.utilityClasses.BoxLayout;
import mx.containers.utilityClasses.Flex;
import mx.controls.FormItemLabel;
import mx.controls.Label;
import mx.core.Container;
import mx.core.EdgeMetrics;
import mx.core.IFlexDisplayObject;
import mx.core.IUIComponent;
import mx.core.mx_internal;
import mx.core.ScrollPolicy;
import mx.styles.CSSStyleDeclaration;
use namespace mx_internal;
//--------------------------------------
// Styles
//--------------------------------------
include "../styles/metadata/GapStyles.as";
/**
* Horizontal alignment of children in the container.
* Possible values are <code>"left"</code>, <code>"center"</code>,
* and <code>"right"</code>.
*
* @default "left"
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
[Style(name="horizontalAlign", type="String", enumeration="left,center,right", inherit="no")]
/**
* Number of pixels between the label and child components of the form item.
*
* @default 14
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
[Style(name="indicatorGap", type="Number", format="Length", inherit="yes")]
/**
* Specifies the skin to use for the required field indicator.
*
* The default value is the "mx.containers.FormItem.Required" symbol in the Assets.swf file.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
[Style(name="indicatorSkin", type="Class", inherit="no")]
/**
* Name of the CSS Style declaration to use for the styles for the
* FormItem's label.
* By default, the label uses the FormItem's inheritable styles or
* those declared by FormItemLabel. This style should be used instead
* of FormItemLabel.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
[Style(name="labelStyleName", type="String", inherit="no")]
/**
* Width of the form labels.
* The default is the length of the longest label in the form.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
[Style(name="labelWidth", type="Number", format="Length", inherit="yes")]
/**
* Number of pixels between the container's bottom border
* and the bottom edge of its content area.
*
* @default 0
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
[Style(name="paddingBottom", type="Number", format="Length", inherit="no")]
/**
* Number of pixels between the container's right border
* and the right edge of its content area.
*
* @default 0
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
[Style(name="paddingRight", type="Number", format="Length", inherit="no")]
/**
* Number of pixels between the container's top border
* and the top edge of its content area.
*
* @default 0
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
[Style(name="paddingTop", type="Number", format="Length", inherit="no")]
//--------------------------------------
// Excluded APIs
//--------------------------------------
[Exclude(name="focusIn", kind="event")]
[Exclude(name="focusOut", kind="event")]
[Exclude(name="focusBlendMode", kind="style")]
[Exclude(name="focusSkin", kind="style")]
[Exclude(name="focusThickness", kind="style")]
[Exclude(name="focusInEffect", kind="effect")]
[Exclude(name="focusOutEffect", kind="effect")]
//--------------------------------------
// Other metadata
//--------------------------------------
[IconFile("FormItem.png")]
[Alternative(replacement="spark.components.FormItem", since="4.5")]
/**
* The FormItem container defines a label and one or more children
* arranged horizontally or vertically.
* Children can be controls or other containers.
* A single Form container can hold multiple FormItem containers.
*
* @mxml
*
* <p>The <code>&lt;mx:FormItem&gt;</code> tag inherits all of the tag
* attributes of its superclass, except <code>paddingLeft</code>,
* and adds the following tag attributes:</p>
*
* <pre>
* &lt;mx:FormItem
* <strong>Properties</strong>
* direction="vertical|horizontal"
* label=""
* required="false|true"
*
* <strong>Styles</strong>
* horizontalAlign="left|center|right"
* horizontalGap="8"
* indicatorGap="14"
* indicatorSkin="<i>'mx.containers.FormItem.Required' symbol in Assets.swf</i>"
* labelStyleName=""
* labelWidth="<i>Calculated</i>"
* paddingBottom="0"
* paddingRight="0"
* paddingTop="0"
* verticalGap="6"
* &gt;
* ...
* <i>child tags</i>
* ...
* &lt;/mx:FormItem&gt;
* </pre>
*
* @see mx.containers.Form
* @see mx.containers.FormItemDirection
* @see mx.containers.FormHeading
* @see mx.controls.FormItemLabel
*
* @includeExample examples/FormExample.mxml
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public class FormItem extends Container
{
include "../core/Version.as";
//--------------------------------------------------------------------------
//
// Constructor
//
//--------------------------------------------------------------------------
/**
* Constructor.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function FormItem()
{
super();
_horizontalScrollPolicy = ScrollPolicy.OFF;
_verticalScrollPolicy = ScrollPolicy.OFF;
verticalLayoutObject.target = this;
verticalLayoutObject.direction = BoxDirection.VERTICAL;
}
//--------------------------------------------------------------------------
//
// Variables
//
//--------------------------------------------------------------------------
/**
* @private
* A reference to the FormItemLabel subcomponent.
*/
private var labelObj:Label;
/**
* @private
* A reference to the "required" indicator.
*/
private var indicatorObj:IFlexDisplayObject;
/**
* @private
*/
private var guessedRowWidth:Number;
/**
* @private
*/
private var guessedNumColumns:int;
/**
* @private
*/
private var numberOfGuesses:int = 0;
/**
* @private
* We use the VBox algorithm when direction="vertical" and make a few adjustments
*/
mx_internal var verticalLayoutObject:BoxLayout = new BoxLayout();
//--------------------------------------------------------------------------
//
// Overridden properties
//
//-------------------------------------------------------------------------
//----------------------------------
// label
//----------------------------------
/**
* @private
* Storage for the label property.
*/
private var _label:String = "";
private var labelChanged:Boolean = false;
[Bindable("labelChanged")]
[Inspectable(category="General", defaultValue="")]
/**
* Text label for the FormItem. This label appears to the left of the
* child components of the form item. The position of the label is
* controlled by the <code>textAlign</code> style property.
*
* @default ""
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
override public function get label():String
{
return _label;
}
/**
* @private
*/
override public function set label(value:String):void
{
_label = value;
labelChanged = true;
invalidateProperties();
invalidateSize();
invalidateDisplayList();
// Changing the label could affect the overall form label width
// so we need to invalidate our parent's size here too
if (parent is Form)
Form(parent).invalidateLabelWidth();
dispatchEvent(new Event("labelChanged"));
}
//--------------------------------------------------------------------------
//
// Properties
//
//--------------------------------------------------------------------------
//----------------------------------
// direction
//----------------------------------
/**
* @private
* Storage for the direction property.
*/
private var _direction:String = FormItemDirection.VERTICAL;
[Bindable("directionChanged")]
[Inspectable(category="General", enumeration="vertical,horizontal", defaultValue="vertical")]
/**
* Direction of the FormItem subcomponents.
* Possible MXML values are <code>"vertical"</code>
* or <code>"horizontal"</code>.
* The default MXML value is <code>"vertical"</code>.
* Possible ActionScript values are <code>FormItemDirection.VERTICAL</code>
* or <code>FormItemDirection.HORIZONTAL</code>.
*
* <p>When <code>direction</code> is <code>"vertical"</code>,
* the children of the FormItem are stacked vertically
* to the right of the FormItem label.
* When <code>direction</code> is <code>"horizontal"</code>,
* the children are placed in a single row (if they fit),
* or in two equally-sized columns.</p>
*
* <p>If you need more control over the layout of FormItem children,
* you can use a container such as Grid or Tile as the direct child
* of the FormItem and put the desired controls inside it.</p>
*
* @default FormItemDirection.VERTICAL
* @see mx.containers.FormItemDirection
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function get direction():String
{
return _direction;
}
/**
* @private
*/
public function set direction(value:String):void
{
_direction = value;
invalidateSize();
invalidateDisplayList();
dispatchEvent(new Event("directionChanged"));
}
//----------------------------------
// itemLabel
//----------------------------------
[Bindable("itemLabelChanged")]
/**
* A read-only reference to the FormItemLabel subcomponent
* displaying the label of the FormItem.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function get itemLabel():Label
{
return labelObj;
}
//----------------------------------
// required
//----------------------------------
/**
* @private
* Storage for the required property.
*/
private var _required:Boolean = false;
[Bindable("requiredChanged")]
[Inspectable(category="General", defaultValue="false")]
/**
* If <code>true</code>, display an indicator
* that the FormItem children require user input.
* If <code>false</code>, indicator is not displayed.
*
* <p>This property controls the indicator display only.
* You must attach a validator to the children
* if you require input validation.</p>
*
* @default false
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function get required():Boolean
{
return _required;
}
/**
* @private
*/
public function set required(value:Boolean):void
{
if (value != _required)
{
_required = value;
invalidateDisplayList();
dispatchEvent(new Event("requiredChanged"));
}
}
//--------------------------------------------------------------------------
//
// Overridden properties: UIComponent
//
//--------------------------------------------------------------------------
/**
* @private
*/
override public function set includeInLayout(value:Boolean):void
{
super.includeInLayout = value;
if(value)
invalidateSize();
}
//--------------------------------------------------------------------------
//
// Overridden methods: UIComponent
//
//--------------------------------------------------------------------------
/**
* @private
*/
override protected function createChildren():void
{
super.createChildren();
if (!labelObj)
{
labelObj = new FormItemLabel();
var labelStyleName:String = getStyle("labelStyleName");
if (labelStyleName)
{
var styleDecl:CSSStyleDeclaration =
styleManager.getMergedStyleDeclaration("." + labelStyleName);
if (styleDecl)
labelObj.styleDeclaration = styleDecl;
}
rawChildren.addChild(labelObj);
dispatchEvent(new Event("itemLabelChanged"));
}
}
/**
* @private
*/
override protected function commitProperties():void
{
super.commitProperties();
if (labelChanged)
{
labelObj.text = label;
labelObj.validateSize();
labelChanged = false;
}
}
/**
* Calculates the preferred, minimum and maximum sizes of the FormItem.
* See the <code>UIComponent.measure()</code> method for more information
* about the <code>measure()</code> method.
*
* <p>The <code>FormItem.measure()</code> method first determines
* the number of columns to use.
* If the <code>direction</code> property is
* <code>FormItemDirection.HORIZONTAL</code>,
* all controls will be placed in a single row if possible.
* If the controls cannot fit in a single row, they are split
* into two columns. If that does not work, then use
* a single column. If <code>direction</code> is
* <code>FormItemDirection.VERTICAL</code>, the controls are
* placed in a single column (like <code>VBox</code>).</p>
*
* <p>A FormItem contains two areas: the label area
* and the controls area.
* The size of the label is the same
* regardless of the direction of the controls.
* The size of the control area depends on how many rows
* and columns are used.</p>
*
* <p>The width of the label area is determined by the
* <code>labelWidth</code> style property.
* If this property is <code>undefined</code> (which is the default),
* the width of the largest label in the parent Form container
* is used.</p>
*
* <p>If all children are on a single row, the width of the
* control area is the sum of the widths of all the children
* plus <code>horizontalGap</code> space between the children.</p>
*
* <p>If the children are on a single column,
* the width of the control area is the width of the widest child.</p>
*
* <p>If the children are on multiple rows and columns,
* the width of the widest child is the column width,
* and the width of the control area is the column width
* multiplied by the number of columns plus the
* <code>horizontalGap</code> space between each column.</p>
*
* <p><code>measuredWidth</code> is set to the
* width of the label area plus the width of the control area
* plus the value of the <code>indicatorGap</code> style property.
* The values of the <code>paddingLeft</code> and
* <code>paddingRight</code> style properties
* and the width of the border are also added.</p>
*
* <p><code>measuredHeight</code> is set to the
* sum of the preferred heights of all rows of children,
* plus <code>verticalGap</code> space between each child.
* The <code>paddingTop</code> and <code>paddingBottom</code>
* style properties and the height of the border are also added.</p>
*
* <p><code>measuredMinWidth</code> is set to the width of the
* label area plus the minimum width of the control area
* plus the value of the <code>indicatorGap</code> style property.
* The values of the <code>paddingLeft</code> and
* <code>paddingRight</code> style properties
* and the width of the border are also added.</p>
*
* <p><code>measuredMinHeight</code> is set to the
* sum of the minimum heights of all rows of children,
* plus <code>verticalGap</code> space between each child.
* The <code>paddingTop</code> and <code>paddingBottom</code>
* style properties and the height of the border are also added.</p>
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
override protected function measure():void
{
super.measure();
if (direction == FormItemDirection.VERTICAL)
measureVertical();
else
measureHorizontal();
}
private function measureVertical():void
{
// use VBox and then take into account label, indictor gap
verticalLayoutObject.measure();
// Need to include label and indictor gap for width
var extraWidth:Number = calculateLabelWidth() + getStyle("indicatorGap");
measuredMinWidth += extraWidth;
measuredWidth += extraWidth;
// need to include label for height
var labelHeight:Number = labelObj.getExplicitOrMeasuredHeight();
measuredMinHeight = Math.max(measuredMinHeight, labelHeight);
measuredHeight = Math.max(measuredHeight, labelHeight);
}
private function measureHorizontal():void
{
// Take a guess at the number of columns we are going to use.
// Initially guessedRowWidth is NaN (which means we try to squeeze
// it on one row), but after first call to updateDisplayList, it
// gets updated with the approximate width the children can have.
// This lets use better estimate how many columns we can have
// Check out updateDisplayList() for more info on this and what if
// our initial guess was wrong.
var numColumns:int = guessedNumColumns =
calcNumColumns(guessedRowWidth);
var horizontalGap:Number = getStyle("horizontalGap");
var verticalGap:Number = getStyle("verticalGap");
var indicatorGap:Number = getStyle("indicatorGap");
var tempMinWidth:Number = 0;
var tempWidth:Number = 0;
var tempMinHeight:Number = 0;
var tempHeight:Number = 0;
var minWidth:Number = 0;
var minHeight:Number = 0;
var preferredWidth:Number = 0;
var preferredHeight:Number = 0;
var maxPreferredWidth:Number = 0;
var i:int;
var col:int = 0;
var child:IUIComponent;
// If the children span multiple rows,
// then updateDisplayList() (below) sets all column widths to the
// preferredWidth of the largest child.
// To find the max. preferred width, just loop through
// all the children
if (numColumns < numChildren)
{
for (i = 0; i < numChildren; i++)
{
child = getLayoutChildAt(i);
if (!child.includeInLayout)
continue;
maxPreferredWidth = Math.max(
maxPreferredWidth, child.getExplicitOrMeasuredWidth());
}
}
var row:int = 0;
// Now with the columnWidth (maxPreferredWidth) figured out,
// figure out the number of rows needed and calculate the
// minWidth/height and the preferredWidth/Height
for (i = 0; i < numChildren; i++)
{
child = getLayoutChildAt(i);
if (!child.includeInLayout)
continue;
tempMinWidth += !isNaN(child.percentWidth) ?
child.minWidth :
child.getExplicitOrMeasuredWidth();
tempWidth += (maxPreferredWidth > 0) ?
maxPreferredWidth :
child.getExplicitOrMeasuredWidth();
// if on anything but 1st column
if (col > 0)
{
tempMinWidth += horizontalGap;
tempWidth += horizontalGap;
}
tempMinHeight = Math.max(tempMinHeight,
!isNaN(child.percentHeight) ?
child.minHeight :
child.getExplicitOrMeasuredHeight());
tempHeight = Math.max(tempHeight,
child.getExplicitOrMeasuredHeight());
col++;
// if at the last column in the row
if (col >= numColumns || i == (numChildren-1))
{
minWidth = Math.max(minWidth, tempMinWidth);
preferredWidth = Math.max(preferredWidth, tempWidth);
minHeight += tempMinHeight;
preferredHeight += tempHeight;
// if on anything but 1st row
if (row > 0)
{
minHeight += verticalGap;
preferredHeight += verticalGap;
}
col = 0;
row++;
tempMinWidth = 0;
tempWidth = 0;
tempMinHeight = 0;
tempHeight = 0;
}
}
// Need to also account for label and indicator gap
var labelWidth:Number = calculateLabelWidth() + indicatorGap;
minWidth += labelWidth;
preferredWidth += labelWidth;
if (label != null && label != "")
{
minHeight = Math.max(minHeight,
labelObj.getExplicitOrMeasuredHeight());
preferredHeight = Math.max(preferredHeight,
labelObj.getExplicitOrMeasuredHeight());
}
var vm:EdgeMetrics = viewMetricsAndPadding;
minHeight += vm.top + vm.bottom;
minWidth += vm.left + vm.right;
preferredHeight += vm.top + vm.bottom;
preferredWidth += vm.left + vm.right;
measuredMinWidth = minWidth;
measuredMinHeight = minHeight;
measuredWidth = preferredWidth;
measuredHeight = preferredHeight;
}
/**
* Responds to size changes by setting the positions and sizes
* of this container's children.
* See the <code>UIComponent.updateDisplayList()</code> method
* for more information about the <code>updateDisplayList()</code> method.
*
* <p>See the <code>FormItem.measure()</code> method for more
* information on how the FormItem controls are positioned.</p>
*
* <p>The label is aligned in the label area according to the <code>textAlign</code> style property.
* All labels in a form are aligned with each other.</p>
*
* <p>If the <code>required</code> property is <code>true</code>,
* a symbol indicating the field is required is placed between
* the label and the controls.</p>
*
* <p>The controls are positioned in columns, as described in the
* documentation for the <code>measure()</code> method.
* The <code>horizontalAlign</code> style property
* determines where the controls are placed horizontally.</p>
*
* <p>When the <code>direction</code> property is
* <code>"vertical"</code>, any child that has no <code>width</code>
* specified uses the <code>measuredWidth</code> rounded up
* to the nearest 1/4 width of the control area.
* This is done to avoid jagged right edges of controls.</p>
*
* <p>This method calls the <code>super.updateDisplayList()</code>
* method before doing anything else.</p>
*
* @param unscaledWidth Specifies the width of the component, in pixels,
* in the component's coordinates, regardless of the value of the
* <code>scaleX</code> property of the component.
*
* @param unscaledHeight Specifies the height of the component, in pixels,
* in the component's coordinates, regardless of the value of the
* <code>scaleY</code> property of the component.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
override protected function updateDisplayList(unscaledWidth:Number,
unscaledHeight:Number):void
{
super.updateDisplayList(unscaledWidth, unscaledHeight);
if (direction == FormItemDirection.VERTICAL)
{
updateDisplayListVerticalChildren(unscaledWidth, unscaledHeight);
}
else
{
updateDisplayListHorizontalChildren(unscaledWidth, unscaledHeight);
}
// Position our label now that our children have been positioned.
// Moving our children can affect the baselinePosition. (Bug 86725)
var vm:EdgeMetrics = viewMetricsAndPadding;
// we have x,y with respect to viewMetricsAndPadding here b/c
// labelObj is added to rawChildren. The children of FormItem
// are added to a container object, so start at paddingLeft
// and paddingTop (as the containerObj has border around it)
var x:Number = vm.left;
var y:Number = vm.top;
var labelWidth:Number = calculateLabelWidth();
var child:IUIComponent;
var childBaseline:Number;
// Position our label.
if (numChildren > 0)
{
// Center label with first child
child = getLayoutChildAt(0);
childBaseline = child.baselinePosition;
if (!isNaN(childBaseline))
y += childBaseline - labelObj.baselinePosition;
}
// Set label size.
labelObj.setActualSize(labelWidth, labelObj.getExplicitOrMeasuredHeight());
labelObj.move(x, y);
x += labelWidth;
// Position the "required" indicator.
displayIndicator(x, y);
}
private function updateDisplayListVerticalChildren(unscaledWidth:Number,
unscaledHeight:Number):void
{
// Need to include label and indictor gap for width
var extraWidth:Number = calculateLabelWidth() + getStyle("indicatorGap");
// a little bit of a hack, but need the target to have a good minWidth as well
if (!isNaN(explicitMinWidth))
_explicitMinWidth -= extraWidth;
else if (!isNaN(measuredMinWidth))
measuredMinWidth -= extraWidth;
// Use the VBox algorithm and then shift everything to the left by
// labeWidth + indicatorGap. Also put where the label should go
verticalLayoutObject.updateDisplayList(unscaledWidth - extraWidth, unscaledHeight);
// reset minWidth from hack
if (!isNaN(explicitMinWidth))
_explicitMinWidth += extraWidth;
else if (!isNaN(measuredMinWidth))
measuredMinWidth += extraWidth;
var n:Number = numChildren;
var child:IUIComponent;
for (var i:Number = 0; i < n; i++)
{
child = getLayoutChildAt(i);
child.move(child.x + extraWidth, child.y);
}
}
private function updateDisplayListHorizontalChildren(unscaledWidth:Number,
unscaledHeight:Number):void
{
// The layout algorithm is similar to BoxLayout, but it has to
// figure out how many rows/columns to use
var vm:EdgeMetrics = viewMetricsAndPadding;
var labelWidth:Number = calculateLabelWidth();
var indicatorGap:Number = getStyle("indicatorGap");
var horizontalGap:Number = getStyle("horizontalGap");
var verticalGap:Number = getStyle("verticalGap");
var paddingLeft:Number = getStyle("paddingLeft");
var paddingTop:Number = getStyle("paddingTop");
var horizontalAlignValue:Number = getHorizontalAlignValue();
var mw:Number = scaleX > 0 && scaleX != 1 ?
minWidth / Math.abs(scaleX) :
minWidth;
var mh:Number = scaleY > 0 && scaleY != 1 ?
minHeight / Math.abs(scaleY) :
minHeight;
var w:Number = Math.max(unscaledWidth, mw) - vm.left - vm.right;
var h:Number = Math.max(unscaledHeight, mh) - vm.top - vm.bottom;
var maxWidth:Number = 0;
var controlWidth:Number = w - labelWidth - indicatorGap;
if (controlWidth < 0)
controlWidth = 0;
var i:int;
var child:IUIComponent;
var childWidth:Number;
var childHeight:Number;
// Earlier, the measure function took a guess at the number
// of columns, but that function didn't know the width of this
// FormItem. Now that we know the width, we may discover that the
// guess was wrong. In that case, call invalidateSize(), so that
// we loop back and repeat the measurement phase again.
//
// It's possible that we might introduce an infinite loop - the
// new guess might change the layout, so that the guess once again
// is found to be wrong. If we get back here a second time and
// discover that the guess is still wrong, we'll just live with it.
var numColumns:int = calcNumColumns(controlWidth);
var col:int = 0;
if (numColumns != guessedNumColumns && isNaN(explicitWidth))
{
if (numberOfGuesses < 2)
{
// try again where third one's a charm
// we actually pick 3 for a real reason.
// numColumns is either 1,2, or numChildren
guessedRowWidth = controlWidth;
numberOfGuesses++;
invalidateSize();
return;
}
else
{
// make best of what we got
// we use our old guess b/c that's what we
// measured() for
numColumns = guessedNumColumns;
numberOfGuesses = 0;
}
}
else
{
numberOfGuesses = 0;
}
var left:Number = paddingLeft + labelWidth + indicatorGap;
var top:Number = paddingTop;
var x:Number = left;
var y:Number = top;
var wPref:Number;
var hPref:Number;
var percentWidth:Number;
var percentHeight:Number;
var numChildrenWithOwnSpace:int = numChildren;
for (i = 0; i < numChildren; i++)
{
if (!IUIComponent(getChildAt(i)).includeInLayout)
numChildrenWithOwnSpace--;
}
// Special case for single row - use the HBox layout algorithm.
if (numColumns == numChildrenWithOwnSpace)
{
var excessSpace:Number = Flex.flexChildWidthsProportionally(
this, controlWidth - (numChildrenWithOwnSpace - 1) * horizontalGap, h);
left += (excessSpace * horizontalAlignValue);
for (i = 0; i < numChildren; i++)
{
child = getLayoutChildAt(i);
if (!child.includeInLayout)
continue;
child.move(Math.floor(left), top);
left += child.width + horizontalGap;
}
}
else
{
// We need to determine the column width. We do this by
// determining the widest child. This time we take into
// account percentages (in calcNumColumns we don't b/c we
// don't know what controlWidth will be)
for (i = 0; i < numChildren; i++)
{
child = getLayoutChildAt(i);
if (!child.includeInLayout)
continue;
wPref = child.getExplicitOrMeasuredWidth();
percentWidth = child.percentWidth;
childWidth = !isNaN(percentWidth) ? (percentWidth*controlWidth)/100 : wPref;
childWidth = Math.max(child.minWidth, Math.min(child.maxWidth, childWidth));
maxWidth = Math.max(maxWidth, childWidth);
}
// need to make sure we didn't overstep our bounds,
// and we need to account for horizontalGap with maxWidth
maxWidth = Math.min(maxWidth, Math.floor((controlWidth -
((numColumns-1) * horizontalGap))/numColumns))
// Determine the left side for the columns:
// (left over space * horizontalAlignValue)
var widthSlop:Number = controlWidth -
(numColumns * maxWidth + (numColumns - 1) * horizontalGap);
if (widthSlop < 0)
widthSlop = 0;
left += (widthSlop * horizontalAlignValue);
var rowPercentHeight:Number = 0;
var rowMaxHeight:Number = 0;
var totalPercentHeight:Number = 0;
var heightSpaceForChildren:Number = h;
var heightSpaceToDistribute:Number = heightSpaceForChildren;
col = 0;
// Place the children in columns
// need to figure out spacePerPercent for height first.
// Loop through all children and take into account
// the fixed height and the percentage height for each row
for (i = 0; i < numChildren; i++)
{
child = getLayoutChildAt(i);
if (!child.includeInLayout)
{
// need to make sure not done with the row b/c then need to do stuff
if (i == numChildren - 1)
{
heightSpaceToDistribute -= rowMaxHeight;
// if not on last row, take into account verticalGap
if (i != (numChildren-1))
heightSpaceToDistribute -= verticalGap;
// discount some of the percent height if there's fixed height in same row
if (rowMaxHeight > 0 && rowPercentHeight > 0)
{
rowPercentHeight = Math.max(0, rowPercentHeight -
(100*rowMaxHeight)/heightSpaceForChildren);
}
totalPercentHeight += rowPercentHeight;
rowMaxHeight = 0;
rowPercentHeight = 0;
col = 0;
}
continue;
}
if (!isNaN(child.percentHeight))
{
rowPercentHeight = Math.max(rowPercentHeight,
child.percentHeight);
}
else
{
rowMaxHeight = Math.max(rowMaxHeight,
child.getExplicitOrMeasuredHeight());
}
// if done with the row, sum up the height totals
if (++col >= numColumns || i == numChildren - 1)
{
heightSpaceToDistribute -= rowMaxHeight;
// if not on last row, take into account verticalGap
if (i != (numChildren-1))
heightSpaceToDistribute -= verticalGap;
// discount some of the percent height if there's fixed height in same row
if (rowMaxHeight > 0 && rowPercentHeight > 0)
{
rowPercentHeight = Math.max(0, rowPercentHeight -
(100*rowMaxHeight)/heightSpaceForChildren);
}
totalPercentHeight += rowPercentHeight;
rowMaxHeight = 0;
rowPercentHeight = 0;
col = 0;
}
}
var done:Boolean = false;
// Continue as long as there are some remaining flexible children.
// The "done" flag isn't strictly necessary, except that it catches
// cases where round-off error causes totalPercent to not exactly
// equal zero.
// explicitHeightChildren is for storing explicitly set heights
// because of a max/min problem. By default, everything is set to
// -1, which means to look at the child's own height (may
// be explicitHeight or based on percentHeight). If it's not
// -1, then use the value in explicitHeightChildren as it's
// either the max or min height the child can be.
var explicitHeightChildren:Array = new Array(numChildren);
for (i = 0; i < numChildren; i++)
{
explicitHeightChildren[i] = -1;
}
// We now do something a little tricky so that we can
// support partial filling of the space. If our total
// percent < 100% then we can trim off some space.
var unused:Number = heightSpaceToDistribute -
(heightSpaceForChildren * totalPercentHeight / 100);
if (unused > 0)
heightSpaceToDistribute -= unused;
do
{
done = true; // we are optimistic
// Space for flexible children is the total amount of space
// available minus the amount of space consumed by non-flexible
// components.Divide that space in proportion to the percent
// of the child
var heightSpacePerPercent:Number = heightSpaceToDistribute / totalPercentHeight;
col = 0;
x = left;
y = top;
var rowMaxChildHeight:Number = 0;
// Attempt to divide out the space using our percent amounts,
// if we hit its limit then that control becomes 'non-flexible'
// and we run the whole space to distribute calculation again.
for (i = 0; i < numChildren; i++)
{
child = getLayoutChildAt(i);
if (!child.includeInLayout)
continue;
wPref = child.getExplicitOrMeasuredWidth();
hPref = child.getExplicitOrMeasuredHeight();
percentWidth = child.percentWidth;
percentHeight = child.percentHeight;
// childWidth can't exceed maxWidth (columnWidth) or it's own max/min widths
childWidth = Math.min(maxWidth,
!isNaN(percentWidth) ? (percentWidth*controlWidth)/100 : wPref);
childWidth = Math.max(child.minWidth, Math.min(child.maxWidth, childWidth));
// if the child has been explicitely sized b/c of a max/min issue
// from a previous run, then use that. Otherwise, ask the child for
// it's height.
if (explicitHeightChildren[i] != -1)
{
childHeight = explicitHeightChildren[i];
}
else
{
childHeight = !isNaN(percentHeight) ?
percentHeight*heightSpacePerPercent : hPref;
// If our flexiblity calc say grow/shrink more than we are
// allowed, then we grow/shrink whatever we can, remove
// ourselves from the array (with explicitHeightChildren)
// for the next pass, and start
// the loop over again so that the space that we weren't
// able to consume / release can be re-used by others.
if (childHeight < child.minHeight)
{
childHeight = child.minHeight;
totalPercentHeight -= child.percentHeight;
heightSpaceToDistribute -= childHeight;
explicitHeightChildren[i] = childHeight;
done = false;
break;
}
else if (childHeight > child.maxHeight)
{
childHeight = child.maxHeight;
totalPercentHeight -= child.percentHeight;
heightSpaceToDistribute -= childHeight;
explicitHeightChildren[i] = childHeight;
done = false;
break;
}
}
// All is well, let's carry on...
if (child.scaleX == 1 && child.scaleY == 1)
{
child.setActualSize(Math.floor(childWidth),
Math.floor(childHeight));
}
else
{
child.setActualSize(childWidth, childHeight);
}
// horizontal align all of controlWidth and also children within each column
var xOffset:Number = (maxWidth - child.width) * horizontalAlignValue;
child.move(Math.floor(x + xOffset), Math.floor(y));
rowMaxChildHeight = Math.max(rowMaxChildHeight, child.height);
if (++col >= numColumns)
{
x = left;
col = 0;
y += rowMaxChildHeight + verticalGap;
rowMaxChildHeight = 0;
}
else
{
x += maxWidth + horizontalGap;
}
}
}
while (!done);
}
}
/**
* @private
*/
override public function styleChanged(styleProp:String):void
{
super.styleChanged(styleProp);
var allStyles:Boolean = styleProp == null || styleProp == "styleName";
if (allStyles || styleProp == "labelStyleName")
{
if (labelObj)
{
var labelStyleName:String = getStyle("labelStyleName");
if (labelStyleName)
{
var styleDecl:CSSStyleDeclaration =
styleManager.getMergedStyleDeclaration("." + labelStyleName);
if (styleDecl)
{
labelObj.styleDeclaration = styleDecl;
labelObj.regenerateStyleCache(true);
}
}
}
}
}
//--------------------------------------------------------------------------
//
// Methods
//
//--------------------------------------------------------------------------
/**
* @private
*/
internal function getPreferredLabelWidth():Number
{
if (!label || label == "")
return 0;
if (isNaN(labelObj.measuredWidth))
labelObj.validateSize();
var labelWidth:Number = labelObj.measuredWidth;
if (isNaN(labelWidth))
return 0;
return labelWidth;
}
/**
* @private
*/
private function calculateLabelWidth():Number
{
var labelWidth:Number = getStyle("labelWidth");
// labelWidth of 0 is the same as NaN
if (labelWidth == 0)
labelWidth = NaN;
if (isNaN(labelWidth) && parent is Form)
labelWidth = Form(parent).calculateLabelWidth();
if (isNaN(labelWidth))
labelWidth = getPreferredLabelWidth();
return labelWidth;
}
/**
* @private
*/
private function calcNumColumns(w:Number):int
{
var totalWidth:Number = 0;
var maxChildWidth:Number = 0;
var horizontalGap:Number = getStyle("horizontalGap");
if (direction != FormItemDirection.HORIZONTAL)
return 1;
var numChildrenWithOwnSpace:Number = numChildren;
for (var i:int = 0; i < numChildren; i++)
{
var child:IUIComponent = getLayoutChildAt(i);
if (!child.includeInLayout)
{
numChildrenWithOwnSpace--;
continue;
}
var childWidth:Number = child.getExplicitOrMeasuredWidth();
maxChildWidth = Math.max(maxChildWidth, childWidth);
totalWidth += childWidth;
if (i > 0)
totalWidth += horizontalGap;
}
// See if everything can fit in a single row
if (isNaN(w) || totalWidth <= w)
return numChildrenWithOwnSpace;
// if the width is enough to contain two children use two columns
if (maxChildWidth*2 <= w)
return 2;
// Default is single column
return 1;
}
/**
* @private
*/
private function displayIndicator(xPos:Number, yPos:Number):void
{
if (required)
{
if (!indicatorObj)
{
var indicatorClass:Class = getStyle("indicatorSkin") as Class;
indicatorObj = IFlexDisplayObject(new indicatorClass());
rawChildren.addChild(DisplayObject(indicatorObj));
}
indicatorObj.x =
xPos + ((getStyle("indicatorGap") - indicatorObj.width) / 2);
if (labelObj)
{
indicatorObj.y = yPos +
(labelObj.getExplicitOrMeasuredHeight() -
indicatorObj.measuredHeight) / 2;
}
}
else
{
if (indicatorObj)
{
rawChildren.removeChild(DisplayObject(indicatorObj));
indicatorObj = null;
}
}
}
/**
* @private
* Returns a numeric value for the align setting.
* 0 = left/top, 0.5 = center, 1 = right/bottom
*/
mx_internal function getHorizontalAlignValue():Number
{
var horizontalAlign:String = getStyle("horizontalAlign");
if (horizontalAlign == "center")
return 0.5;
else if (horizontalAlign == "right")
return 1;
// default = left
return 0;
}
}
}