blob: 0f9b664b11b69bc76e33b55f840970995867fc2a [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.controls
{
import flash.display.DisplayObject;
import flash.display.DisplayObjectContainer;
import flash.events.Event;
import flash.events.FocusEvent;
import flash.events.KeyboardEvent;
import flash.events.MouseEvent;
import flash.geom.Point;
import flash.geom.Rectangle;
import flash.ui.Keyboard;
import flash.utils.clearInterval;
import flash.utils.setTimeout;
import flash.xml.XMLNode;
import mx.collections.ArrayCollection;
import mx.collections.CursorBookmark;
import mx.collections.ICollectionView;
import mx.collections.XMLListCollection;
import mx.controls.listClasses.BaseListData;
import mx.controls.listClasses.IListItemRenderer;
import mx.controls.menuClasses.IMenuBarItemRenderer;
import mx.controls.menuClasses.IMenuDataDescriptor;
import mx.controls.menuClasses.IMenuItemRenderer;
import mx.controls.menuClasses.MenuItemRenderer;
import mx.controls.menuClasses.MenuListData;
import mx.controls.treeClasses.DefaultDataDescriptor;
import mx.core.ClassFactory;
import mx.core.EdgeMetrics;
import mx.core.EventPriority;
import mx.core.FlexGlobals;
import mx.core.IFlexDisplayObject;
import mx.core.LayoutDirection;
import mx.core.ScrollPolicy;
import mx.core.UIComponent;
import mx.core.UIComponentGlobals;
import mx.core.mx_internal;
import mx.effects.Tween;
import mx.events.CollectionEvent;
import mx.events.CollectionEventKind;
import mx.events.FlexEvent;
import mx.events.FlexMouseEvent;
import mx.events.InterManagerRequest;
import mx.events.ListEvent;
import mx.events.MenuEvent;
import mx.events.SandboxMouseEvent;
import mx.managers.IActiveWindowManager;
import mx.managers.IFocusManagerContainer;
import mx.managers.ISystemManager;
import mx.managers.PopUpManager;
use namespace mx_internal;
//--------------------------------------
// Events
//--------------------------------------
/**
* Dispatched when selection changes as a result
* of user interaction.
*
* @eventType mx.events.MenuEvent.CHANGE
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
[Event(name="change", type="mx.events.MenuEvent")]
/**
* Dispatched when a menu item is selected.
*
* @eventType mx.events.MenuEvent.ITEM_CLICK
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
[Event(name="itemClick", type="mx.events.MenuEvent")]
/**
* Dispatched when a menu or submenu is dismissed.
*
* @eventType mx.events.MenuEvent.MENU_HIDE
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
[Event(name="menuHide", type="mx.events.MenuEvent")]
/**
* Dispatched when a menu or submenu opens.
*
* @eventType mx.events.MenuEvent.MENU_SHOW
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
[Event(name="menuShow", type="mx.events.MenuEvent")]
/**
* Dispatched when a user rolls the mouse out of a menu item.
*
* @eventType mx.events.MenuEvent.ITEM_ROLL_OUT
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
[Event(name="itemRollOut", type="mx.events.MenuEvent")]
/**
* Dispatched when a user rolls the mouse over a menu item.
*
* @eventType mx.events.MenuEvent.ITEM_ROLL_OVER
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
[Event(name="itemRollOver", type="mx.events.MenuEvent")]
//--------------------------------------
// Styles
//--------------------------------------
/**
* The colors used for menu or submenu menu items in an alternating pattern.
* The value can be an Array of one or more colors.
* This style is only used if <code>backgroundColor</code> is not specified.
*
* @default "undefined"
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
[Style(name="alternatingItemColors", type="Array", arrayType="uint", format="Color", inherit="yes")]
/**
* Number of pixels between children (icons and label) in the horizontal direction.
*
* @default 6
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
[Style(name="horizontalGap", type="Number", format="Length", inherit="no")]
/**
* The gap to the left of the label in a menu item. If the icons (custom
* icon and type icon) do not fit in this gap, the gap is expanded to
* fit them properly.
* The default value is 18.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
[Style(name="leftIconGap", type="Number", format="Length", inherit="no")]
/**
* The gap to the right of the label in a menu item. If the branch icon
* does not fit in this gap, the gap is expanded to fit it properly.
* The default value is 15.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
[Style(name="rightIconGap", type="Number", format="Length", inherit="no")]
/**
* The duration of the menu or submenu opening transition, in milliseconds.
* The value 0 specifies no transition.
*
* The default value for the Halo theme is 250.
* The default value for the Spark theme is 0.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
[Style(name="openDuration", type="Number", format="Time", inherit="no")]
/**
* The color of the menu item background when a user rolls the mouse over it.
*
* The default value for the Halo theme is <code>0xB2E1FF</code>.
* The default value for the Spark theme is <code>0xCEDBEF</code>.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
[Style(name="rollOverColor", type="uint", format="Color", inherit="yes")]
/**
* The color of the menu item background when a menu item is selected.
*
* The default value for the Halo theme is <code>0x7FCEFF</code>.
* The default value for the Spark theme is <code>0xA8C6EE</code>.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
[Style(name="selectionColor", type="uint", format="Color", inherit="yes")]
/**
* The reference to an <code>easingFunction</code> equation which is used to
* control programmatic tweening.
*
* @default "undefined"
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
[Style(name="selectionEasingFunction", type="Function", inherit="no")]
/**
* The offset of the first line of text from the left side of the menu or
* submenu menu item.
*
* @default 0
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
[Style(name="textIndent", type="Number", format="Length", inherit="yes")]
/**
* The color of the menu item text when a user rolls the mouse over the
* menu item.
*
* The default value for the Halo theme is <code>0x2B333C</code>.
* The default value for the Spark theme is <code>0x000000</code>.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
[Style(name="textRollOverColor", type="uint", format="Color", inherit="yes")]
/**
* The color of the menu item text when the menu item is selected.
*
* The default value for the Halo theme is <code>0x2B333C</code>.
* The default value for the Spark theme is <code>0x000000</code>.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
[Style(name="textSelectedColor", type="uint", format="Color", inherit="yes")]
/**
* The icon for all enabled menu items that have submenus.
*
* The default value for the Halo theme is the "MenuBranchEnabled" symbol
* in the Assets.swf file.
* The default value for the Spark theme is <code>mx.skins.spark.MenuArrow</code>.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
[Style(name="branchIcon", type="Class", inherit="no")]
/**
* The icon for all disabled menu items that have submenus.
*
* The default value for the Halo theme is the "MenuBranchDisabled" symbol
* in the Assets.swf file.
* The default value for the Spark theme is <code>mx.skins.spark.MenuArrowDisabled</code>.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
[Style(name="branchDisabledIcon", type="Class", inherit="no")]
/**
* The icon for all enabled menu items whose type identifier is a check box.
*
* The default value for the Halo theme is the "MenuCheckEnabled" symbol
* in the Assets.swf file.
* The default value for the Spark theme is <code>mx.skins.spark.MenuCheck</code>.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
[Style(name="checkIcon", type="Class", inherit="no")]
/**
* The icon for all dsiabled menu items whose type identifier is a check box.
*
* The default value for the Halo theme is the "MenuCheckDisabled" symbol
* in the Assets.swf file.
* The default value for the Spark theme is <code>mx.skins.spark.MenuCheckDisabled</code>.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
[Style(name="checkDisabledIcon", type="Class", inherit="no")]
/**
* The icon for all enabled menu items whose type identifier is a radio
* button.
*
* The default value for the Halo theme is the "MenuRadioEnabled" symbol
* in the Assets.swf file.
* The default value for the Spark theme is <code>mx.skins.spark.MenuRadio</code>.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
[Style(name="radioIcon", type="Class", inherit="no")]
/**
* The icon for all disabled menu items whose type identifier is a radio
* button.
*
* The default value for the Halo theme is the "MenuRadioDisabled" symbol
* in the Assets.swf file.
* The default value for the Spark theme is <code>mx.skins.spark.MenuRadioDisabled</code>.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
[Style(name="radioDisabledIcon", type="Class", inherit="no")]
/**
* The skin for all menu items which are identified as separators.
*
* The default value for the Halo theme is the "MenuSeparator" symbol
* in the Assets.swf file.
* The default value for the Spark theme is <code>mx.skins.spark.MenuSeparatorSkin</code>.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
[Style(name="separatorSkin", type="Class", inherit="no")]
//--------------------------------------
// Excluded APIs
//--------------------------------------
[Exclude(name="allowMultipleSelection", kind="property")]
[Exclude(name="horizontalScrollBarStyleName", kind="property")]
[Exclude(name="horizontalScrollPolicy", kind="property")]
[Exclude(name="horizontalScrollPosition", kind="property")]
[Exclude(name="liveScrolling", kind="property")]
[Exclude(name="maxHorizontalScrollPosition", kind="property")]
[Exclude(name="maxVerticalScrollPosition", kind="property")]
[Exclude(name="scrollTipFunction", kind="property")]
[Exclude(name="showScrollTips", kind="property")]
[Exclude(name="verticalScrollBarStyleName", kind="property")]
[Exclude(name="verticalScrollPolicy", kind="property")]
[Exclude(name="verticalScrollPosition", kind="property")]
//--------------------------------------
// Other metadata
//--------------------------------------
[AccessibilityClass(implementation="mx.accessibility.MenuAccImpl")]
[DefaultBindingProperty(destination="dataProvider")]
[DefaultTriggerEvent("change")]
[IconFile("Menu.png")]
[RequiresDataBinding(true)]
/**
* The Menu control creates a pop-up menu of individually selectable choices,
* similar to the File or Edit menu found in most software applications. The
* popped up menu can have as many levels of submenus as needed.
* After a Menu control has opened, it remains visible until it is closed by
* any of the following actions:
*
* <ul>
* <li>A call to the <code>Menu.hide()</code> method.</li>
* <li>When a user selects an enabled menu item.</li>
* <li>When a user clicks outside of the Menu control.</li>
* <li>When a user selects another component in the application.</li>
* </ul>
*
* <p>The Menu class has no corresponding MXML tag. You must create it using ActionScript.</p>
*
* <p>The Menu control has the following sizing characteristics:</p>
* <table class="innertable">
* <tr>
* <th>Characteristic</th>
* <th>Description</th>
* </tr>
* <tr>
* <td>Default size</td>
* <td>The width is determined from the Menu text. The
* default height is the number of menu rows multiplied
* by 19 pixels per row (the default row height).</td>
* </tr>
* </table>
*
* <p>The data provider for Menu items can specify several attributes that determine how
* the item is displayed and behaves, as the following XML data provider shows:</p>
* <pre>
* &lt;mx:XML format=&quot;e4x&quot; id=&quot;myMenuData&quot;&gt;
* &lt;root&gt;
* &lt;menuitem label=&quot;MenuItem A&quot; icon=&quot;myTopIcon&quot;&gt;
* &lt;menuitem label=&quot;SubMenuItem A-1&quot; enabled=&quot;False&quot;/&gt;
* &lt;menuitem label=&quot;SubMenuItem A-2&quot;/&gt;
* &lt;/menuitem&gt;
* &lt;menuitem label=&quot;MenuItem B&quot; type=&quot;check&quot; toggled=&quot;true&quot;/&gt;
* &lt;menuitem label=&quot;MenuItem C&quot; type=&quot;check&quot; toggled=&quot;false&quot; icon=&quot;myTopIcon&quot;/&gt;
* &lt;menuitem type=&quot;separator&quot;/&gt;
* &lt;menuitem label=&quot;MenuItem D&quot; icon=&quot;myTopIcon&quot;&gt;
* &lt;menuitem label=&quot;SubMenuItem D-1&quot; type=&quot;radio&quot; groupName=&quot;one&quot;/&gt;
* &lt;menuitem label=&quot;SubMenuItem D-2&quot; type=&quot;radio&quot; groupName=&quot;one&quot; toggled=&quot;true&quot;/&gt;
* &lt;menuitem label=&quot;SubMenuItem D-3&quot; type=&quot;radio&quot; groupName=&quot;one&quot;/&gt;
* &lt;/menuitem&gt;
* &lt;/root&gt;
* &lt;/mx:XML&gt;</pre>
*
* <p>The following table lists the attributes you can specify,
* their data types, their purposes, and how the data provider must represent
* them if the menu uses the DefaultDataDescriptor class to parse the data provider:</p>
*
* <table class="innertable">
* <tr>
* <th>Attribute</th>
* <th>Type</th>
* <th>Description</th>
* </tr>
* <tr>
* <td><code>enabled</code></td>
* <td>Boolean</td>
* <td>Specifies whether the user can select the menu item (<code>true</code>),
* or not (<code>false</code>). If not specified, Flex treats the item as if
* the value were <code>true</code>.
* If you use the default data descriptor, data providers must use an <code>enabled</code>
* XML attribute or object field to specify this characteristic.</td>
* </tr>
* <tr>
* <td><code>groupName</code></td>
* <td>String</td>
* <td> (Required, and meaningful, for <code>radio</code> type only) The identifier that
* associates radio button items in a radio group. If you use the default data descriptor,
* data providers must use a <code>groupName</code> XML attribute or object field to
* specify this characteristic.</td>
* </tr>
* <tr>
* <td><code>icon</code></td>
* <td>Class</td>
* <td>Specifies the class identifier of an image asset. This item is not used for
* the <code>check</code>, <code>radio</code>, or <code>separator</code> types.
* You can use the <code>checkIcon</code> and <code>radioIcon</code> styles to
* specify the icons used for radio and check box items that are selected.
* The menu's <code>iconField</code> or <code>iconFunction</code> property determines
* the name of the field in the data that specifies the icon, or a function for determining the icons.</td>
* </tr>
* <tr>
* <td><code>label</code></td>
* <td>String</td>
* <td>Specifies the text that appears in the control. This item is used for all
* menu item types except <code>separator</code>.
* The menu's <code>labelField</code> or <code>labelFunction</code> property
* determines the name of the field in the data that specifies the label,
* or a function for determining the labels. (If the data provider is in E4X XML format,
* you must specify one of these properties to display a label.)
* If the data provider is an Array of Strings, Flex uses the String value as the label.</td>
* </tr>
* <tr>
* <td><code>toggled</code></td>
* <td>Boolean</td>
* <td>Specifies whether a check or radio item is selected.
* If not specified, Flex treats the item as if the value were <code>false</code>
* and the item is not selected.
* If you use the default data descriptor, data providers must use a toggled
* XML attribute or object field to specify this characteristic.</td>
* </tr>
* <tr>
* <td><code>type</code></td>
* <td>String</td>
* <td>Specifies the type of menu item. Meaningful values are <code>separator</code>,
* <code>check</code>, or <code>radio</code>. Flex treats all other values,
* or nodes with no type entry, as normal menu entries.
* If you use the default data descriptor, data providers must use a type
* XML attribute or object field to specify this characteristic.</td>
* </tr>
* </table>
*
*
* @includeExample examples/SimpleMenuExample.mxml
*
* @see mx.controls.MenuBar
* @see mx.controls.PopUpMenuButton
* @see mx.controls.menuClasses.IMenuDataDescriptor
* @see mx.controls.treeClasses.DefaultDataDescriptor
* @see mx.effects.Tween
* @see mx.managers.PopUpManager
*
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public class Menu extends List implements IFocusManagerContainer
{
include "../core/Version.as";
//--------------------------------------------------------------------------
//
// Class mixins
//
//--------------------------------------------------------------------------
/**
* @private
* Placeholder for mixin by MenuAccImpl.
*/
mx_internal static var createAccessibilityImplementation:Function;
//--------------------------------------------------------------------------
//
// Class methods
//
//--------------------------------------------------------------------------
/**
* Creates and returns an instance of the Menu class. The Menu control's
* content is determined by the method's <code>mdp</code> argument. The
* Menu control is placed in the parent container specified by the
* method's <code>parent</code> argument.
*
* This method does not show the Menu control. Instead,
* this method just creates the Menu control and allows for modifications
* to be made to the Menu instance before the Menu is shown. To show the
* Menu, call the <code>Menu.show()</code> method.
*
* @param parent A container that the PopUpManager uses to place the Menu
* control in. The Menu control may not actually be parented by this object.
*
* @param mdp The data provider for the Menu control.
* @see mx.controls.Menu@dataProvider
*
* @param showRoot A Boolean flag that specifies whether to display the
* root node of the data provider.
* @see mx.controls.Menu@showRoot
*
* @return An instance of the Menu class.
*
* @see mx.controls.Menu#popUpMenu()
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public static function createMenu(parent:DisplayObjectContainer, mdp:Object, showRoot:Boolean = true):Menu
{
var menu:Menu = new Menu();
menu.tabEnabled = false;
menu.tabFocusEnabled = false;
menu.owner = DisplayObjectContainer(FlexGlobals.topLevelApplication);
menu.showRoot = showRoot;
popUpMenu(menu, parent, mdp);
return menu;
}
/**
* Sets the dataProvider of an existing Menu control and places the Menu
* control in the specified parent container.
*
* This method does not show the Menu control; you must use the
* <code>Menu.show()</code> method to display the Menu control.
*
* The <code>Menu.createMenu()</code> method uses this method.
*
* @param menu Menu control to popup.
*
* @param parent A container that the PopUpManager uses to place the Menu
* control in. The Menu control may not actually be parented by this object.
* If you omit this property, the method sets the Menu control's parent to
* the application.
*
* @param mdp dataProvider object set on the popped up Menu. If you omit this
* property, the method sets the Menu data provider to a new, empty XML object.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public static function popUpMenu(menu:Menu, parent:DisplayObjectContainer, mdp:Object):void
{
menu.parentDisplayObject = parent ?
parent :
DisplayObject(FlexGlobals.topLevelApplication);
if (!mdp)
mdp = new XML();
menu.supposedToLoseFocus = true;
// menu.isPressed = true;
menu.dataProvider = mdp;
}
//--------------------------------------------------------------------------
//
// Constructor
//
//--------------------------------------------------------------------------
/**
* Constructor.
*
* <p>Applications do not normally call the Menu constructor directly.
* Instead, Applications will call the <code>Menu.createMenu()</code>
* method.</p>
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function Menu()
{
super();
itemRenderer = new ClassFactory(MenuItemRenderer);
setRowHeight(19);
iconField = "icon";
visible = false;
}
//--------------------------------------------------------------------------
//
// Variables
//
//--------------------------------------------------------------------------
/**
* @private
* internal measuring stick
*/
private var hiddenItem:IListItemRenderer;
/**
* @private
* The maximum width for icons. Used so they
* can align properly across all MenuItemRenderers
*/
private var maxMeasuredIconWidth:Number = 0;
/**
* @private
* The maximum width for type icons (checkbox, radiobox).
* Used so they can align properly across all MenuItemRenderers
*/
private var maxMeasuredTypeIconWidth:Number = 0;
/**
* @private
* The maximum width for branch icons. Used so they
* can align properly across all MenuItemRenderers
*/
private var maxMeasuredBranchIconWidth:Number = 0;
/**
* @private
* Whether the left icons should layout into two separate columns
* (one for icons and one for type icons, like check and radio)
*/
private var useTwoColumns:Boolean = false;
/**
* @private
* The menu bar that eventually spawned this menu
*/
mx_internal var sourceMenuBar:MenuBar; // optional
/**
* @private
* the IMenuBarItemRenderer instance in the menubar
* that spawned this menu
*/
mx_internal var sourceMenuBarItem:IMenuBarItemRenderer; // optional
/**
* @private
* Where to add this menu on the display list.
*/
mx_internal var parentDisplayObject:DisplayObject;
/**
* @private
* Whether the menu was opened from the left or the right.
* This really only applies to submenus and helps with cascading submenus.
*/
private var isDirectionLeft:Boolean = false;
// the anchor is the row in the parent menu that opened to be this menu
// or the row in this menu that opened to be a submenu
private var anchorRow:IListItemRenderer;
//private var _anchor:String; // reference to the ID of the last opened submenu within a menu level
//private var _anchorIndex:int; // reference to the rowIndex of a menu's anchor in the parent menu
private var subMenu:Menu;
mx_internal var popupTween:Tween;
mx_internal var supposedToLoseFocus:Boolean = false;
/**
* @private
* When this timer fires, we'll open a submenu
*/
mx_internal var openSubMenuTimer:int = 0;
/**
* @private
* When this timer fires, we'll hide this menu
*/
mx_internal var closeTimer:int = 0;
/**
* @private
* Storage variable for the original dataProvider
*/
mx_internal var _rootModel:ICollectionView;
/**
* @private
* Storage variable for dataProvider passed to parent
*/
mx_internal var _listDataProvider:ICollectionView;
//--------------------------------------------------------------------------
//
// Properties
//
//--------------------------------------------------------------------------
//--------------------------------------------------------------------------
// parentMenu
//--------------------------------------------------------------------------
/**
* @private
*/
mx_internal var _parentMenu:Menu;
/**
* The parent menu in a hierarchical chain of menus, where the current
* menu is a submenu of the parent.
*
* @return The parent Menu control.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function get parentMenu():Menu
{
return _parentMenu;
}
/**
* @private
*/
public function set parentMenu(value:Menu):void
{
_parentMenu = value;
// setup weak references
value.addEventListener(FlexEvent.HIDE, parentHideHandler, false, 0, true);
value.addEventListener("rowHeightChanged", parentRowHeightHandler, false, 0, true);
value.addEventListener("iconFieldChanged", parentIconFieldHandler, false, 0, true);
value.addEventListener("iconFunctionChanged", parentIconFunctionHandler, false, 0, true);
value.addEventListener("labelFieldChanged", parentLabelFieldHandler, false, 0, true);
value.addEventListener("labelFunctionChanged", parentLabelFunctionHandler, false, 0, true);
value.addEventListener("itemRendererChanged", parentItemRendererHandler, false, 0, true);
}
//--------------------------------------------------------------------------
// dataDescriptor
//--------------------------------------------------------------------------
/**
* @private
*/
mx_internal var _dataDescriptor:IMenuDataDescriptor =
new DefaultDataDescriptor();
/**
* The object that accesses and manipulates data in the data provider.
* The Menu control delegates to the data descriptor for information
* about its data. This data is then used to parse and move about the
* data source. The data descriptor defined for the root menu is used
* for all submenus.
*
* The default value is an internal instance of the
* DefaultDataDescriptor class.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function get dataDescriptor():IMenuDataDescriptor
{
return IMenuDataDescriptor(_dataDescriptor);
}
/**
* @private
*/
public function set dataDescriptor(value:IMenuDataDescriptor):void
{
_dataDescriptor = value;
}
//--------------------------------------------------------------------------
// dataProvider
//--------------------------------------------------------------------------
/**
* @private
*/
mx_internal var dataProviderChanged:Boolean = false;
/**
* @private
* Convert user data to collection.
*
* @see mx.controls.listClasses.ListBase
* @see mx.controls.List
* @see mx.controls.Tree
*/
override public function set dataProvider(value:Object):void
{
// handle strings and xml
if (typeof(value)=="string")
value = new XML(value);
else if (value is XMLNode)
value = new XML(XMLNode(value).toString());
else if (value is XMLList)
value = new XMLListCollection(value as XMLList);
if (value is XML)
{
_hasRoot = true;
var xl:XMLList = new XMLList();
xl += value;
_rootModel = new XMLListCollection(xl);
}
//if already a collection dont make new one
else if (value is ICollectionView)
{
_rootModel = ICollectionView(value);
if (_rootModel.length == 1)
_hasRoot = true;
}
else if (value is Array)
{
_rootModel = new ArrayCollection(value as Array);
}
//all other types get wrapped in an ArrayCollection
else if (value is Object)
{
_hasRoot = true;
// convert to an array containing this one item
var tmp:Array = [];
tmp.push(value);
_rootModel = new ArrayCollection(tmp);
}
else
{
_rootModel = new ArrayCollection();
}
//flag for processing in commitProps
dataProviderChanged = true;
invalidateProperties();
}
/**
* @private
*/
override public function get dataProvider():Object
{
var model:* = super.dataProvider;
if (model == null)
{
if (_rootModel != null )
{
return _rootModel;
}
}
else
{
return model;
}
return null;
}
//--------------------------------------------------------------------------
// showRoot
//--------------------------------------------------------------------------
/**
* @private
* Storage variable for showRoot flag.
*/
mx_internal var _showRoot:Boolean = true;
/**
* @private
* Storage variable for changes to showRoot.
*/
mx_internal var showRootChanged:Boolean = false;
/**
* A Boolean flag that specifies whether to display the data provider's
* root node.
*
* If the dataProvider object has a root node, and showRoot is set to
* <code>false</code>, the Menu control does not display the root node;
* only the descendants of the root node will be displayed.
*
* This flag has no effect on data providers without root nodes,
* like Lists and Arrays.
*
* @default true
* @see #hasRoot
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function get showRoot():Boolean
{
return _showRoot;
}
/**
* @private
*/
public function set showRoot(value:Boolean):void
{
if (_showRoot != value)
{
_showRoot = value;
showRootChanged = true;
invalidateProperties();
}
}
//--------------------------------------------------------------------------
// hasRoot
//--------------------------------------------------------------------------
/**
* @private
* Flag to indicate if the model has a root
*/
mx_internal var _hasRoot:Boolean = false;
/**
* A flag that indicates that the current data provider has a root node; for example,
* a single top node in a hierarchical structure. XML and Object
* are examples of types that have a root node, while Lists and Arrays do
* not.
*
* @default false
* @see #showRoot
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function get hasRoot():Boolean
{
return _hasRoot;
}
//--------------------------------------------------------------------------
// defaultButton
//--------------------------------------------------------------------------
/**
* @private
*/
public function get defaultButton():IFlexDisplayObject
{
return null;
}
/**
* @private
*/
public function set defaultButton(value:IFlexDisplayObject):void
{
}
//--------------------------------------------------------------------------
//
// Event handlers
//
//--------------------------------------------------------------------------
/**
* @private
*/
private function parentHideHandler(event:FlexEvent):void
{
visible = false;
}
/**
* @private
*/
private function parentRowHeightHandler(event:Event):void
{
rowHeight = parentMenu.rowHeight;
}
/**
* @private
*/
private function parentIconFieldHandler(event:Event):void
{
iconField = parentMenu.iconField;
}
/**
* @private
*/
private function parentIconFunctionHandler(event:Event):void
{
iconFunction = parentMenu.iconFunction;
}
/**
* @private
*/
private function parentLabelFieldHandler(event:Event):void
{
labelField = parentMenu.labelField;
}
/**
* @private
*/
private function parentLabelFunctionHandler(event:Event):void
{
labelFunction = parentMenu.labelFunction;
}
/**
* @private
*/
private function parentItemRendererHandler(event:Event):void
{
itemRenderer = parentMenu.itemRenderer;
}
// -------------------------------------------------------------------------
// horizontalScrollPolicy
// -------------------------------------------------------------------------
[Inspectable(environment="none")]
/**
* @private
*/
override public function get horizontalScrollPolicy():String
{
return ScrollPolicy.OFF;
}
/**
* @private
*/
override public function set horizontalScrollPolicy(value:String):void
{
}
// -------------------------------------------------------------------------
// verticalScrollPolicy
// -------------------------------------------------------------------------
[Inspectable(environment="none")]
/**
* @private
*/
override public function get verticalScrollPolicy():String
{
return ScrollPolicy.OFF;
}
/**
* @private
*/
override public function set verticalScrollPolicy(value:String):void
{
}
//--------------------------------------------------------------------------
//
// Overridden methods
//
//--------------------------------------------------------------------------
/**
* @private
* Called by the initialize() method of UIComponent
* to hook in the accessibility code.
*/
override protected function initializeAccessibility():void
{
if (createAccessibilityImplementation != null)
createAccessibilityImplementation(this);
}
/**
* @private
*/
override protected function commitProperties():void
{
if (showRootChanged)
{
if (!_hasRoot)
showRootChanged = false;
}
if (dataProviderChanged || showRootChanged)
{
var tmpCollection:ICollectionView;
// Reset flags.
dataProviderChanged = false;
showRootChanged = false;
// Are we swallowing the root?
if (_rootModel && !_showRoot && _hasRoot)
{
var rootItem:* = _rootModel.createCursor().current;
if (rootItem != null &&
_dataDescriptor.isBranch(rootItem, _rootModel) &&
_dataDescriptor.hasChildren(rootItem, _rootModel))
{
// then get rootItem children
tmpCollection = _dataDescriptor.getChildren(rootItem, _rootModel);
}
}
if (_listDataProvider)
{
_listDataProvider.removeEventListener(CollectionEvent.COLLECTION_CHANGE,
collectionChangeHandler, false);
}
// At this point _rootModel may be null so we dont need to continue.
if (_rootModel)
{
if (!tmpCollection)
tmpCollection = _rootModel;
_listDataProvider = tmpCollection;
super.dataProvider = tmpCollection;
// not really a default handler, but we need to be later than the wrapper
_listDataProvider.addEventListener(CollectionEvent.COLLECTION_CHANGE,
collectionChangeHandler,
false,
EventPriority.DEFAULT_HANDLER, true);
}
else
{
_listDataProvider = null;
super.dataProvider = null;
}
}
// Send it up the chain.
super.commitProperties();
}
/**
* Calculates the preferred width and height of the Menu based on the
* widths and heights of its menu items. This method does not take into
* account the position and size of submenus.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
override protected function measure():void
{
super.measure();
if (!dataProvider || dataProvider.length == 0)
{
// we show a collapsed menu if nothing in it so we can dispatch
// a menuShow and give someone a chance to add things
measuredWidth = 0;
measuredHeight = 0;
}
else
{
var vm:EdgeMetrics = viewMetrics;
measuredMinWidth = measuredWidth = measureWidthOfItems(0, dataProvider.length);
var requiredHeight:int;
if (variableRowHeight)
requiredHeight = measureHeightOfItems(0, dataProvider.length);
else
requiredHeight = dataProvider.length * rowHeight;
measuredMinHeight = measuredHeight =
requiredHeight + vm.top + vm.bottom;
}
}
/**
* @private
*/
override public function measureWidthOfItems(index:int = -1, count:int = 0):Number
{
var w:Number = 0;
// reset max's so we can shrink if we should
var minimumLeftMargin:Number = getStyle("leftIconGap");
var minimumRightMargin:Number = getStyle("rightIconGap");
maxMeasuredIconWidth = 0;
maxMeasuredTypeIconWidth = 0;
maxMeasuredBranchIconWidth = 0;
useTwoColumns = false;
if (collection && collection.length)
{
var placeHolder:CursorBookmark = iterator.bookmark;
var previousCount:int = count;
// we loop through up to 2 times because the first time, we may
// need to do a re-draw due to the margins changing from a
// larger icon width
for (var i:int = 0; i < 2; i++)
{
iterator.seek(CursorBookmark.FIRST, index);
count = previousCount;
var valuesChanged:Boolean = false;
while (count)
{
var data:Object = iterator.current;
var menuListData:MenuListData;
var item:IListItemRenderer = hiddenItem = getMeasuringRenderer(data);
item.explicitWidth = NaN;
setupRendererFromData(item, data);
w = Math.max(item.getExplicitOrMeasuredWidth(), w);
if (item is IMenuItemRenderer)
{
var menuItemRenderer:IMenuItemRenderer = IMenuItemRenderer(item);
if (menuItemRenderer.measuredIconWidth > maxMeasuredIconWidth)
{
maxMeasuredIconWidth = menuItemRenderer.measuredIconWidth;
valuesChanged = true;
}
if (menuItemRenderer.measuredTypeIconWidth > maxMeasuredTypeIconWidth)
{
maxMeasuredTypeIconWidth = menuItemRenderer.measuredTypeIconWidth;
valuesChanged = true;
}
if (menuItemRenderer.measuredBranchIconWidth > maxMeasuredBranchIconWidth)
{
maxMeasuredBranchIconWidth = menuItemRenderer.measuredBranchIconWidth;
valuesChanged = true;
}
// use two columns if the same MenuItemRenderer can have both a type icon and a
// regular icon
if (menuItemRenderer.measuredIconWidth > 0 && menuItemRenderer.measuredTypeIconWidth)
{
useTwoColumns = true;
valuesChanged = true;
}
}
count--;
if (!iterator.moveNext())
break;
}
if (i == 0)
{
if (!(valuesChanged &&
(maxMeasuredIconWidth + maxMeasuredTypeIconWidth > minimumLeftMargin ||
maxMeasuredBranchIconWidth > minimumRightMargin)))
{
break;
}
}
}
iterator.seek(placeHolder, 0);
}
if (!w)
{
// If we couldn't calculate a width, use 200
w = 200;
}
w += getStyle("paddingLeft") + getStyle("paddingRight");
return w;
}
/**
* @private
*/
override protected function updateDisplayList(unscaledWidth:Number,
unscaledHeight:Number):void
{
super.updateDisplayList(unscaledWidth, unscaledHeight);
if (border)
{
border.move(0, 0);
border.visible = (dataProvider != null && dataProvider.length > 0);
}
// there's sort of a bug in the player that if we don't give size to the
// hiddenItem it gets set to width = 0 and then you can't measure text anymore.
if (hiddenItem)
hiddenItem.setActualSize(unscaledWidth, hiddenItem.getExplicitOrMeasuredHeight());
}
/**
* @private
* Relying on UIComponent description.
*/
override public function styleChanged(styleProp:String):void
{
super.styleChanged(styleProp);
deleteDependentSubMenus();
}
/**
* @private
*/
override protected function drawItem(item:IListItemRenderer,
selected:Boolean = false,
highlighted:Boolean = false,
caret:Boolean = false,
transition:Boolean = false):void
{
if (!getStyle("useRollOver"))
{
super.drawItem(item, selected, false, false, transition);
}
else
{
super.drawItem(item, selected, highlighted, caret, transition);
}
}
/**
* @private
*/
override protected function configureScrollBars():void
{
}
override mx_internal function clearHighlight(item:IListItemRenderer):void
{
var uid:String = itemToUID(item.data);
drawItem(visibleData[uid], isItemSelected(item.data),
false, uid == caretUID);
var pt:Point = itemRendererToIndices(item);
if (pt)
{
var menuEvent:MenuEvent = new MenuEvent(MenuEvent.ITEM_ROLL_OUT);
menuEvent.menu = this;
menuEvent.index = getRowIndex(item);
menuEvent.menuBar = sourceMenuBar;
menuEvent.label = itemToLabel(item.data);
menuEvent.item = item.data;
menuEvent.itemRenderer = item;
getRootMenu().dispatchEvent(menuEvent);
}
}
/**
* @private
* Determines if the itemrenderer is a separator. If so, return null to prevent separators
* from highlighting and emitting menu-level events.
*/
override protected function mouseEventToItemRenderer(event:MouseEvent):IListItemRenderer
{
var row:IListItemRenderer = super.mouseEventToItemRenderer(event);
if (row && row.data && _dataDescriptor.getType(row.data) == "separator")
return null;
else return row;
}
/**
* @private
*/
override public function setFocus():void
{
super.setFocus();
}
/**
* @private
*/
override protected function focusOutHandler(event:FocusEvent):void
{
super.focusOutHandler(event);
if (!supposedToLoseFocus)
{
hideAllMenus();
}
supposedToLoseFocus = false;
}
/**
* @private
*/
override public function dispatchEvent(event:Event):Boolean
{
if (!(event is MenuEvent) && event is ListEvent &&
(event.type == ListEvent.ITEM_ROLL_OUT ||
event.type == ListEvent.ITEM_ROLL_OVER ||
event.type == ListEvent.CHANGE))
{
// we don't want to dispatch ListEvent.ITEM_ROLL_OVER or
// ListEvent.ITEM_ROLL_OUT or ListEvent.CHANGE events
// because Menu dispatches its own
event.stopImmediatePropagation();
}
// in case we encounter a ListEvent.ITEM_CLICK from
// a superclass that we did not account for,
// lets convert the ListEvent and pass it on up to
// avoid an RTE
if (!(event is MenuEvent) && event is ListEvent &&
(event.type == ListEvent.ITEM_CLICK))
{
var me:MenuEvent = new MenuEvent(event.type,
event.bubbles,
event.cancelable);
me.item = ListEvent(event).itemRenderer.data;
me.label = itemToLabel(ListEvent(event).itemRenderer);
return super.dispatchEvent(me);
}
// we'll let everything else go through
return super.dispatchEvent(event);
}
//--------------------------------------------------------------------------
//
// Methods
//
//--------------------------------------------------------------------------
/**
* Toggles the menu item. The menu item type identifier must be a
* check box or radio button, otherwise this method has no effect.
*
* @param item The menu item to toggle.
* @param toggle Boolean value that indicates whether the item is
* toggled.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
protected function setMenuItemToggled(item:Object, toggle:Boolean):void
{
itemsSizeChanged = true;
invalidateDisplayList();
if (_dataDescriptor.getType(item) == "radio")
{
var groupName:String = _dataDescriptor.getGroupName(item);
// Find other items with the same groupName and toggle them to the state opposite the item.
// Toggle the item.
for (var i:int = 0; i < listItems.length; i++)
{
var row:IListItemRenderer = listItems[i][0];
var rowItem:Object = row.data;
if (_dataDescriptor.getType(rowItem) == "radio" &&
_dataDescriptor.getGroupName(rowItem) == groupName)
{
_dataDescriptor.setToggled(rowItem, (rowItem == item));
}
}
}
if (toggle != _dataDescriptor.isToggled(item))
{
_dataDescriptor.setToggled(item, toggle);
}
}
/**
* Creates a new MenuListData instance and populates the fields based on
* the input data provider item.
*
* @param data The data provider item used to populate the ListData.
* @param uid The UID for the item.
* @param rowNum The index of the item in the data provider.
*
* @return A newly constructed ListData object.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
override protected function makeListData(data:Object, uid:String,
rowNum:int):BaseListData
{
var menuListData:MenuListData = new MenuListData(itemToLabel(data), itemToIcon(data),
labelField, uid, this, rowNum);
menuListData.maxMeasuredIconWidth = maxMeasuredIconWidth;
menuListData.maxMeasuredTypeIconWidth = maxMeasuredTypeIconWidth;
menuListData.maxMeasuredBranchIconWidth = maxMeasuredBranchIconWidth;
menuListData.useTwoColumns = useTwoColumns;
return menuListData;
}
/**
* Shows the Menu control. If the Menu control is not visible, this method
* places the Menu in the upper-left corner of the parent application at
* the given coordinates, resizes the Menu control as needed, and makes
* the Menu control visible.
*
* The x and y arguments of the <code>show()</code> method specify the
* coordinates of the upper-left corner of the Menu control relative to the
* sandbox root, which is not necessarily the direct parent of the
* Menu control.
*
* For example, if the Menu control is in an HBox container which is
* nested within a Panel container, the x and y coordinates are
* relative to the Application container, not to the HBox container.
*
* @param x Horizontal location of the Menu control's upper-left
* corner (optional).
*
* @param y Vertical location of the Menu control's upper-left
* corner (optional).
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function show(xShow:Object = null, yShow:Object = null):void
{
//this could be an empty menu so we'll return if it is
if (collection && collection.length == 0)
return;
// If parent is closed, then don't show this submenu
if (parentMenu && !parentMenu.visible)
return;
// If I'm already visible, then do nothing
if (visible)
return;
if (parentDisplayObject && (!parent || !parent.contains(parentDisplayObject)))
{
PopUpManager.addPopUp(this, parentDisplayObject, false);
addEventListener(MenuEvent.MENU_HIDE, menuHideHandler, false, EventPriority.DEFAULT_HANDLER);
}
// Fire an event
var menuEvent:MenuEvent = new MenuEvent(MenuEvent.MENU_SHOW);
menuEvent.menu = this;
menuEvent.menuBar = sourceMenuBar;
getRootMenu().dispatchEvent(menuEvent);
// Activate the focus manager for that menu
var awm:IActiveWindowManager =
IActiveWindowManager(systemManager.getImplementation("mx.managers::IActiveWindowManager"));
awm.activate(this);
// Position it
if (xShow !== null && !isNaN(Number(xShow)))
x = Number(xShow);
if (yShow !== null && !isNaN(Number(yShow)))
y = Number(yShow);
// Adjust for menus that extend out of bounds
var sm:ISystemManager = systemManager.topLevelSystemManager;
var sbRoot:DisplayObject = sm.getSandboxRoot();
if (this != getRootMenu())
{
// do x
var screen:Rectangle;
var pt:Point = new Point(x, y);
pt = sbRoot.localToGlobal(pt);
screen = sm.getVisibleApplicationRect(null, true);
var shift:Number = pt.x + width - screen.right;
if (shift > 0)
x = Math.max(x - shift, 0);
// now do y
shift = pt.y + height - screen.bottom;
if (shift > 0)
y = Math.max(y - shift, 0);
}
// Make sure the Menu's width and height has been determined
// before we try to set the size for its mask
UIComponentGlobals.layoutManager.validateClient(this, true);
setActualSize(getExplicitOrMeasuredWidth(), getExplicitOrMeasuredHeight());
cacheAsBitmap = true;
var duration:Number = getStyle("openDuration");
if (duration != 0)
{
scrollRect = new Rectangle(0, 0, unscaledWidth, 0);
// Make it visible
visible = true;
UIComponentGlobals.layoutManager.validateNow();
// Block all layout, responses from web service, and other background
// processing until the tween finishes executing.
UIComponent.suspendBackgroundProcessing();
popupTween = new Tween(this, [0,0], [unscaledWidth,unscaledHeight], duration);
}
else
{
// if duration is zero, this should allow alternate
// effects specified via showEffect to work
UIComponentGlobals.layoutManager.validateNow();
visible = true;
}
focusManager.setFocus(this);
supposedToLoseFocus = true;
// If the user clicks outside the menu, then hide the menu
sbRoot.addEventListener(MouseEvent.MOUSE_DOWN, mouseDownOutsideHandler, false, 0, true);
addEventListener(SandboxMouseEvent.MOUSE_DOWN_SOMEWHERE, mouseDownOutsideHandler, false, 0, true);
}
/**
* @private
*/
mx_internal function onTweenUpdate(value:Object):void
{
// Slide the mask down until it covers the Menu
scrollRect = new Rectangle(0, 0, value[0], value[1]);
}
/**
* @private
*/
mx_internal function onTweenEnd(value:Object):void
{
UIComponent.resumeBackgroundProcessing();
scrollRect = null;
popupTween = null;
}
/**
* Hides the Menu control and any of its submenus if the Menu control is
* visible.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function hide():void
{
if (visible)
{
// Kill any tween that's currently running
if (popupTween)
popupTween.endTween();
clearSelected();
if (anchorRow)
{
drawItem(anchorRow, false, false);
anchorRow = null;
}
visible = false;
// isPressed = false;
// Now that the menu is no longer visible, it doesn't need
// to listen to mouseDown events anymore.
var sbRoot:DisplayObject = systemManager.getSandboxRoot();
sbRoot.removeEventListener(MouseEvent.MOUSE_DOWN, mouseDownOutsideHandler);
removeEventListener(SandboxMouseEvent.MOUSE_DOWN_SOMEWHERE, mouseDownOutsideHandler);
// Fire an event
var menuEvent:MenuEvent = new MenuEvent(MenuEvent.MENU_HIDE);
menuEvent.menu = this;
menuEvent.menuBar = sourceMenuBar;
getRootMenu().dispatchEvent(menuEvent);
}
}
// -------------------------------------------------------------------------
// Event Handlers
// -------------------------------------------------------------------------
/**
* @private
*/
override protected function collectionChangeHandler(event:Event):void
{
//trace("[Menu] caught model changed:", CollectionEvent(event).kind);
if (event is CollectionEvent)
{
var ce:CollectionEvent = CollectionEvent(event);
if (ce.kind == CollectionEventKind.ADD)
{
super.collectionChangeHandler(event);
dataProviderChanged = true;
invalidateProperties();
invalidateSize();
UIComponentGlobals.layoutManager.validateClient(this);
setActualSize(getExplicitOrMeasuredWidth(), getExplicitOrMeasuredHeight());
}
else if (ce.kind == CollectionEventKind.REMOVE)
{
super.collectionChangeHandler(event);
dataProviderChanged = true;
invalidateProperties();
invalidateSize();
UIComponentGlobals.layoutManager.validateClient(this);
setActualSize(getExplicitOrMeasuredWidth(), getExplicitOrMeasuredHeight());
}
else if (ce.kind == CollectionEventKind.REFRESH)
{
dataProviderChanged = true;
invalidateProperties();
invalidateSize();
}
else if (ce.kind == CollectionEventKind.RESET)
{
dataProviderChanged = true;
invalidateProperties();
invalidateSize();
}
}
itemsSizeChanged = true;
invalidateDisplayList();
}
/**
* @private
*/
private function mouseDownOutsideHandler(event:Event):void
{
if (event is MouseEvent)
{
var mouseEvent:MouseEvent = MouseEvent(event);
if (!isMouseOverMenu(mouseEvent) && !isMouseOverMenuBarItem(mouseEvent))
hideAllMenus();
}
else if (event is SandboxMouseEvent)
hideAllMenus();
}
/**
* @private
* Removes the root menu from the display list. This is called only for
* menus created using "createMenu".
*
* MJM private static?
*/
private static function menuHideHandler(event:MenuEvent):void
{
var menu:Menu = Menu(event.target);
if (!event.isDefaultPrevented() && event.menu == menu)
{
menu.supposedToLoseFocus = true;
PopUpManager.removePopUp(menu);
menu.removeEventListener(MenuEvent.MENU_HIDE, menuHideHandler);
}
}
/**
* @private
* Handle mouse release on an item.
*
* For separators or items with sub-menu, do nothing.
* For check items, toggle state, then fire change event.
* For radio items, toggle state if untoggled, then fire change event.
* For normal items, fire change event.
*/
override protected function mouseUpHandler(event:MouseEvent):void
{
var menuEvent:MenuEvent;
if (!enabled || !selectable || !visible)
return;
super.mouseUpHandler(event);
var row:IListItemRenderer = mouseEventToItemRenderer(event);
var item:Object;
if (row && row.data)
item = row.data;
if (item != null && _dataDescriptor.isEnabled(item) && !_dataDescriptor.isBranch(item))
{
// Toggle the item if it is a check or it is previously untoggled radio item.
// For previously toggled radio items, we will dispatch MenuEvent.ITEM_CLICK events but
// won't toggle or dispatch MenuEvent.CHANGE events.
// Custom item renderers can choose how to display this state
var toggleItem:Boolean = _dataDescriptor.getType(item) != "radio"
|| !_dataDescriptor.isToggled(item);
if (toggleItem)
setMenuItemToggled(item, !_dataDescriptor.isToggled(item));
menuEvent = new MenuEvent(MenuEvent.ITEM_CLICK);
menuEvent.menu = this;
menuEvent.index = this.selectedIndex;
menuEvent.menuBar = sourceMenuBar;
menuEvent.label = itemToLabel(item);
menuEvent.item = item;
menuEvent.itemRenderer = row;
getRootMenu().dispatchEvent(menuEvent);
if (toggleItem)
{
menuEvent = new MenuEvent(MenuEvent.CHANGE);
menuEvent.menu = this;
menuEvent.index = this.selectedIndex;
menuEvent.menuBar = sourceMenuBar;
menuEvent.label = itemToLabel(item);
menuEvent.item = item;
menuEvent.itemRenderer = row;
getRootMenu().dispatchEvent(menuEvent);
}
hideAllMenus();
}
}
/**
* @private
* Extend the behavior from ScrollSelectList to handle row presses over
* separators, branch items, and disabled row items.
*/
override protected function mouseDownHandler(event:MouseEvent):void
{
var row:IListItemRenderer = mouseEventToItemRenderer(event);
var item:Object;
if (row && row.data)
item = row.data;
// only allow action on items that are enabled which are not branches
if (item && _dataDescriptor.isEnabled(item) && !_dataDescriptor.isBranch(item))
super.mouseDownHandler(event);
}
/**
* @private
* Notify listeners when the mouse leaves
*/
override protected function mouseOutHandler(event:MouseEvent):void
{
if (!enabled || !selectable || !visible)
return;
systemManager.removeEventListener(MouseEvent.MOUSE_UP, mouseUpHandler, true);
// Fire the appropriate rollout event
var row:IListItemRenderer = mouseEventToItemRenderer(event);
if (!row)
return;
var item:Object;
if (row && row.data)
item = row.data;
// If a submenu was waiting to open, then clear the timeout
// so it doesn't open
if (openSubMenuTimer)
{
clearInterval(openSubMenuTimer);
openSubMenuTimer = 0;
}
// either we're rolling onto different subpieces of ourself or our
// highlight indicator, or the clearing of the highlighted item has
// already happened care of the mouseMove handler
if (itemRendererContains(row, event.relatedObject) ||
itemRendererContains(row, DisplayObject(event.target)) ||
event.relatedObject == highlightIndicator ||
event.relatedObject == listContent ||
!highlightItemRenderer)
{
return;
}
if (getStyle("useRollOver") && item)
{
clearHighlight(row);
}
}
/**
* @private
* Extend the behavior from ScrollSelectList to pop up submenus
*/
override protected function mouseOverHandler(event:MouseEvent):void
{
if (!enabled || !selectable || !visible)
return;
systemManager.addEventListener(MouseEvent.MOUSE_UP, mouseUpHandler, true, 0, true);
var row:IListItemRenderer = mouseEventToItemRenderer(event);
if (!row)
return;
var item:Object;
if (row && row.data)
item = row.data;
isPressed = event.buttonDown;
if (row && row != anchorRow)
{
if (anchorRow)
// no longer on anchor so close its submenu
drawItem(anchorRow, false, false);
if (subMenu)
{
subMenu.supposedToLoseFocus = true;
subMenu.closeTimer = setTimeout(closeSubMenu, 250, subMenu);
}
subMenu = null;
anchorRow = null;
}
else if (subMenu && subMenu.subMenu)
{
// Close grandchild submenus - only children are allowed to be open
subMenu.subMenu.hide();
}
// Update the view
if (_dataDescriptor.isBranch(item) && _dataDescriptor.isEnabled(item))
{
anchorRow = row;
// If there's a timer waiting to close this menu, cancel the
// timer so that the menu doesn't close
if (subMenu && subMenu.closeTimer)
{
clearInterval(subMenu.closeTimer);
subMenu.closeTimer = 0;
}
// If the menu is not visible, pop it up after a short delay
if (!subMenu || !subMenu.visible)
{
if (openSubMenuTimer)
clearInterval(openSubMenuTimer);
openSubMenuTimer = setTimeout(
function(row:IListItemRenderer):void
{
openSubMenu(row);
},
250,
row);
}
}
// Send event and update view
if (item && _dataDescriptor.isEnabled(item))
{
// we're rolling onto different subpieces of ourself or our highlight indicator
if (event.relatedObject)
{
if (itemRendererContains(row, event.relatedObject) ||
row == lastHighlightItemRenderer ||
event.relatedObject == highlightIndicator)
return;
}
}
if (row)
{
drawItem(row, false, Boolean(item && _dataDescriptor.isEnabled(item)));
if (isPressed)
{
if (item && _dataDescriptor.isEnabled(item))
{
if (!_dataDescriptor.isBranch(item))
selectItem(row, event.shiftKey, event.ctrlKey);
else
clearSelected();
}
}
if (item && _dataDescriptor.isEnabled(item))
{
// Fire the appropriate rollover event
var menuEvent:MenuEvent = new MenuEvent(MenuEvent.ITEM_ROLL_OVER);
menuEvent.menu = this;
menuEvent.index = getRowIndex(row);
menuEvent.menuBar = sourceMenuBar;
menuEvent.label = itemToLabel(item);
menuEvent.item = item;
menuEvent.itemRenderer = row;
getRootMenu().dispatchEvent(menuEvent);
}
}
}
/**
* @private
* ListBase uses the mouseClickHandler dispatch the ListEvent.ITEM_CLICK event.
* In Menu we chose to do that in the mouseUpHandler so we will do nothing
* in the mouseClickHandler.
*/
override protected function mouseClickHandler(event:MouseEvent):void
{
}
/**
* @private
*/
override protected function keyDownHandler(event:KeyboardEvent):void
{
var row:IListItemRenderer = selectedIndex == -1 ? null : listItems[selectedIndex - verticalScrollPosition][0];
var rowData:Object = row ? row.data : null;
var menu:Menu = row ? IMenuItemRenderer(row).menu : null;
var menuEvent:MenuEvent;
// If rtl layout, need to swap LEFT and RIGHT so correct action
// is done.
var keyCode:uint = mapKeycodeForLayoutDirection(event);
// Handle Key.UP Navigation
if (keyCode == Keyboard.UP)
{
if (rowData && _dataDescriptor.isBranch(rowData) && menu && menu.visible)
{
supposedToLoseFocus = true;
menu.setFocus();
// start at end and move up 1
menu.moveSelBy(menu.dataProvider.length, -1);
}
else
{
moveSelBy(selectedIndex, -1);
}
event.stopPropagation();
}
// Handle Key.DOWN Navigation
else if (keyCode == Keyboard.DOWN)
{
if (rowData && _dataDescriptor.isBranch(rowData) && menu && menu.visible)
{
supposedToLoseFocus = true;
menu.setFocus();
// start at -1 and move down 1
menu.moveSelBy(-1, 1);
}
else
{
moveSelBy(selectedIndex, 1);
}
event.stopPropagation();
}
// Handle Key.RIGHT Navigation
else if (keyCode == Keyboard.RIGHT)
{
if (rowData && _dataDescriptor.isBranch(rowData))
{
openSubMenu(row);
menu = IMenuItemRenderer(row).menu;
supposedToLoseFocus = true;
menu.setFocus();
// start at -1 and move down 1
menu.moveSelBy(-1, 1);
}
else
{
// jump to next sibling in the menubar
if (sourceMenuBar)
{
supposedToLoseFocus = true;
sourceMenuBar.setFocus();
// Redispatch the event to the MenuBar
// and let its keyDownHandler() handle it.
sourceMenuBar.dispatchEvent(event);
}
}
event.stopPropagation();
}
// Handle Key.LEFT Navigation
else if (keyCode == Keyboard.LEFT)
{
if (_parentMenu)
{
supposedToLoseFocus = true;
hide(); // hide this menu
_parentMenu.setFocus();
}
else
{
// jump to previous sibling in the menubar
if (sourceMenuBar)
{
supposedToLoseFocus = true;
sourceMenuBar.setFocus();
// Redispatch the event to the MenuBar
// and let its keyDownHandler() handle it.
sourceMenuBar.dispatchEvent(event);
}
}
event.stopPropagation();
}
// Handle Key.ENTER Commands
else if (keyCode == Keyboard.ENTER || keyCode == Keyboard.SPACE)
{
if (rowData && _dataDescriptor.isBranch(rowData))
{
openSubMenu(row);
menu = IMenuItemRenderer(row).menu;
supposedToLoseFocus = true;
menu.setFocus();
// start at -1 and move down 1
menu.moveSelBy(-1, 1);
}
else if (rowData)
{
// Toggle the item regardless of whether it is a check or radio
// Custom item renderers can choose how to display this state
setMenuItemToggled(rowData, !_dataDescriptor.isToggled(rowData));
menuEvent = new MenuEvent(MenuEvent.ITEM_CLICK);
menuEvent.menu = this;
menuEvent.index = this.selectedIndex;
menuEvent.menuBar = sourceMenuBar;
menuEvent.label = itemToLabel(rowData);
menuEvent.item = rowData;
menuEvent.itemRenderer = row;
getRootMenu().dispatchEvent(menuEvent);
menuEvent = new MenuEvent(MenuEvent.CHANGE);
menuEvent.menu = this;
menuEvent.index = this.selectedIndex;
menuEvent.menuBar = sourceMenuBar;
menuEvent.label = itemToLabel(rowData);
menuEvent.item = rowData;
menuEvent.itemRenderer = row;
getRootMenu().dispatchEvent(menuEvent);
hideAllMenus();
}
event.stopPropagation();
}
// Handle Key.ESCAPE commands
else if (keyCode == Keyboard.TAB)
{
menuEvent = new MenuEvent(MenuEvent.MENU_HIDE);
menuEvent.menu = getRootMenu();
menuEvent.menuBar = sourceMenuBar;
getRootMenu().dispatchEvent(menuEvent);
hideAllMenus();
event.stopPropagation();
}
else if (keyCode == Keyboard.ESCAPE)
{
if (_parentMenu)
{
supposedToLoseFocus = true;
hide(); // hide this menu
_parentMenu.setFocus();
}
else
{
menuEvent = new MenuEvent(MenuEvent.MENU_HIDE);
menuEvent.menu = getRootMenu();
menuEvent.menuBar = sourceMenuBar;
getRootMenu().dispatchEvent(menuEvent);
hideAllMenus();
event.stopPropagation();
}
}
}
/**
* @private
*/
private function moveSelBy(oldIndex:Number, incr:Number):void
{
var curIndex:Number = oldIndex;
if (isNaN(curIndex))
curIndex = -1;
var limit:Number = Math.max(0, Math.min(rowCount, collection.length) - 1);
var newIndex:Number = curIndex;
var item:Object;
var curItem:int = 0;
do
{
newIndex = newIndex + incr;
if (curItem > limit)
return;
else
curItem++;
if (newIndex > limit)
newIndex = 0;
else if (newIndex < 0)
newIndex = limit;
item = listItems[newIndex][0];
}
while (item.data && (_dataDescriptor.getType(item.data) == "separator" || !_dataDescriptor.isEnabled(item.data)));
var menuEvent:MenuEvent;
if (selectedIndex != -1)
{
var oldItem:Object = listItems[selectedIndex][0];
menuEvent = new MenuEvent(MenuEvent.ITEM_ROLL_OUT);
menuEvent.menu = this;
menuEvent.index = this.selectedIndex;
menuEvent.menuBar = sourceMenuBar;
menuEvent.label = itemToLabel(oldItem.data);
menuEvent.item = oldItem.data;
menuEvent.itemRenderer = IListItemRenderer(oldItem);
getRootMenu().dispatchEvent(menuEvent);
}
if (item.data)
{
selectItem(listItems[newIndex - verticalScrollPosition][0], false, false);
menuEvent = new MenuEvent(MenuEvent.ITEM_ROLL_OVER);
menuEvent.menu = this;
menuEvent.index = this.selectedIndex;
menuEvent.menuBar = sourceMenuBar;
menuEvent.label = itemToLabel(item.data);
menuEvent.item = item.data;
menuEvent.itemRenderer = IListItemRenderer(item);
getRootMenu().dispatchEvent(menuEvent);
}
}
// -------------------------------------------------------------------------
// Menu visibility management
// -------------------------------------------------------------------------
/**
* @private
*/
mx_internal function openSubMenu(row:IListItemRenderer):void
{
supposedToLoseFocus = true;
var r:Menu = getRootMenu();
var menu:Menu;
// check to see if the menu exists, if not create it
if (!IMenuItemRenderer(row).menu)
{
menu = new Menu();
menu.parentMenu = this;
menu.owner = this;
menu.showRoot = showRoot;
menu.dataDescriptor = r.dataDescriptor;
menu.styleName = r;
menu.labelField = r.labelField;
menu.labelFunction = r.labelFunction;
menu.iconField = r.iconField;
menu.iconFunction = r.iconFunction;
menu.itemRenderer = r.itemRenderer;
menu.rowHeight = r.rowHeight;
menu.scaleY = r.scaleY;
menu.scaleX = r.scaleX;
// if there's data and it has children then add the items
if (row.data &&
_dataDescriptor.isBranch(row.data) &&
_dataDescriptor.hasChildren(row.data))
{
menu.dataProvider = _dataDescriptor.getChildren(row.data);
}
menu.sourceMenuBar = sourceMenuBar;
menu.sourceMenuBarItem = sourceMenuBarItem;
IMenuItemRenderer(row).menu = menu;
PopUpManager.addPopUp(menu, r, false);
}
else
{
menu = IMenuItemRenderer(row).menu;
}
var _do:DisplayObject = DisplayObject(row);
var sandBoxRootPoint:Point = new Point(0,0);
sandBoxRootPoint = _do.localToGlobal(sandBoxRootPoint);
// when loadMovied, you may not be in global coordinates
if (_do.root) //verify this is sufficient
sandBoxRootPoint = _do.root.globalToLocal(sandBoxRootPoint);
// If the layout is mirrored, and isDirectionLeft then we add the
// menu's width to find the origin of menu, rather than subtracting.
// Similarly, if isDirectionLeft=false, we subtract the menu item's width.
const isLayoutMirrored:Boolean =
(layoutDirection == LayoutDirection.RTL);
const menuWidth:Number = menu.getExplicitOrMeasuredWidth();
const menuItemWidth:Number = row.width;
// showX, showY are in sandbox root coordinates
var showY:Number = sandBoxRootPoint.y;
var showX:Number;
if (!isDirectionLeft)
showX = sandBoxRootPoint.x + ((isLayoutMirrored) ? -menuItemWidth : +menuItemWidth);
else
showX = sandBoxRootPoint.x + ((isLayoutMirrored) ? +menuWidth : -menuWidth);
// If the layout has been mirrored, then showX is currently menu's right
// edge; compensate here.
if (isLayoutMirrored)
showX -= menuWidth;
// convert to global coordinates to compare with getVisibleApplicationRect().
// the screen is the visible coordinates of our sandbox (written in global coordinates)
var screen:Rectangle = systemManager.getVisibleApplicationRect(null, true);
var sbRoot:DisplayObject = systemManager.getSandboxRoot();
var screenPoint:Point = sbRoot.localToGlobal(new Point(showX, showY));
// do x
var shift:Number = screenPoint.x + menuWidth - screen.right;
if (shift > 0 || screenPoint.x < screen.x)
{
// if we want to ensure our parent's visible, let's
// modify the shift so that we're not just on-screen
// but we're also shifted away from our parent.
var shiftForParent:Number = getExplicitOrMeasuredWidth() + menu.getExplicitOrMeasuredWidth();
// if was going left, shift to right. otherwise, shift to left
if (isDirectionLeft)
shiftForParent *= -1;
showX = Math.max(showX - shiftForParent, 0);
// now make sure we're still on-screen again
screenPoint = new Point(showX, showY);
screenPoint = sbRoot.localToGlobal(screenPoint);
// only shift if greater our position + width > width of screen
shift = Math.max(0, screenPoint.x + width - screen.right);
showX = Math.max(showX - shift, 0);
}
menu.isDirectionLeft = (isLayoutMirrored) ? (this.x <= showX) : (this.x > showX);
// now do y
shift = screenPoint.y + menu.getExplicitOrMeasuredHeight() - screen.bottom;
if (shift > 0 || screenPoint.y < screen.y)
showY = Math.max(showY - shift, 0);
menu.show(showX, showY);
subMenu = menu;
clearInterval(openSubMenuTimer);
openSubMenuTimer = 0;
}
/**
* @private
*/
private function closeSubMenu(menu:Menu):void
{
menu.hide();
clearInterval(menu.closeTimer);
menu.closeTimer = 0;
}
mx_internal function deleteDependentSubMenus():void
{
var n:int = listItems.length;
for (var i:int = 0; i < n; i++)
{
// Check to see if the listItems array has a renderer at this index.
if (listItems[i][0])
{
var subMenu:Menu = IMenuItemRenderer(listItems[i][0]).menu;
if (subMenu)
{
subMenu.deleteDependentSubMenus();
PopUpManager.removePopUp(subMenu);
IMenuItemRenderer(listItems[i][0]).menu = null;
}
}
}
}
mx_internal function hideAllMenus():void
{
getRootMenu().hide();
getRootMenu().deleteDependentSubMenus();
}
//--------------------------------------------------------------------------
// Internal utilities
//--------------------------------------------------------------------------
private function isMouseOverMenu(event:MouseEvent):Boolean
{
var target:DisplayObject = DisplayObject(event.target);
while (target)
{
if (target is Menu)
return true;
target = target.parent;
}
return false;
}
private function isMouseOverMenuBarItem(event:MouseEvent):Boolean
{
if (!sourceMenuBarItem)
return false;
var target:DisplayObject = DisplayObject(event.target);
while (target)
{
if (target == sourceMenuBarItem)
return true;
target = target.parent;
}
return false;
}
/**
* From any menu, walks up the parent menu chain and finds the root menu.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
mx_internal function getRootMenu():Menu
{
var target:Menu = this;
while (target.parentMenu)
target = target.parentMenu;
return target;
}
/**
* Given a row, find the row's index in the Menu.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
private function getRowIndex(row:IListItemRenderer):int
{
for (var i:int = 0; i < listItems.length; i++)
{
var item:IListItemRenderer = listItems[i][0];
if (item && item.data && !(_dataDescriptor.getType(item.data) == "separator"))
if (item == row)
return i;
}
return -1;
}
/**
* For autotesting, get the current set of submenus
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
mx_internal function get subMenus():Array
{
var arr:Array = [];
for (var i:int = 0; i < listItems.length; i++)
{
arr.push(listItems[i][0].menu);
}
return arr;
}
}
}