Files
nenya/src/as/com/threerings/flex/CommandMenu.as
T
Ray Greenwell 05cbe9db64 Add support for titles in menus.
Added an addTitle() convenience method.
Unfortunately, styling the background of the title either
impossible or so deep down the rabbit hole... deeper than this???


git-svn-id: svn+ssh://src.earth.threerings.net/nenya/trunk@806 ed5b42cb-e716-0410-a449-f6a68f950b19
2009-04-16 01:42:48 +00:00

481 lines
17 KiB
ActionScript

//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2007 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/nenya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.flex {
import flash.display.DisplayObject;
import flash.display.DisplayObjectContainer;
import flash.events.Event;
import flash.events.IEventDispatcher;
import flash.geom.Point;
import flash.geom.Rectangle;
import mx.controls.Menu;
import mx.controls.listClasses.BaseListData;
import mx.controls.listClasses.IListItemRenderer;
import mx.controls.menuClasses.IMenuItemRenderer;
import mx.controls.menuClasses.MenuListData;
import mx.core.mx_internal;
import mx.core.Application;
import mx.core.ClassFactory;
import mx.core.IFlexDisplayObject;
import mx.core.ScrollPolicy;
import mx.events.MenuEvent;
import mx.managers.PopUpManager;
import com.threerings.util.CommandEvent;
import com.threerings.flex.PopUpUtil;
import com.threerings.flex.menuClasses.CommandMenuItemRenderer;
import com.threerings.flex.menuClasses.CommandListData;
use namespace mx_internal;
/**
* A pretty standard menu that can submit CommandEvents if menu items have "command" and possibly
* "arg" properties. Commands are submitted to controllers for processing. Alternatively, you may
* specify "callback" properties that specify a function closure to call, with the "arg" property
* containing either a single arg or an array of args.
*
* Another property now supported is "iconObject", which is an already-instantiated
* IFlexDisplayObject to use as the icon. This will only be applied if the "icon" property
* (which specifies a Class) is not used.
*
* Another 'type' now supported is "title", which gives the label for that item the style
* '.menuTitle' and disables it. Your ".menuTitle" style should use the disabledColor.
*
* Example dataProvider array:
* [ { label: "Go home", icon: homeIconClass,
* command: Controller.GO_HOME, arg: homeId },
* { type: "separator"},
{ label: "Crazytown", callback: setCrazy, arg: [ true, false ] },
* { label: "Other places", children: subMenuArray }
* ];
*
* See "Defining menu structure and data" in the Flex manual for the full list.
*
* CommandMenu is scrollable. This can be forced by setting verticalScrollPolicy to
* ScrollPolicy.AUTO or ScrollPolicy.ON and setting the maxHeight. Also, if the scrolling isn't
* forced on, but the menu does not fit within the vertical bounds given (either the stage size by
* default, or the height of the rectangle given in setBounds()), scrolling will be turned on
* so that none of the content is lost.
*
* Note: we don't extend flexlib's ScrollableMenu (or its sub-class ScrollableArrowMenu) because it
* does some weird stuff in measure() that forces some of our menus down to a very small height...
*/
public class CommandMenu extends Menu
{
/**
* Factory method to create a command menu.
*
* @param items an array of menu items.
* @param dispatcher an override event dispatcher to use for command events, rather than
* our parent.
*/
public static function createMenu (
items :Array, dispatcher :IEventDispatcher = null) :CommandMenu
{
var menu :CommandMenu = new CommandMenu();
menu.owner = DisplayObjectContainer(Application.application);
menu.tabEnabled = false;
menu.showRoot = true;
menu.setDispatcher(dispatcher);
Menu.popUpMenu(menu, null, items); // does not actually pop up, but needed.
return menu;
}
/**
* Add a title to the TOP of the specified menu.
*
* @param icon may be a Class or IFlexDisplayObject.
*/
public static function addTitle (
menuItems :Array, title :String, icon :* = null, separatorAfter :Boolean = true) :void
{
var o :Object = { label: title, type: "title" };
if (icon is Class) {
o["icon"] = icon;
} else if (icon is IFlexDisplayObject) {
o["iconObj"] = icon;
}
if (separatorAfter) {
menuItems.unshift({ type: "separator" });
}
menuItems.unshift(o);
}
/**
* Add a separator to the specified menu, unless it would not make sense to do so, because
* the menu is empty or already ends with a separator.
*/
public static function addSeparator (menuItems :Array) :void
{
const len :int = menuItems.length;
if (len > 0 && menuItems[len - 1].type != "separator") {
menuItems.push({ type: "separator" });
}
}
/**
* The mx.controls.Menu class overrides setting and getting the verticalScrollPolicy
* basically setting the verticalScrollPolicy did nothing, and getting it always returned
* ScrollPolicy.OFF. So that's not going to work if we want the menu to scroll. Here we
* reinstate the verticalScrollPolicy setter, and keep a local copy of the value in a
* protected variable _verticalScrollPolicy.
*
* This setter is basically a copy of what ScrollControlBase and ListBase do.
*/
override public function set verticalScrollPolicy (value :String) :void
{
var newPolicy :String = value.toLowerCase();
itemsSizeChanged = true;
if (_verticalScrollPolicy != newPolicy)
{
_verticalScrollPolicy = newPolicy;
dispatchEvent(new Event("verticalScrollPolicyChanged"));
}
invalidateDisplayList();
}
override public function get verticalScrollPolicy () :String
{
return _verticalScrollPolicy;
}
public function CommandMenu ()
{
super();
itemRenderer = new ClassFactory(getItemRenderer());
verticalScrollPolicy = ScrollPolicy.OFF;
addEventListener(MenuEvent.ITEM_CLICK, itemClicked);
}
/**
* Called in the CommandMenu constructor, this should return the item rendering class for this
* CommandMenu.
*/
protected function getItemRenderer () :Class
{
return CommandMenuItemRenderer;
}
/**
* Configures the event dispatcher to be used when dispatching this menu's command events.
* Normally they will be dispatched on our parent (usually the SystemManager or something).
*/
public function setDispatcher (dispatcher :IEventDispatcher) :void
{
_dispatcher = dispatcher;
}
/**
* Sets a Rectangle (in stage coords) that the menu will attempt to keep submenus positioned
* within. If a submenu is too large to fit, it will position it in the lower right corner
* of this Rectangle (or top if popping upwards, or left if popping leftwards). This
* value defaults to the stage bounds.
*/
public function setBounds (fitting :Rectangle) :void
{
_fitting = fitting;
}
/**
* Actually pop up the menu. This can be used instead of show().
*/
public function popUp (
trigger :DisplayObject, popUpwards :Boolean = false, popLeftwards :Boolean = false) :void
{
_upping = popUpwards;
_lefting = popLeftwards;
var r :Rectangle = trigger.getBounds(trigger.stage);
show(_lefting ? r.left : r.right, _upping ? r.top : r.bottom);
}
/**
* Shows the menu at the specified mouse coordinates.
*/
public function popUpAt (
mx :int, my :int, popUpwards :Boolean = false, popLeftwards :Boolean = false) :void
{
_upping = popUpwards;
_lefting = popLeftwards;
show(mx, my);
}
/**
* Shows the menu at the current mouse location.
*/
public function popUpAtMouse (popUpwards :Boolean = false, popLeftwards :Boolean = false) :void
{
_upping = popUpwards;
_lefting = popLeftwards;
show(); // our show, with no args, pops at the mouse
}
/**
* Just like our superclass's show(), except that when invoked with no args, causes the menu to
* show at the current mouse location instead of the top-left corner of the application.
* Also, we ensure that the resulting menu is in-bounds.
*/
override public function show (xShow :Object = null, yShow :Object = null) :void
{
if (xShow == null) {
xShow = DisplayObject(Application.application).mouseX;
}
if (yShow == null) {
yShow = DisplayObject(Application.application).mouseY;
}
super.show(xShow, yShow);
// reposition now that we know our size
if (_lefting) {
y = x - getExplicitOrMeasuredWidth();
}
if (_upping) {
y = y - getExplicitOrMeasuredHeight();
}
var fitting :Rectangle = _fitting || screen;
PopUpUtil.fitInRect(this, fitting);
// if after fitting as best we can, the menu is outside of the declared bounds, we force
// scrolling and set the max height
if (y < fitting.y) {
verticalScrollPolicy = ScrollPolicy.AUTO;
maxHeight = fitting.height;
y = fitting.y;
}
}
/**
* The Menu class overrode configureScrollBars() and made the function do nothing. That means
* the scrollbars don't know how to draw themselves, so here we reinstate configureScrollBars.
* This is basically a copy of the same method from the mx.controls.List class.
*/
override protected function configureScrollBars () :void
{
var rowCount :int = listItems.length;
if (rowCount == 0) {
return;
}
// if there is more than one row and it is a partial row we don't count it
if (rowCount > 1 &&
rowInfo[rowCount - 1].y + rowInfo[rowCount - 1].height > listContent.height) {
rowCount--;
}
// offset, when added to rowCount, is hte index of the dataProvider item for that row.
// IOW, row 10 in listItems is showing dataProvider item 10 + verticalScrollPosition -
// lockedRowCount - 1
var offset :int = verticalScrollPosition - lockedRowCount - 1;
// don't count filler rows at the bottom either.
var fillerRows :int = 0;
while (rowCount > 0 && listItems[rowCount - 1].length == 0)
{
if (collection && rowCount + offset >= collection.length) {
rowCount--;
fillerRows++;
} else {
break;
}
}
var colCount :int = listItems[0].length;
var oldHorizontalScrollBar :Object = horizontalScrollBar;
var oldVerticalScrollBar :Object = verticalScrollBar;
var roundedWidth :int = Math.round(unscaledWidth);
var length :int = collection ? collection.length - lockedRowCount : 0;
var numRows :int = rowCount - lockedRowCount;
setScrollBarProperties(Math.round(listContent.width), roundedWidth, length, numRows);
maxVerticalScrollPosition = Math.max(length - numRows, 0);
}
/**
* Callback for MenuEvent.ITEM_CLICK.
*/
protected function itemClicked (event :MenuEvent) :void
{
var arg :Object = getItemProp(event.item, "arg");
var cmdOrFn :Object = getItemProp(event.item, "command");
if (cmdOrFn == null) {
cmdOrFn = getItemProp(event.item, "callback");
}
if (cmdOrFn != null) {
event.stopImmediatePropagation();
CommandEvent.dispatch(_dispatcher == null ? mx_internal::parentDisplayObject :
_dispatcher, cmdOrFn, arg);
}
// else: no warning. There may be non-command menu items mixed in.
}
// from ScrollableArrowMenu..
override mx_internal function openSubMenu (row :IListItemRenderer) :void
{
supposedToLoseFocus = true;
var r :Menu = getRootMenu();
var menu :CommandMenu;
// check to see if the menu exists, if not create it
if (!IMenuItemRenderer(row).menu) {
// the only differences between this method and the original method in mx.controls.Menu
// are these few lines.
menu = new CommandMenu();
menu.maxHeight = this.maxHeight;
menu.verticalScrollPolicy = this.verticalScrollPolicy;
menu.variableRowHeight = this.variableRowHeight;
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);
}
super.openSubMenu(row);
// if we're lefting, upping or fitting make sure our submenu does so as well
var submenu :Menu = IMenuItemRenderer(row).menu;
if (_lefting) {
submenu.x -= submenu.getExplicitOrMeasuredWidth();
}
if (_upping) {
var rowLoc :Point = row.localToGlobal(new Point());
submenu.y = rowLoc.y - submenu.getExplicitOrMeasuredHeight() + row.height;
}
var fitting :Rectangle = _fitting || screen;
PopUpUtil.fitInRect(submenu, fitting);
// if after fitting as best we can, the menu is outside of the declared bounds, we force
// scrolling and set the max height
if (submenu.y < fitting.y) {
submenu.verticalScrollPolicy = ScrollPolicy.AUTO;
submenu.maxHeight = fitting.height;
submenu.y = fitting.y;
}
}
override protected function measure () :void
{
super.measure();
if (measuredHeight > this.maxHeight) {
measuredHeight = this.maxHeight;
}
if (verticalScrollPolicy == ScrollPolicy.ON || verticalScrollPolicy == ScrollPolicy.AUTO) {
if (verticalScrollBar) {
measuredMinWidth = measuredWidth = measuredWidth + verticalScrollBar.minWidth;
}
}
commitProperties();
}
// from List
override protected function makeListData (data :Object, uid :String, rowNum :int) :BaseListData
{
// Oh, FFS.
// We need to set up these "maxMeasuredIconWidth" fields on the MenuListData, but our
// superclass has made those variables private.
// We can get the values out of another MenuListData, so we just always call super()
// to create one of those, and if we need to make a CommandListData, we construct one
// from the fields in the MenuListData.
var menuListData :MenuListData = super.makeListData(data, uid, rowNum) as MenuListData;
var iconObject :IFlexDisplayObject = getItemProp(data, "iconObject") as IFlexDisplayObject;
if (iconObject != null) {
var cmdListData :CommandListData = new CommandListData(menuListData.label,
menuListData.icon, iconObject, labelField, uid, this, rowNum);
cmdListData.maxMeasuredIconWidth = menuListData.maxMeasuredIconWidth;
cmdListData.maxMeasuredTypeIconWidth = menuListData.maxMeasuredTypeIconWidth;
cmdListData.maxMeasuredBranchIconWidth = menuListData.maxMeasuredBranchIconWidth;
cmdListData.useTwoColumns = menuListData.useTwoColumns;
return cmdListData;
}
return menuListData;
}
/**
* Get the specified property for the specified item, if any. Somewhat similar to bits in the
* DefaultDataDescriptor.
*/
protected function getItemProp (item :Object, prop :String) :Object
{
try {
if (item is XML) {
return String((item as XML).attribute(prop));
} else if (prop in item) {
return item[prop];
}
} catch (e :Error) {
// alas; fall through
}
return null;
}
protected var _dispatcher :IEventDispatcher;
protected var _lefting :Boolean = false;
protected var _upping :Boolean = false;
protected var _fitting :Rectangle = null;
protected var _verticalScrollPolicy :String;
}
}