/* Copyright (c) 2009 Yahoo! Inc. All rights reserved. The copyrights embodied in the content of this file are licensed under the BSD (revised) open source license */ package com.yahoo.astra.fl.containers.layoutClasses { import com.yahoo.astra.fl.utils.UIComponentUtil; import com.yahoo.astra.layout.ILayoutContainer; import com.yahoo.astra.layout.LayoutContainer; import com.yahoo.astra.layout.LayoutManager; import com.yahoo.astra.layout.events.LayoutEvent; import com.yahoo.astra.layout.modes.ILayoutMode; import com.yahoo.astra.utils.NumberUtil; import fl.containers.BaseScrollPane; import fl.controls.ScrollBar; import fl.controls.ScrollPolicy; import fl.core.InvalidationType; import fl.core.UIComponent; import fl.events.ComponentEvent; import flash.display.DisplayObject; import flash.display.DisplayObjectContainer; import flash.display.Shape; import flash.display.Sprite; import flash.events.Event; import flash.geom.Rectangle; import flash.utils.getDefinitionByName; /** * A variation on the ScrollPane container that accepts children * to be arranged using a layout algorithm. * * @see fl.containers.ScrollPane * * @author Josh Tynjala */ public class BaseLayoutPane extends BaseScrollPane { //-------------------------------------- // Static Properties //-------------------------------------- /** * @private * New invalidation type to capture when debug mode changes. */ protected static const INVALIDATION_TYPE_DEBUG_MODE:String = "debugModeInvalid"; /** * @private * New invalidation type to capture when something will affect the layout. */ protected static const INVALIDATION_TYPE_LAYOUT:String = "layoutInvalid"; /** * @private */ private static var defaultStyles:Object = { skin: Shape, //transparent background skin focusRectSkin: null, focusRectPadding: null, contentPadding: 0 //recommended to use padding properties of the layout mode, if available } //-------------------------------------- // Static Methods //-------------------------------------- /** * @copy fl.core.UIComponent#getStyleDefinition() * * @see fl.core.UIComponent#getStyle() UIComponent.getStyle() * @see fl.core.UIComponent#setStyle() UIComponent.setStyle() * @see fl.managers.StyleManager StyleManager */ public static function getStyleDefinition():Object { return mergeStyles(defaultStyles, BaseScrollPane.getStyleDefinition()); } /** * @private * Tell the LayoutManager which events from the Flash CS3 UIComponents * should trigger a layout update. */ private static function initializeLayoutEvents():void { LayoutManager.registerInvalidatingEvents(UIComponent, [ComponentEvent.MOVE, ComponentEvent.RESIZE]); try { //if the UILoader component is available, register Event.COMPLETE var uiLoader:Class = getDefinitionByName("fl.containers.UILoader") as Class; if(uiLoader) { LayoutManager.registerInvalidatingEvents(uiLoader, [Event.COMPLETE]); } } catch(error:Error) { //do nothing } } initializeLayoutEvents(); //-------------------------------------- // Constructor //-------------------------------------- /** * Constructor. * * @param mode An instance of an ILayoutMode implementation. */ public function BaseLayoutPane(mode:ILayoutMode = null) { super(); this.layoutMode = mode; } //-------------------------------------- // Properties //-------------------------------------- /** * The generic container used for the children. */ protected var layoutContainer:ILayoutContainer; /** * @private * Flag indicating that children added to the BaseLayoutPane * may actually be added to the layout container. */ private var _uiConfigured:Boolean = false; /** * @private * Storage for the layoutMode property. */ private var _layoutMode:ILayoutMode; /** * The algorithm used to layout children of this container (no default). */ public function get layoutMode():ILayoutMode { return this._layoutMode; } /** * @private */ public function set layoutMode(value:ILayoutMode):void { this._layoutMode = value; this.invalidate(INVALIDATION_TYPE_LAYOUT); } /** * @private * Indicates whether the width has been explicitly set. If not, the * stored value will be NaN. */ protected var explicitWidth:Number = NaN; /** * @private */ override public function set width(value:Number):void { this.setSize(value, this.explicitHeight); } /** * @private * Indicates whether the height has been explicitly set. If not, the * stored value will be NaN. */ protected var explicitHeight:Number = NaN; /** * @private */ override public function set height(value:Number):void { this.setSize(this.explicitWidth, value); } /** * @private * Overrode this because the original Adobe version led to infinite loops. */ override public function get horizontalPageScrollSize():Number { return (this._horizontalPageScrollSize == 0 && !isNaN(this.availableWidth)) ? this.availableWidth : this._horizontalPageScrollSize; } /** * @private * Overrode this because the original Adobe version led to infinite loops. */ override public function get verticalPageScrollSize():Number { return (this._verticalPageScrollSize == 0 && !isNaN(this.availableHeight)) ? this.availableHeight : this._verticalPageScrollSize; } /** * @private * If the UI has been configured, return the child count from the * ILayoutContainer instead. */ override public function get numChildren():int { if(!this._uiConfigured) { return super.numChildren; } var container:DisplayObjectContainer = DisplayObjectContainer(this.layoutContainer); return container.numChildren; } /** * @private * Storage for the autoSize property. */ private var _autoSize:Boolean = false; /** * If true, the container will automatically calculate the ideal width * and height for itself. Any attempts to set the width and height * values manually while autoSize is set to true will be ignored. * *

Note: If you want only one dimension to automatically resize, * autoSize must be false, and you should set the width or height * property to the value of NaN. */ public function get autoSize():Boolean { return this._autoSize; } /** * @private */ public function set autoSize(value:Boolean):void { this._autoSize = value; this.invalidate(InvalidationType.SIZE); } /** * @private * Used for displaying debug data. */ protected var debugCanvas:Sprite; /** * @private * Storage for the debug mode property. */ private var _debugMode:Boolean = false; /** * If true, a simple border around the layout pane will identify * its bounds. */ public function get debugMode():Boolean { return this._debugMode; } /** * @private */ public function set debugMode(value:Boolean):void { this._debugMode = value; this.invalidate(INVALIDATION_TYPE_DEBUG_MODE); } //-------------------------------------- // Public Methods //-------------------------------------- /** * @private * If the UI has been configured, then all children are added to the * ILayoutContainer instead. */ override public function addChild(child:DisplayObject):DisplayObject { if(!this._uiConfigured) { return super.addChild(child); } var container:DisplayObjectContainer = DisplayObjectContainer(this.layoutContainer); child = container.addChild(child); this.invalidate(INVALIDATION_TYPE_LAYOUT); return child; } /** * @private * If the UI has been configured, then all children are added to the * ILayoutContainer instead. */ override public function addChildAt(child:DisplayObject, index:int):DisplayObject { if(!this._uiConfigured) { return super.addChildAt(child, index); } var container:DisplayObjectContainer = DisplayObjectContainer(this.layoutContainer); child = container.addChildAt(child, index); this.invalidate(INVALIDATION_TYPE_LAYOUT); return child; } /** * @private * If the UI has been configured, then all children are removed from the * ILayoutContainer instead. */ override public function removeChild(child:DisplayObject):DisplayObject { if(!this._uiConfigured) { return super.removeChild(child); } var container:DisplayObjectContainer = DisplayObjectContainer(this.layoutContainer); child = container.removeChild(child); this.invalidate(INVALIDATION_TYPE_LAYOUT); return child; } /** * @private * If the UI has been configured, then all children are removed from the * ILayoutContainer instead. */ override public function removeChildAt(index:int):DisplayObject { if(!this._uiConfigured) { return super.removeChildAt(index); } var container:DisplayObjectContainer = DisplayObjectContainer(this.layoutContainer); var child:DisplayObject = container.removeChildAt(index); this.invalidate(INVALIDATION_TYPE_LAYOUT); return child; } /** * @private * If the UI has been configured, then all children are actually in the layoutContainer. */ override public function setChildIndex(child:DisplayObject, index:int):void { if(!this._uiConfigured) { super.setChildIndex(child, index); return; } var container:DisplayObjectContainer = DisplayObjectContainer(this.layoutContainer); container.setChildIndex(child, index); this.invalidate(INVALIDATION_TYPE_LAYOUT); } /** * @private * If the UI has been configured, then all children are actually in the layoutContainer. */ override public function getChildAt(index:int):DisplayObject { if(!this._uiConfigured) { return super.getChildAt(index); } var container:DisplayObjectContainer = DisplayObjectContainer(this.layoutContainer); return container.getChildAt(index); } /** * @private * If the UI has been configured, then all children are actually in the layoutContainer. */ override public function getChildByName(name:String):DisplayObject { if(!this._uiConfigured) { return super.getChildByName(name); } var container:DisplayObjectContainer = DisplayObjectContainer(this.layoutContainer); return container.getChildByName(name); } /** * @inheritDoc * *

Setting the width or height of the container to the value of NaN * will cause it to automatically determine the ideal size for that * dimension. Additionally, the autoSize property can be * set to true to force both the width and height values to * be automatically calculated based on the content. * * @see autoSize */ override public function setSize(w:Number, h:Number):void { this.explicitWidth = w; this.explicitHeight = h; //but we don't want NaN after this point. use the existing _width //and _height values with super.setSize() like Adobe already does. if(isNaN(w)) { w = this._width; } if(isNaN(h)) { h = this._height; } super.setSize(w, h); } //-------------------------------------- // Protected Methods //-------------------------------------- /** * @private */ override protected function configUI():void { //save the avatar because super.configUI() removes it if(this.numChildren > 0) { var avatar:DisplayObject = this.getChildAt(0); } super.configUI(); //if there was no avatar, then we can setSize() with NaN as width and height (for autosize). //if the user dragged this component on stage, then the size is explicitly set because that's //all Flash CS3 can do. if they made it programmatically, then there is no avatar. if(!avatar) { this.setSize(NaN, NaN); } if(!this.layoutContainer) { this.layoutContainer = new LayoutContainer(); LayoutContainer(this.layoutContainer).autoMask = false; this.layoutContainer.addEventListener(LayoutEvent.LAYOUT_CHANGE, layoutChangeHandler); var container:DisplayObject = DisplayObject(this.layoutContainer); container.scrollRect = contentScrollRect; container.visible = false; this.addChild(container); } if(!this.debugCanvas) { this.debugCanvas = new Sprite(); this.addChild(this.debugCanvas); } this._horizontalScrollPolicy = ScrollPolicy.AUTO; this._verticalScrollPolicy = ScrollPolicy.AUTO; //now all children can be added to the ILayoutContainer this._uiConfigured = true; } /** * @private */ override protected function draw():void { //fix for document class constructor/Event.RENDER bug in CS3 architecture DisplayObject(this.layoutContainer).visible = true; //we have to draw all children once before layout //to get initial sizing this.redrawUIComponentChildren(); //ensure that we only update the layout if the dimensions have changed //or if one of the layout properties has changed. this is very, very expensive! if(this.isInvalid(InvalidationType.SIZE, INVALIDATION_TYPE_LAYOUT)) { var oldWidth:Number = this.width; var oldHeight:Number = this.height; var container:DisplayObjectContainer = DisplayObjectContainer(this.layoutContainer); this.layoutContainer.layoutMode = this.layoutMode; //the first measurement will be based on explicit values or autosizing (with NaN) //this is the ideal sizing without scrollbars if(this.autoSize || isNaN(this.explicitWidth)) { container.width = NaN; } else { container.width = this.explicitWidth; } if(this.autoSize || isNaN(this.explicitHeight)) { container.height = NaN; } else { container.height = this.explicitHeight; } this.layoutContainer.validateLayout(); //floor it because Flash CS3 components are pixel constrained and that may //cause the scrollbars to appear when they should not! this.setContentSize(Math.floor(this.layoutContainer.contentWidth), Math.floor(this.layoutContainer.contentHeight)); //determine if scrollbars are needed this.calculateAvailableSize(); var loopCount:int = 0; do { //scrollbars may be present now that we're working with available dimensions //instead of explicit values. we loop in case the scrollbars change again. var oldAvailableWidth:Number = this.availableWidth; var oldAvailableHeight:Number = this.availableHeight; container.width = this.availableWidth; container.height = this.availableHeight; this.layoutContainer.validateLayout(); //if the width and height haven't been set explicitly, //the layout pane will resize to fit its contents (no scrollbars, obviously). if(isNaN(this.explicitWidth) || this.autoSize) { var generatedWidth:Number = this.layoutContainer.contentWidth; if(this.vScrollBar) { generatedWidth += ScrollBar.WIDTH; } this._width = Math.round(generatedWidth); } if(isNaN(this.explicitHeight) || this.autoSize) { var generatedHeight:Number = this.layoutContainer.contentHeight; if(this.hScrollBar) { generatedHeight += ScrollBar.WIDTH; } this._height = Math.round(generatedHeight); } this.setContentSize(Math.floor(this.layoutContainer.contentWidth), Math.floor(this.layoutContainer.contentHeight)); this.calculateAvailableSize(); loopCount++ } while((!NumberUtil.fuzzyEquals(oldAvailableWidth, this.availableWidth, 10) || !NumberUtil.fuzzyEquals(oldAvailableHeight, this.availableHeight)) && loopCount < 10) //if we've gone through this loop 10 times, it should be safe to assume that we need to kill an infinite loop //draw again after layout to ensure proper sizing this.redrawUIComponentChildren(); //draw a transparent background so that the mouse scrollwheel works //when the skin is empty. this.graphics.clear(); if(this.width > 0 && this.height > 0) { this.graphics.beginFill(0xff00ff, 0); this.graphics.drawRect(0, 0, this.width, this.height); this.graphics.endFill(); } //dispatch a resize event if we've grown or shrunk if(!NumberUtil.fuzzyEquals(oldWidth, this.width) || !NumberUtil.fuzzyEquals(oldHeight, this.height)) { this.dispatchEvent(new ComponentEvent(ComponentEvent.RESIZE)); } } if(this.debugMode && this.isInvalid(INVALIDATION_TYPE_DEBUG_MODE, InvalidationType.SIZE, INVALIDATION_TYPE_LAYOUT)) { this.debugCanvas.graphics.clear(); this.debugCanvas.graphics.lineStyle(1, 0xff00ff); this.debugCanvas.graphics.drawRect(0, 0, this.width, this.height); } this.debugCanvas.visible = this.debugMode; super.draw(); } /** * @private * Update the scrollRect of the ILayoutContainer */ override protected function drawLayout():void { super.drawLayout(); var container:DisplayObject = DisplayObject(this.layoutContainer); this.contentScrollRect = container.scrollRect; this.contentScrollRect.width = this.availableWidth; this.contentScrollRect.height = this.availableHeight; container.cacheAsBitmap = useBitmapScrolling; container.scrollRect = this.contentScrollRect; } /** * @private * Make sure the background isn't in the layout container */ override protected function drawBackground():void { var bg:DisplayObject = this.background; this.background = UIComponentUtil.getDisplayObjectInstance(this, this.getStyleValue("skin")); this.background.width = this.width; this.background.height = this.height; super.addChildAt(this.background,0); if(bg != null && bg != background) { super.removeChild(bg); } } /** * @private * Call drawNow() on any Flash UIComponent children this container holds. */ protected function redrawUIComponentChildren():void { var container:DisplayObjectContainer = DisplayObjectContainer(this.layoutContainer); for(var i:int = 0; i < container.numChildren; i++) { var child:UIComponent = container.getChildAt(i) as UIComponent; if(child) { child.drawNow(); } } } /** * @private * Update the scrollRect of the ILayoutContainer */ override protected function setVerticalScrollPosition(scrollPos:Number, fireEvent:Boolean=false):void { var container:DisplayObject = DisplayObject(this.layoutContainer); var contentScrollRect:Rectangle = container.scrollRect; contentScrollRect.y = scrollPos; container.scrollRect = contentScrollRect; } /** * @private * Update the scrollRect of the ILayoutContainer */ override protected function setHorizontalScrollPosition(scrollPos:Number, fireEvent:Boolean=false):void { var container:DisplayObject = DisplayObject(this.layoutContainer); var contentScrollRect:Rectangle = container.scrollRect; contentScrollRect.x = scrollPos; container.scrollRect = contentScrollRect; } /** * @private * Custom calculation of the available size */ override protected function calculateAvailableSize():void { super.calculateAvailableSize(); //if we're autosizing, available dimensions are the same as the content if(isNaN(this.explicitWidth) || this.autoSize) { this.availableWidth = this.layoutContainer.contentWidth; this.hScrollBar = false; } if(isNaN(this.explicitHeight) || this.autoSize) { this.availableHeight = this.layoutContainer.contentHeight; this.vScrollBar = false; } } //-------------------------------------- // Protected Event Handlers //-------------------------------------- /** * @private * If the layout has changed, we're probably in Flash Player's * render phase. If the UIComponent initiated it, this control * is drawing and we don't need to redraw. If the layout container * initiated it, then we should draw immediately because we * may not get a render event... */ protected function layoutChangeHandler(event:LayoutEvent):void { this.invalidate(INVALIDATION_TYPE_LAYOUT); } } }