Files
nenya/src/as/com/threerings/flex/CommandMenu.as
T
Michael Bayne 4e50f81400 CommandMenu no longer scrolls. It now supports methods to display a menu inside
a visible area or to expliclty pop up or left which it correctly passes on to
its submenus.


git-svn-id: svn+ssh://src.earth.threerings.net/nenya/trunk@385 ed5b42cb-e716-0410-a449-f6a68f950b19
2007-12-20 03:23:04 +00:00

303 lines
10 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.IEventDispatcher;
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.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 mx.utils.ObjectUtil;
import flexlib.controls.ScrollableArrowMenu;
import com.threerings.util.CommandEvent;
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.
*
* 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.
*/
public class CommandMenu extends Menu
{
/**
* Factory method to create a command menu.
*
* @param items an array of menu items.
*/
public static function createMenu (items :Array) :CommandMenu
{
var menu :CommandMenu = new CommandMenu();
menu.owner = DisplayObjectContainer(Application.application);
menu.tabEnabled = false;
menu.showRoot = true;
Menu.popUpMenu(menu, null, items);
return menu;
}
public function CommandMenu ()
{
super();
itemRenderer = new ClassFactory(CommandMenuItemRenderer);
verticalScrollPolicy = ScrollPolicy.OFF;
addEventListener(MenuEvent.ITEM_CLICK, itemClicked);
}
/**
* Configures the event dispatcher to be used when dispatching this menu's events. By default
* they will be dispatched on the stage.
*/
public function setDispatcher (dispatcher :IEventDispatcher) :void
{
_dispatcher = dispatcher;
}
/**
* 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;
_fitting = null;
var r :Rectangle = trigger.getBounds(trigger.stage);
show(_lefting ? r.left : r.right, _upping ? r.top : r.bottom);
}
/**
* Shows the menu at the current mouse coordinates, popping upwards or leftwards as necessary
* to fit the menu into the supplied bounds.
*/
public function popUpIn (bounds :Rectangle) :void
{
var mx :int = DisplayObject(Application.application).mouseX;
var my :int = DisplayObject(Application.application).mouseY;
// show it at zero zero so that it is layed out and measured
show(0, 0);
_fitting = bounds;
// then determine if we need to point it leftwards or upwards
if (mx + getExplicitOrMeasuredWidth() > bounds.right) {
mx -= getExplicitOrMeasuredWidth();
}
if (my + getExplicitOrMeasuredHeight() > bounds.bottom) {
my -= getExplicitOrMeasuredHeight();
}
// finally move it to the correct position
x = mx;
y = my;
}
/**
* 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;
_fitting = null;
show(mx, my);
}
/**
* 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.
*/
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();
}
}
/**
* 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;
var lefting :Boolean = _lefting;
var upping :Boolean = _upping;
if (_fitting != null) {
if (submenu.x + submenu.getExplicitOrMeasuredWidth() > _fitting.right) {
lefting = true;
}
if (submenu.y + submenu.getExplicitOrMeasuredHeight() > _fitting.bottom) {
upping = true;
}
}
if (lefting) {
submenu.x -= submenu.getExplicitOrMeasuredWidth();
}
if (upping) {
submenu.y -= (submenu.getExplicitOrMeasuredHeight() - DisplayObject(row).height);
}
}
// from List
override protected function makeListData (data :Object, uid :String, rowNum :int) :BaseListData
{
return new CommandListData(itemToLabel(data), itemToIcon(data),
getItemProp(data, "iconObject") as IFlexDisplayObject, labelField, uid, this, rowNum);
}
/**
* 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 _fitting :Rectangle;
protected var _lefting :Boolean = false;
protected var _upping :Boolean = false;
}
}