MX ComboBox Open Direction Bug - Alternative Solution
Written by Simon Bailey
Here’s another ComboBox bug (plus potential feature) that irritated me no end with a fix being made difficult due to the relevent methods I needed access to in ComboBox.as declared as private (as so many are in mx components [bangs head]!!!).
Bug: ComboBox opens in a specific direction according to its current position on the stage, so the logic is if its too close to the bottom boundary and its content is of a particular height then it will open upwards. This does function but it is inconsistent and the result is a list being cropped with options therefore rendered inaccessible [bad]. Ideally we want to override this logic written in ComboBox.as but it’s private so you have absolutely no control over the direction the list opens.
Solution: Ditch the ComboBox and extend PopUpButton (as pointed out by the above link) applying some small modifications as I have done here http://gist.github.com/505255. What I have done completely removes the stage detection and enables you to declare whether the list opens upwards or downwards. It’s a simple solution so don’t expect bells and whistles and essentially fixes the problem I was having leaving you free to modify too your hearts content adding in any other functionality i.e. stage boundary detection etc.
package | |
{ | |
import flash.display.DisplayObject; | |
import flash.events.KeyboardEvent; | |
import flash.events.MouseEvent; | |
import flash.geom.Point; | |
import flash.geom.Rectangle; | |
import flash.ui.Keyboard; | |
import mx.controls.PopUpButton; | |
import mx.core.UIComponent; | |
import mx.core.UIComponentGlobals; | |
import mx.core.mx_internal; | |
import mx.effects.Tween; | |
import mx.events.InterManagerRequest; | |
import mx.managers.ISystemManager; | |
import mx.managers.PopUpManager; | |
use namespace mx_internal; | |
public class PopUpCombo extends PopUpButton | |
{ | |
protected var _isOpen:Boolean = false; | |
protected var _inTween:Boolean = false; | |
protected var _tween:Tween; | |
private var _specificOpenDirection:String; | |
public static const OPEN_UP:String = "OPEN_UP"; | |
public static const OPEN_DOWN:String = "OPEN_DOWN"; | |
public function PopUpCombo() | |
{ | |
super(); | |
} | |
public function get specificOpenDirection():String | |
{ | |
if(!_specificOpenDirection) _specificOpenDirection = PopUpCombo.OPEN_DOWN; | |
return _specificOpenDirection; | |
} | |
public function set specificOpenDirection(value:String):void | |
{ | |
_specificOpenDirection = value; | |
} | |
override protected function clickHandler(event:MouseEvent):void | |
{ | |
event.stopImmediatePropagation(); | |
displayPopUp(); | |
} | |
override protected function keyDownHandler(event:KeyboardEvent):void | |
{ | |
super.keyDownHandler(event); | |
if (event.ctrlKey && event.keyCode == Keyboard.DOWN) | |
{ | |
event.stopImmediatePropagation(); | |
displayPopUp(); | |
} | |
} | |
public function displayPopUp():void | |
{ | |
var show:Boolean = !_isOpen; | |
var popUpGap:Number = getStyle("popUpGap"); | |
var point:Point = new Point(0, unscaledHeight + popUpGap); | |
point = localToGlobal(point); | |
var initY:Number; | |
var endY:Number; | |
var easingFunction:Function; | |
var duration:Number; | |
if (popUp.parent == null) | |
{ | |
PopUpManager.addPopUp(popUp, this, false); | |
popUp.owner = this; | |
} | |
else | |
PopUpManager.bringToFront(popUp); | |
if(show) | |
{ | |
if(specificOpenDirection == PopUpCombo.OPEN_UP) | |
{ | |
point.y -= (unscaledHeight + popUp.height + 2*popUpGap); | |
initY = -popUp.height; | |
} | |
else | |
{ | |
initY = popUp.height; | |
} | |
point.x = Math.min( point.x, visibleScreen.right - popUp.getExplicitOrMeasuredWidth()); | |
point.x = Math.max( point.x, 0); | |
point = popUp.parent.globalToLocal(point); | |
if (popUp.x != point.x || popUp.y != point.y) | |
popUp.move(point.x, point.y); | |
popUp.scrollRect = new Rectangle(0, initY, | |
popUp.width, popUp.height); | |
if (!popUp.visible) | |
popUp.visible = true; | |
endY = 0; | |
_isOpen = show; | |
duration = getStyle("openDuration"); | |
easingFunction = getStyle("openEasingFunction") as Function; | |
} | |
else | |
{ | |
_isOpen = show; | |
if (popUp.parent == null) | |
return; | |
point = popUp.parent.globalToLocal(point); | |
endY = (specificOpenDirection == PopUpCombo.OPEN_UP) ? -popUp.height - 2 : popUp.height + 2; | |
initY = 0; | |
duration = getStyle("closeDuration") | |
easingFunction = getStyle("closeEasingFunction") as Function; | |
} | |
_inTween = true; | |
UIComponentGlobals.layoutManager.validateNow(); | |
// Block all layout, responses from web service, and other background | |
// processing until the tween finishes executing. | |
UIComponent.suspendBackgroundProcessing(); | |
_tween = new Tween(this, initY, endY, duration); | |
if (easingFunction != null) | |
_tween.easingFunction = easingFunction; | |
} | |
protected function get visibleScreen():Rectangle | |
{ | |
var sm:ISystemManager = systemManager.topLevelSystemManager; | |
var sbRoot:DisplayObject = sm.getSandboxRoot(); | |
var _screen:Rectangle; | |
if (sm != sbRoot) | |
{ | |
var request:InterManagerRequest = new InterManagerRequest(InterManagerRequest.SYSTEM_MANAGER_REQUEST, | |
false, false, | |
"getVisibleApplicationRect"); | |
sbRoot.dispatchEvent(request); | |
_screen = Rectangle(request.value); | |
} | |
else | |
_screen = sm.getVisibleApplicationRect(); | |
return _screen; | |
} | |
override mx_internal function onTweenUpdate(value:Number):void | |
{ | |
popUp.scrollRect = | |
new Rectangle(0, value, popUp.width, popUp.height); | |
} | |
override mx_internal function onTweenEnd(value:Number):void | |
{ | |
popUp.scrollRect = | |
new Rectangle(0, value, popUp.width, popUp.height); | |
_inTween = false; | |
UIComponent.resumeBackgroundProcessing(); | |
if (!_isOpen) | |
{ | |
popUp.visible = false; | |
popUp.scrollRect = null; | |
} | |
} | |
} | |
} |
<?xml version="1.0" encoding="utf-8"?> | |
<!-- http://blog.flexexamples.com/2008/02/11/determining-if-a-check-box-menu-item-is-toggled-in-a-flex-popupbutton-controls-pop-up-menu/ --> | |
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" | |
layout="vertical" | |
verticalAlign="middle" | |
backgroundColor="white" | |
xmlns:local="*"> | |
<mx:Script> | |
<![CDATA[ | |
import mx.controls.Alert; | |
import mx.controls.Menu; | |
import mx.events.DropdownEvent; | |
import mx.events.MenuEvent; | |
private var menu:Menu; | |
private function init():void { | |
menu = new Menu(); | |
menu.variableRowHeight = true; | |
menu.dataProvider = arr; | |
menu.addEventListener(MenuEvent.CHANGE, menu_change); | |
popUpButton.popUp = menu; | |
} | |
private function menu_change(evt:MenuEvent):void { | |
popUpButton.displayPopUp(); | |
} | |
]]> | |
</mx:Script> | |
<mx:Array id="arr"> | |
<mx:Object label="Option 1" | |
type="label"/> | |
<mx:Object label="Option 2" | |
type="label"/> | |
</mx:Array> | |
<local:PopUpCombo id="popUpButton" | |
openAlways="true" | |
label="Click to open..." | |
specificOpenDirection="{PopUpCombo.OPEN_UP}" | |
initialize="init();" /> | |
</mx:Application> | |