diff --git a/src/as/com/dougmccune/controls/ScrollableArrowMenu.as b/src/as/com/dougmccune/controls/ScrollableArrowMenu.as new file mode 100644 index 00000000..d5016ae3 --- /dev/null +++ b/src/as/com/dougmccune/controls/ScrollableArrowMenu.as @@ -0,0 +1,288 @@ +package com.dougmccune.controls +{ + import flash.display.DisplayObjectContainer; + import flash.events.Event; + import flash.events.MouseEvent; + import flash.events.TimerEvent; + import flash.utils.Timer; + + import mx.controls.Button; + import mx.controls.List; + import mx.controls.Menu; + import mx.controls.listClasses.IListItemRenderer; + import mx.controls.listClasses.ListBase; + import mx.controls.menuClasses.IMenuItemRenderer; + import mx.controls.scrollClasses.ScrollBar; + import mx.core.Application; + import mx.core.ScrollControlBase; + import mx.core.ScrollPolicy; + import mx.core.mx_internal; + import mx.events.ScrollEvent; + import mx.managers.PopUpManager; + + use namespace mx_internal; + + public class ScrollableArrowMenu extends ScrollableMenu + { + //The buttons that are used for the scrolling up and down + private var upButton:Button; + private var downButton:Button; + + //We use a timer to control the scrolling while the mouse is over the buttons + private var timer:Timer; + + //scrollSpeed is the delay between scrolling the list, so a smaller number + //here will increase the speed of the scrolling. This is in ms. + public var scrollSpeed:Number = 80; + + //scrollJump specifies how many rows to scroll each time. Leaving it at 1 makes + //for the smoothest scrolling + public var scrollJump:Number = 1; + + + /** + * We are adding a new property called arrowScrollPolicy. This is just like + * verticalScrollPolicy, except it controls how we display the up and down arrows + * for scrolling. If this is set to ScrollPolicy.OFF we never show the arrows. + * If it's ScrollPolicy.ON we always show the arrows. And if it's ScrollPolicy.AUTO + * then we show the arrows if they are needed. OFF and AUTO are the only ones + * that should probably be used, since ON gets in the way of the first menu item + * in the list. + */ + private var _arrowScrollPolicy:String = ScrollPolicy.AUTO; + + public function get arrowScrollPolicy():String { + return _arrowScrollPolicy; + } + public function set arrowScrollPolicy(value:String):void { + this._arrowScrollPolicy = value; + + invalidateDisplayList(); + } + + /* A few icons that we'll use for the up and down buttons */ + [Embed("../skins/menu_assets.swf#up_arrow")] + public var up_icon:Class; + + [Embed("../skins/menu_assets.swf#down_arrow")] + public var down_icon:Class; + + [Embed("../skins/menu_assets.swf#up_arrow_disabled")] + public var up_icon_disabled:Class; + + [Embed("../skins/menu_assets.swf#down_arrow_disabled")] + public var down_icon_disabled:Class; + + /** + * We have to override the static function createMenu so that we create a + * ScrollableMenu instead of a normal Menu. + */ + public static function createMenu(parent:DisplayObjectContainer, mdp:Object, showRoot:Boolean=true):ScrollableArrowMenu + { + var menu:ScrollableArrowMenu = new ScrollableArrowMenu(); + menu.tabEnabled = false; + + menu.owner = DisplayObjectContainer(Application.application); + menu.showRoot = showRoot; + popUpMenu(menu, parent, mdp); + return menu; + } + + public function ScrollableArrowMenu() + { + super(); + } + + /** + * We override createChildren so we can instantiate our up and down buttons + * and add them as children. + */ + override protected function createChildren():void { + super.createChildren(); + + upButton = new Button(); + upButton.setStyle("cornerRadius", 0); + upButton.setStyle("fillAlphas", [1,1,1,1]); + + downButton = new Button(); + downButton.setStyle("cornerRadius", 0); + downButton.setStyle("fillAlphas", [1,1,1,1]); + + upButton.setStyle("icon", up_icon); + downButton.setStyle("icon", down_icon); + upButton.setStyle("disabledIcon", up_icon_disabled); + downButton.setStyle("disabledIcon", down_icon_disabled); + + addChild(upButton); + addChild(downButton); + + upButton.addEventListener(MouseEvent.ROLL_OVER, startScrollingUp); + upButton.addEventListener(MouseEvent.ROLL_OUT, stopScrolling); + + downButton.addEventListener(MouseEvent.ROLL_OVER, startScrollingDown); + downButton.addEventListener(MouseEvent.ROLL_OUT, stopScrolling); + + //we're using an event listener to check if we should still be showing the + //up and down buttons. This checks every time the list is scrolled at all. + this.addEventListener(ScrollEvent.SCROLL, checkButtons); + } + + /** + * We need to override openSubMenu as well, so that any subMenus opened by this Menu controls + * will also be ScrollableMenus and will have the same maxHeight set + */ + override mx_internal function openSubMenu(row:IListItemRenderer):void + { + supposedToLoseFocus = true; + + var r:Menu = getRootMenu(); + var menu:ScrollableArrowMenu; + + // 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 four lines. + */ + menu = new ScrollableArrowMenu(); + menu.maxHeight = this.maxHeight; + menu.verticalScrollPolicy = this.verticalScrollPolicy; + menu.arrowScrollPolicy = this.arrowScrollPolicy; + + 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); + } + + /** + * We've got to layout the up and down buttons now. They are overlaid on the list + * at the very top and bottom. + */ + override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void { + measure(); + + super.updateDisplayList(unscaledWidth, unscaledHeight); + + + var w:Number = unscaledWidth; + + if(verticalScrollBar) { + w = unscaledWidth - ScrollBar.THICKNESS; + } + + upButton.setActualSize(w, 15); + downButton.setActualSize(w, 15); + + upButton.move(0, 0); + downButton.move(0, measuredHeight - downButton.height); + + checkButtons(null); + } + + /** + * This method is used to hide or show the up and down buttons, depending on where we + * are scrolled in the list and what the setting of arrowScrollPolicy is + */ + private function checkButtons(event:Event):void { + if(this.arrowScrollPolicy == ScrollPolicy.AUTO) { + upButton.visible = upButton.enabled = (this.verticalScrollPosition != 0); + downButton.visible = downButton.enabled = (this.verticalScrollPosition != this.maxVerticalScrollPosition); + } + else if(this.arrowScrollPolicy == ScrollPolicy.ON) { + upButton.visible = downButton.visible = true; + upButton.enabled = (this.verticalScrollPosition != 0); + downButton.enabled = (this.verticalScrollPosition != this.maxVerticalScrollPosition); + } + else { + upButton.visible = upButton.enabled = downButton.visible = downButton.enabled = false; + } + } + + /** + * We start a timer that updates the verticalScrollPosition at a regular interval + * until the mouse rolls off the button. + */ + private function startScrollingUp(event:Event):void { + if(timer && timer.running) { + timer.stop(); + } + + timer = new Timer(this.scrollSpeed); + timer.addEventListener(TimerEvent.TIMER, scrollUp); + + timer.start(); + } + + private function startScrollingDown(event:Event):void { + if(timer && timer.running) { + timer.stop(); + } + + timer = new Timer(this.scrollSpeed); + timer.addEventListener(TimerEvent.TIMER, scrollDown); + + timer.start(); + } + + private function stopScrolling(event:Event):void { + + stage.removeEventListener(MouseEvent.MOUSE_UP, stopScrolling); + + if(timer && timer.running) { + timer.stop(); + } + } + + private function scrollUp(event:TimerEvent):void { + if(this.verticalScrollPosition - scrollJump > 0) { + this.verticalScrollPosition -= scrollJump; + } + else { + this.verticalScrollPosition = 0; + } + + + checkButtons(null); + } + + private function scrollDown(event:TimerEvent):void { + if(this.verticalScrollPosition + scrollJump < this.maxVerticalScrollPosition) { + this.verticalScrollPosition += scrollJump; + } + else { + this.verticalScrollPosition = this.maxVerticalScrollPosition; + } + + + checkButtons(null); + } + + } +} + diff --git a/src/as/com/dougmccune/controls/ScrollableMenu.as b/src/as/com/dougmccune/controls/ScrollableMenu.as new file mode 100644 index 00000000..45485538 --- /dev/null +++ b/src/as/com/dougmccune/controls/ScrollableMenu.as @@ -0,0 +1,240 @@ +//////////////////////////////////////////////////////////////////////////////// +// +// Code written by Doug McCune. +// http://dougmccune.com/blog +// +// You can use this code for whatever you want. Just don't go and try to sell +// it as your own. If you use it to make something better and want to sell that +// then go for it. +// +// Let's all play nice and be happy. +// +//////////////////////////////////////////////////////////////////////////////// + +package com.dougmccune.controls +{ + import flash.display.DisplayObject; + import flash.display.DisplayObjectContainer; + import flash.events.Event; + import flash.geom.Point; + + import mx.controls.Menu; + import mx.controls.listClasses.IListItemRenderer; + import mx.controls.menuClasses.IMenuBarItemRenderer; + import mx.controls.menuClasses.IMenuItemRenderer; + import mx.controls.scrollClasses.ScrollBar; + import mx.core.Application; + import mx.core.EdgeMetrics; + import mx.core.ScrollPolicy; + import mx.core.mx_internal; + import mx.managers.PopUpManager; + + use namespace mx_internal; + + public class ScrollableMenu extends Menu + { + public function ScrollableMenu() + { + super(); + } + + /** + * We have to override the static function createMenu so that we create a + * ScrollableMenu instead of a normal Menu. + */ + public static function createMenu(parent:DisplayObjectContainer, mdp:Object, showRoot:Boolean=true):ScrollableMenu + { + var menu:ScrollableMenu = new ScrollableMenu(); + menu.tabEnabled = false; + menu.owner = DisplayObjectContainer(Application.application); + menu.showRoot = showRoot; + popUpMenu(menu, parent, mdp); + return menu; + } + + + /** + * 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 private 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(); + } + + /** Again, the Menu class just returned ScrollPolicy.OFF every time. + * Now we actually return the value that we stored with the setter. + * */ + override public function get verticalScrollPolicy():String { + return this._verticalScrollPolicy; + } + + /** + * 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. It would have been nice if + * we could have called this method from down in a subclass of Menu, but AS + * doesn't let us do something like super.super, so instead we have to recreate + * the class here. + * */ + 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 dont count it + if (rowCount > 1 && rowInfo[rowCount - 1].y + rowInfo[rowCount-1].height > listContent.height) + rowCount--; + + // offset, when added to rowCount, is the 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; + // don't count filler rows at the bottom either. + while (rowCount && listItems[rowCount - 1].length == 0) + { + if (collection && rowCount + offset >= collection.length) + { + rowCount--; + ++fillerRows; + } + else + break; + } + + /** + * This part needs further functions from mx.controls.List that we don't have + * access to. What to do? Whatever, I'll just comment it out and cross my fingers + * */ + // we have to scroll up. We can't have filler rows unless the scrollPosition is 0 + /* + if (verticalScrollPosition > 0 && fillerRows > 0) + { + if (adjustVerticalScrollPositionDownward(Math.max(rowCount, 1))) + return; + }*/ + + 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; + + /* This call is slightly modified from mx.controls.List, but not by much */ + setScrollBarProperties( + Math.round(listContent.width) , + roundedWidth, length, numRows); + maxVerticalScrollPosition = Math.max(length - numRows, 0); + + } + + /** + * We need to override openSubMenu as well, so that any subMenus opened by this Menu controls + * will also be ScrollableMenus and will have the same maxHeight set + */ + override 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) + { + /* The only differences between this method and the original method in mx.controls.Menu + * are these two lines. + */ + menu = new ScrollableMenu(); + menu.maxHeight = this.maxHeight; + menu.verticalScrollPolicy = this.verticalScrollPolicy; + + + 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); + } + + /** + * We overide the measure() method because we need to check if the menu is going off + * the stage. If it's going to be too high, then we make it smaller to keep it from + * going off. I also stuck in a buffer of 10 pixels from the bottom of the stage. + * + * We also check if we're showing the vertical scrollbar, and if so we adjust the + * width to account for that. + */ + 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; + } + } + + var pt:Point = new Point(0, 0); + pt = this.localToGlobal(pt); + + var stageHeightAvailable:Number = screen.y + screen.height - pt.y - 10; + if(stageHeightAvailable < measuredHeight) { + measuredHeight = stageHeightAvailable; + invalidateSize(); + } + + commitProperties(); + } + + + } +} + diff --git a/src/as/com/dougmccune/skins/menu_assets.swf b/src/as/com/dougmccune/skins/menu_assets.swf new file mode 100644 index 00000000..7bff0c56 Binary files /dev/null and b/src/as/com/dougmccune/skins/menu_assets.swf differ diff --git a/src/as/com/threerings/flex/CommandMenu.as b/src/as/com/threerings/flex/CommandMenu.as index 6d2913ef..b55ca2f2 100644 --- a/src/as/com/threerings/flex/CommandMenu.as +++ b/src/as/com/threerings/flex/CommandMenu.as @@ -10,6 +10,8 @@ import mx.core.mx_internal; import mx.core.ScrollPolicy; import mx.events.MenuEvent; +import com.dougmccune.controls.ScrollableArrowMenu; + import com.threerings.util.CommandEvent; /** @@ -30,7 +32,7 @@ import com.threerings.util.CommandEvent; * See "Defining menu structure and data" in the Flex manual for the * full list. */ -public class CommandMenu extends Menu +public class CommandMenu extends ScrollableArrowMenu { public function CommandMenu () {