05cbe9db64
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
481 lines
17 KiB
ActionScript
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;
|
|
}
|
|
}
|