Return-Path: X-Original-To: apmail-incubator-flex-commits-archive@minotaur.apache.org Delivered-To: apmail-incubator-flex-commits-archive@minotaur.apache.org Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by minotaur.apache.org (Postfix) with SMTP id 343EADCD5 for ; Mon, 6 Aug 2012 21:28:15 +0000 (UTC) Received: (qmail 64027 invoked by uid 500); 6 Aug 2012 21:28:14 -0000 Delivered-To: apmail-incubator-flex-commits-archive@incubator.apache.org Received: (qmail 63999 invoked by uid 500); 6 Aug 2012 21:28:14 -0000 Mailing-List: contact flex-commits-help@incubator.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: flex-dev@incubator.apache.org Delivered-To: mailing list flex-commits@incubator.apache.org Received: (qmail 63987 invoked by uid 99); 6 Aug 2012 21:28:14 -0000 Received: from athena.apache.org (HELO athena.apache.org) (140.211.11.136) by apache.org (qpsmtpd/0.29) with ESMTP; Mon, 06 Aug 2012 21:28:14 +0000 X-ASF-Spam-Status: No, hits=-2000.0 required=5.0 tests=ALL_TRUSTED X-Spam-Check-By: apache.org Received: from [140.211.11.4] (HELO eris.apache.org) (140.211.11.4) by apache.org (qpsmtpd/0.29) with ESMTP; Mon, 06 Aug 2012 21:28:10 +0000 Received: from eris.apache.org (localhost [127.0.0.1]) by eris.apache.org (Postfix) with ESMTP id ADE0F2388C90; Mon, 6 Aug 2012 21:26:16 +0000 (UTC) Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit Subject: svn commit: r1370028 [39/43] - in /incubator/flex/whiteboard/cframpton/adobe.next: ./ frameworks/ frameworks/projects/advancedgrids/src/mx/collections/ frameworks/projects/advancedgrids/src/mx/controls/ frameworks/projects/airframework/src/mx/managers/... Date: Mon, 06 Aug 2012 21:26:02 -0000 To: flex-commits@incubator.apache.org From: cframpton@apache.org X-Mailer: svnmailer-1.0.8-patched Message-Id: <20120806212616.ADE0F2388C90@eris.apache.org> X-Virus-Checked: Checked by ClamAV on apache.org Added: incubator/flex/whiteboard/cframpton/adobe.next/frameworks/projects/spark/src/spark/components/gridClasses/GridViewLayout.as URL: http://svn.apache.org/viewvc/incubator/flex/whiteboard/cframpton/adobe.next/frameworks/projects/spark/src/spark/components/gridClasses/GridViewLayout.as?rev=1370028&view=auto ============================================================================== --- incubator/flex/whiteboard/cframpton/adobe.next/frameworks/projects/spark/src/spark/components/gridClasses/GridViewLayout.as (added) +++ incubator/flex/whiteboard/cframpton/adobe.next/frameworks/projects/spark/src/spark/components/gridClasses/GridViewLayout.as Mon Aug 6 21:25:54 2012 @@ -0,0 +1,3021 @@ +//////////////////////////////////////////////////////////////////////////////// +// +// Licensed to the Apache Software Foundation (ASF) under one or more +// contributor license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright ownership. +// The ASF licenses this file to You under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +package spark.components.gridClasses +{ +import flash.events.Event; +import flash.geom.Rectangle; +import flash.utils.Dictionary; +import flash.utils.getTimer; + +import mx.collections.IList; +import mx.core.ClassFactory; +import mx.core.IFactory; +import mx.core.IInvalidating; +import mx.core.IUITextField; +import mx.core.IVisualElement; +import mx.core.IVisualElementContainer; +import mx.core.Singleton; +import mx.core.mx_internal; +import mx.events.CollectionEvent; +import mx.events.CollectionEventKind; +import mx.events.PropertyChangeEvent; +import mx.managers.ILayoutManagerClient; +import mx.managers.LayoutManager; + +import spark.collections.SubListView; +import spark.components.DataGrid; +import spark.components.Grid; +import spark.components.supportClasses.GroupBase; +import spark.core.IGraphicElement; +import spark.layouts.supportClasses.DropLocation; +import spark.layouts.supportClasses.LayoutBase; + +use namespace mx_internal; + +[ExcludeClass] + +/** + * @private + * A virtual two dimensional layout for the Grid class. This is not a general purpose layout, + * it's only intended to be use with GridView. + */ +public class GridViewLayout extends LayoutBase +{ + include "../../core/Version.as"; + + //-------------------------------------------------------------------------- + // + // Variables + // + //-------------------------------------------------------------------------- + + /** + * @private + * The following variables define the visible part of the grid, where each item + * renderer typically displays dataProvider[rowIndex][columns[columnIndex]].dataField. + * The index vectors are sorted in increasing order but their items may not be + * sequential. + */ + private var visibleRowIndices:Vector. = new Vector.(0); + private var visibleColumnIndices:Vector. = new Vector.(0); + + /** + * @private + * The previous values of the corresponding variables. Set by layoutItemRenderers() + * and only valid during updateDisplayList(), for a complete relayout. + */ + private var oldVisibleRowIndices:Vector. = new Vector.(0); + private var oldVisibleColumnIndices:Vector. = new Vector.(0); + + /** + * TODO (hmuller): document how do these vectors relate to visibleRow,ColumnIndices + */ + private var visibleRowBackgrounds:Vector. = new Vector.(0); + private var visibleRowSeparators:Vector. = new Vector.(0); + private var visibleColumnSeparators:Vector. = new Vector.(0); + private var visibleItemRenderers:Vector. = new Vector.(0); + + /** + * @private + * TODO (hmuller): provide documentation + */ + private var hoverIndicator:IVisualElement = null; + private var caretIndicator:IVisualElement = null; + private var editorIndicator:IVisualElement = null; + + /** + * @private + * The bounding rectangle for all of the visible item renderers. Note that this + * rectangle may be larger than the scrollRect, since the first/last rows/columns + * of item renderers may only be partially visible. See scrollPositionChanged(). + */ + private const visibleItemRenderersBounds:Rectangle = new Rectangle(); + + /** + * @private + * The viewport's bounding rectangle; often smaller then visibleItemRenderersBounds. + * Initialized by updateDisplayList with the current scrollPosition, and grid.width,Height. + */ + private const visibleGridBounds:Rectangle = new Rectangle(); + + /** + * @private + * The elements available for reuse. Maps from an IFactory to a list of the elements + * that have been allocated by that factory and then freed. The list is represented + * by a Vector.. + * + * Updated by allocateGridElement(). + */ + private const freeElementMap:Dictionary = new Dictionary(); + + /** + * @private + * Records the IFactory used to allocate a Element so that free(Element) can find it again. + * + * Updated by createGridElement(). + */ + private const elementToFactoryMap:Dictionary = new Dictionary(); + + /** + * @private + * Used by scrollPositionChanged() to determine which scroll position properties changed. + */ + private var oldVerticalScrollPosition:Number = 0; + private var oldHorizontalScrollPosition:Number = 0; + + //-------------------------------------------------------------------------- + // + // Class methods and properties + // + //-------------------------------------------------------------------------- + + /** + * @private + * The static embeddedFontsRegistryExists property is initialized lazily. + */ + private static var _embeddedFontRegistryExists:Boolean = false; + private static var embeddedFontRegistryExistsInitialized:Boolean = false; + + /** + * @private + * True if an embedded font registry singleton exists. + */ + private static function get embeddedFontRegistryExists():Boolean + { + if (!embeddedFontRegistryExistsInitialized) + { + embeddedFontRegistryExistsInitialized = true; + try + { + _embeddedFontRegistryExists = Singleton.getInstance("mx.core::IEmbeddedFontRegistry") != null; + } + catch (e:Error) + { + _embeddedFontRegistryExists = false; + } + } + + return _embeddedFontRegistryExists; + } + + //-------------------------------------------------------------------------- + // + // Constructor + // + //-------------------------------------------------------------------------- + + /** + * Constructor. + * + * @langversion 3.0 + * @playerversion Flash 10 + * @playerversion AIR 2.4 + * @productversion Flex 4.5 + */ + public function GridViewLayout() + { + super(); + } + + //-------------------------------------------------------------------------- + // + // Properties + // + //-------------------------------------------------------------------------- + + /** + * @private + */ + private function dispatchChangeEvent(type:String):void + { + if (hasEventListener(type)) + dispatchEvent(new Event(type)); + } + + //---------------------------------- + // columnsView + //---------------------------------- + + private var _columnsView:SubListView = null; + + [Bindable("columnsViewChanged")] + + /** + * @default null + */ + public function get columnsView():SubListView + { + return _columnsView; + } + + /** + * @private + */ + public function set columnsView(value:SubListView):void + { + if (value == _columnsView) + return; + + _columnsView = value; + dispatchChangeEvent("columnsViewChanged"); + } + + //---------------------------------- + // dataProviderView + //---------------------------------- + + private var _dataProviderView:SubListView = null; + + [Bindable("dataProviderViewChanged")] + + /** + * @default null + */ + public function get dataProviderView():SubListView + { + return _dataProviderView; + } + + /** + * @private + */ + public function set dataProviderView(value:SubListView):void + { + if (value == _dataProviderView) + return; + + _dataProviderView = value; + dispatchChangeEvent("dataProviderViewChanged"); + } + + //---------------------------------- + // grid + //---------------------------------- + + private var _grid:Grid = null; + + /** + * @private + */ + public function get grid():Grid + { + return _grid; + } + + /** + * The Grid parent of this layout's target. This property is set by the Grid when the + * target GridView is added/removed from the Grid. + * + */ + public function set grid(value:Grid):void + { + if (_grid == value) + return; + + if (_grid) + { + _grid.removeEventListener("dataProviderChanged", grid_dataProviderChangedHandler); + _grid.removeEventListener("columnsChanged", grid_columnsChangedHandler); + } + + _grid = value; + + if (_grid) + { + dataProviderView = new SubListView(grid.dataProvider); + columnsView = new SubListView(grid.columns); + gridDimensionsView = new GridDimensionsView(grid.gridDimensions); + + _grid.addEventListener("dataProviderChanged", grid_dataProviderChangedHandler); + _grid.addEventListener("columnsChanged", grid_columnsChangedHandler); + } + else + { + dataProviderView = null; + columnsView = null; + gridDimensionsView = null; + } + } + + /** + * @private + * Called when the Grid's dataProvider property is set - not when the dataProvider itself changes. + */ + private function grid_dataProviderChangedHandler(ignored:Event):void + { + dataProviderView = new SubListView(grid.dataProvider); + dataProviderView.startIndex = viewRowIndex; + dataProviderView.count = viewRowCount; + } + + /** + * @private + * Called when the Grid's column property is set - not when columns are added/removed (etc). + */ + private function grid_columnsChangedHandler(ignored:Event):void + { + columnsView = new SubListView(grid.columns); + columnsView.startIndex = viewColumnIndex; + columnsView.count = viewColumnCount; + } + + //---------------------------------- + // gridDimensionsView + //---------------------------------- + + private var _gridDimensionsView:GridDimensionsView = null; + + [Bindable("gridDimensionsViewChanged")] + + /** + * @default null + */ + public function get gridDimensionsView():GridDimensionsView + { + return _gridDimensionsView; + } + + /** + * @private + */ + public function set gridDimensionsView(value:GridDimensionsView):void + { + if (value == _gridDimensionsView) + return; + + _gridDimensionsView = value; + dispatchChangeEvent("gridDimensionsViewChanged"); + } + + //---------------------------------- + // horizontalScrollingLocked + //---------------------------------- + + private var _horizontalScrollingLocked:Boolean = false; + + [Bindable("horizontalScrollingLockedChanged")] + + /** + * @default false + */ + public function get horizontalScrollingLocked():Boolean + { + return _horizontalScrollingLocked; + } + + /** + * @private + */ + public function set horizontalScrollingLocked(value:Boolean):void + { + if (value == _horizontalScrollingLocked) + return; + + _horizontalScrollingLocked = value; + dispatchChangeEvent("horizontalScrollingLockedChanged"); + } + + //---------------------------------- + // requestedColumnCount + //---------------------------------- + + private var _requestedColumnCount:int = 0; + + [Bindable("requestedColumnCountChanged")] + + /** + * @default 0 + */ + public function get requestedColumnCount():int + { + return _requestedColumnCount; + } + + /** + * @private + */ + public function set requestedColumnCount(value:int):void + { + if (value == _requestedColumnCount) + return; + + _requestedColumnCount = value; + dispatchChangeEvent("requestedColumnCountChanged"); + } + + //---------------------------------- + // requestedRowCount + //---------------------------------- + + private var _requestedRowCount:int = 0; + + [Bindable("requestedRowCountChanged")] + + /** + * @default 0 + */ + public function get requestedRowCount():int + { + return _requestedRowCount; + } + + /** + * @private + */ + public function set requestedRowCount(value:int):void + { + if (value == _requestedRowCount) + return; + + _requestedRowCount = value; + dispatchChangeEvent("requestedRowCountChanged"); + } + + //---------------------------------- + // useVirtualLayout (override) + //---------------------------------- + + /** + * GridLayout only supports virtual layout, the value of this property can not be changed. + * + * @return true. + * + * @langversion 3.0 + * @playerversion Flash 10 + * @playerversion AIR 2.0 + * @productversion Flex 4.5 + */ + override public function get useVirtualLayout():Boolean + { + return true; + } + + /** + * @private + */ + override public function set useVirtualLayout(value:Boolean):void + { + } + + //---------------------------------- + // verticalScrollingLocked + //---------------------------------- + + private var _verticalScrollingLocked:Boolean = false; + + [Bindable("verticalScrollingLockedChanged")] + + /** + * @default false + */ + public function get verticalScrollingLocked():Boolean + { + return _verticalScrollingLocked; + } + + /** + * @private + */ + public function set verticalScrollingLocked(value:Boolean):void + { + if (value == _verticalScrollingLocked) + return; + + _verticalScrollingLocked = value; + dispatchChangeEvent("verticalScrollingLockedChanged"); + } + + //---------------------------------- + // viewColumnCount + //---------------------------------- + + /** + * The number of columns displayed by the target GridView. + * + * @default -1 + */ + public function get viewColumnCount():int + { + return gridDimensionsView.viewColumnCount; + } + + /** + * @private + */ + public function set viewColumnCount(value:int):void + { + gridDimensionsView.viewColumnCount = value; + columnsView.count = value; + } + + //---------------------------------- + // viewColumnIndex + //---------------------------------- + + /** + * The column index origin of the grid region displayed by the target GridView. + * + * @default 0 + */ + public function get viewColumnIndex():int + { + return gridDimensionsView.viewColumnIndex; + } + + /** + * @private + */ + public function set viewColumnIndex(value:int):void + { + gridDimensionsView.viewColumnIndex = value; + columnsView.startIndex = value; + } + + //---------------------------------- + // viewRowCount + //---------------------------------- + + /** + * The number of rows displayed by the target GridView. + * + * @default -1 + */ + public function get viewRowCount():int + { + return gridDimensionsView.viewRowCount; + } + + /** + * @private + */ + public function set viewRowCount(value:int):void + { + gridDimensionsView.viewRowCount = value; + dataProviderView.count = value; + } + + //---------------------------------- + // viewRowIndex + //---------------------------------- + + /** + * The row index origin of the grid region displayed by the target GridView. + * + * @default 0 + */ + public function get viewRowIndex():int + { + return gridDimensionsView.viewRowIndex; + } + + /** + * @private + */ + public function set viewRowIndex(value:int):void + { + gridDimensionsView.viewRowIndex = value; + dataProviderView.startIndex = value; + } + + //-------------------------------------------------------------------------- + // + // Method Overrides + // + //-------------------------------------------------------------------------- + + /** + * Returns the index where a new item should be inserted if + * the user releases the mouse at the specified coordinates + * while completing a drag and drop gesture. + * + * Called by the calculatedDropLocation() method. + * + * @param x The x coordinate of the drag and drop gesture, in + * local coordinates. + * + * @param y The y coordinate of the drag and drop gesture, in + * the drop target's local coordinates. + * + * @return The drop index or -1 if the drop operation is not available + * at the specified coordinates. + * + * @see #calculateDropLocation() + * + * @langversion 3.0 + * @playerversion Flash 11 + * @playerversion AIR 3.0 + * @productversion Flex 5.0 + */ + override protected function calculateDropIndex(x:Number, y:Number):int + { + var rowIndex:int = gridDimensionsView.getRowIndexAt(x, y); + if (rowIndex == -1) + { + rowIndex = gridDimensionsView.rowCount; + } + else + { + // If we are closer to the next row then drop on the next row. + var bounds:Rectangle = gridDimensionsView.getRowBounds(rowIndex); + if (y > (bounds.y + (bounds.height / 2))) + rowIndex++; + } + + return rowIndex + gridDimensionsView.viewRowIndex; + } + + /** + * Calculates the bounds for the drop indicator that provides visual feedback + * to the user of where the items will be inserted at the end of a drag and drop + * gesture. + * + * Called by the showDropIndicator() method. + * + * @param dropLocation A valid DropLocation object previously returned + * by the calculateDropLocation() method. + * + * @return The bounds for the drop indicator or null. + * + * @see spark.layouts.supportClasses.DropLocation + * @see #calculateDropIndex() + * @see #calculateDragScrollDelta() + * + * @langversion 3.0 + * @playerversion Flash 11 + * @playerversion AIR 3.0 + * @productversion Flex 5.0 + */ + override protected function calculateDropIndicatorBounds(dropLocation:DropLocation):Rectangle + { + var rowIndex:int = gridDimensionsView.getRowIndexAt(dropLocation.dropPoint.x, dropLocation.dropPoint.y); + if (rowIndex == -1) + { + // If the last row is visible then put the drop indicator below the last row. + if (grid.dataProvider && isCellVisible(grid.dataProvider.length - 1, -1)) + rowIndex = grid.dataProvider.length - 1; + else + return null; + } + + var bounds:Rectangle = gridDimensionsView.getRowBounds(rowIndex); + + // If we are closer to the next row then put the drop indicator on + // the next row. + // TODO (dloverin): TBD how the drop indicator should be sized when + // rowGap is implemented. + // NOTE: The bounds indicator for the MX DataGrid start at the top of + // the cell. The top of the drag indicator was moved up by two pixels + // so it could be seen when the indicator bounds are below the last row. + // The issue is there may not be any space below the last row and the drop + // indicator would not be visible if it started at the top of the next cell. + if (dropLocation.dropPoint.y > (bounds.top + (bounds.height * 1) / 2)) + return new Rectangle(2, bounds.bottom - 2, bounds.width -4, 4); + + return new Rectangle(2, Math.max(0, bounds.y - 2), bounds.width - 4, 4); + } + + /** + * @private + * Clear everything. + */ + override public function clearVirtualLayoutCache():void + { + freeGridElements(visibleRowBackgrounds); + freeGridElements(visibleRowSeparators); + visibleRowIndices.length = 0; + + freeGridElements(visibleColumnSeparators); + visibleColumnIndices.length = 0; + + freeItemRenderers(visibleItemRenderers); + + clearSelectionIndicators(); + + freeGridElement(hoverIndicator) + hoverIndicator = null; + + freeGridElement(caretIndicator); + caretIndicator = null; + + freeGridElement(editorIndicator); + editorIndicator = null; + + visibleItemRenderersBounds.setEmpty(); + visibleGridBounds.setEmpty(); + } + + /** + * @private + * This version of the method uses gridDimensions to calcuate the bounds + * of the specified cell. The index is the cell's position in the row-major + * layout. + */ + override public function getElementBounds(index:int):Rectangle + { + const columnsLength:int = gridDimensionsView.columnCount; + if (columnsLength == -1) + return null; + + const rowIndex:int = index / columnsLength; + const columnIndex:int = index - (rowIndex * columnsLength); + return gridDimensionsView.getCellBounds(rowIndex, columnIndex); + } + + /** + * @private + */ + override protected function getElementBoundsAboveScrollRect(scrollRect:Rectangle):Rectangle + { + const y:int = Math.max(0, scrollRect.top - 1); + const rowIndex:int = gridDimensionsView.getRowIndexAt(scrollRect.x, y); + return gridDimensionsView.getRowBounds(rowIndex); + } + + /** + * @private + */ + override protected function getElementBoundsBelowScrollRect(scrollRect:Rectangle):Rectangle + { + const rowCount:int = gridDimensionsView.rowCount; + const maxY:int = Math.max(0, gridDimensionsView.getContentHeight(rowCount) - 1); + const y:int = Math.min(maxY, scrollRect.bottom + 1); + const rowIndex:int = gridDimensionsView.getRowIndexAt(scrollRect.x, y); + return gridDimensionsView.getRowBounds(rowIndex); + } + + /** + * @private + */ + override protected function getElementBoundsLeftOfScrollRect(scrollRect:Rectangle):Rectangle + { + const x:int = Math.max(0, scrollRect.left - 1); + const columnIndex:int = gridDimensionsView.getColumnIndexAt(x, scrollRect.y); + return gridDimensionsView.getColumnBounds(columnIndex); + } + + /** + * @private + */ + override protected function getElementBoundsRightOfScrollRect(scrollRect:Rectangle):Rectangle + { + const columnCount:int = gridDimensionsView.columnCount; + const maxX:int = Math.max(0, gridDimensionsView.getContentWidth(columnCount) - 1); + const x:int = Math.min(maxX, scrollRect.right + 1); + const columnIndex:int = gridDimensionsView.getColumnIndexAt(x, scrollRect.y); + return gridDimensionsView.getColumnBounds(columnIndex); + } + + /** + * @private + */ + override protected function scrollPositionChanged():void + { + if (!grid) + return; + + grid.hoverRowIndex = -1; + grid.hoverColumnIndex = -1; + + super.scrollPositionChanged(); // sets GridView's scrollRect + + const hspChanged:Boolean = oldHorizontalScrollPosition != horizontalScrollPosition; + const vspChanged:Boolean = oldVerticalScrollPosition != verticalScrollPosition; + + oldHorizontalScrollPosition = horizontalScrollPosition; + oldVerticalScrollPosition = verticalScrollPosition; + + // Only invalidate if we're clipping and rows and/or columns covered + // by the scrollR changes. If so, the visible row/column indicies need + // to be updated. + + var invalidate:Boolean = false; + + if (visibleRowIndices.length == 0 || visibleColumnIndices.length == 0) + invalidate = true; + + if (!invalidate && vspChanged) + { + const oldFirstRowIndex:int = visibleRowIndices[0]; + const oldLastRowIndex:int = visibleRowIndices[visibleRowIndices.length - 1]; + + const newFirstRowIndex:int = + gridDimensionsView.getRowIndexAt(horizontalScrollPosition, verticalScrollPosition); + const newLastRowIndex:int = + gridDimensionsView.getRowIndexAt(horizontalScrollPosition, verticalScrollPosition + target.height); + + if (oldFirstRowIndex != newFirstRowIndex || oldLastRowIndex != newLastRowIndex) + invalidate = true; + } + + if (!invalidate && hspChanged) + { + const oldFirstColIndex:int = visibleColumnIndices[0]; + const oldLastColIndex:int = visibleColumnIndices[visibleColumnIndices.length - 1]; + + const newFirstColIndex:int = + gridDimensionsView.getColumnIndexAt(horizontalScrollPosition, verticalScrollPosition); + const newLastColIndex:int = + gridDimensionsView.getColumnIndexAt(horizontalScrollPosition + target.width, verticalScrollPosition); + + if (oldFirstColIndex != newFirstColIndex || oldLastColIndex != newLastColIndex) + invalidate = true; + } + + if (invalidate) + { + var reason:String = "none"; + if (vspChanged && hspChanged) + reason = "bothScrollPositions"; + else if (vspChanged) + reason = "verticalScrollPosition" + else if (hspChanged) + reason = "horizontalScrollPosition"; + + grid.invalidateDisplayListFor(reason); + } + } + + /** + * @private + * Computes new values for the grid's measuredWidth,Height and + * measuredMinWidth,Height properties. + * + * If grid.requestedRowCount is GTE 0, then measuredHeight is estimated + * content height for as many rows. Otherwise the measuredHeight is the estimated + * content height for all rows. The measuredWidth calculation is similar. The + * measuredMinWidth,Height properties are also similar however if the corresponding + * requestedMin property isn't specified, then the measuredMin size is the same + * as the measured size. + */ + override public function measure():void + { + const gridView:GridView = target as GridView; // TBD: requestedRowCount should be a local property... + const grid:Grid = this.grid; + + if (!gridView || !grid) + return; + + updateTypicalCellSizes(); + + var measuredRowCount:int = requestedRowCount; + if (measuredRowCount == -1) + { + measuredRowCount = gridDimensionsView.rowCount; + if (grid.requestedMaxRowCount != -1) + measuredRowCount = Math.min(grid.requestedMaxRowCount, measuredRowCount); + if (grid.requestedMinRowCount != -1) + measuredRowCount = Math.max(grid.requestedMinRowCount, measuredRowCount); + } + + var measuredColumnCount:int = requestedColumnCount; + if (measuredColumnCount == -1) + { + measuredColumnCount = getColumnsLength(); + if (grid.requestedMinColumnCount != -1) + measuredColumnCount = Math.max(grid.requestedMinColumnCount, measuredColumnCount); + } + + var measuredWidth:Number = gridDimensionsView.getTypicalContentWidth(measuredColumnCount); + var measuredHeight:Number = gridDimensionsView.getTypicalContentHeight(measuredRowCount); + var measuredMinWidth:Number = gridDimensionsView.getTypicalContentWidth(grid.requestedMinColumnCount); + var measuredMinHeight:Number = gridDimensionsView.getTypicalContentHeight(grid.requestedMinRowCount); + + // Use Math.ceil() to make sure that if the content partially occupies + // the last pixel, we'll count it as if the whole pixel is occupied. + + target.measuredWidth = Math.ceil(measuredWidth); + target.measuredHeight = Math.ceil(measuredHeight); + target.measuredMinWidth = Math.ceil(measuredMinWidth); + target.measuredMinHeight = Math.ceil(measuredMinHeight); + } + + /** + * @private + */ + override public function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void + { + const grid:Grid = this.grid; + if (!grid) + return; + + // Find the index of the last GridColumn.visible==true column + + const columnsLength:int = gridDimensionsView.columnCount; + const lastVisibleColumnIndex:int = (columnsLength > 0) ? getPreviousVisibleColumnIndex(columnsLength) : -1; + if (lastVisibleColumnIndex < 0) + return; + + // Layers + + const backgroundLayer:GridLayer = getLayer("backgroundLayer"); + const selectionLayer:GridLayer = getLayer("selectionLayer"); + const editorIndicatorLayer:GridLayer = getLayer("editorIndicatorLayer"); + const rendererLayer:GridLayer = getLayer("rendererLayer"); + const overlayLayer:GridLayer = getLayer("overlayLayer"); + + // Relayout everything if the scroll position changed or if no + // "invalidateDisplayList reason" was specified. See + // Grid/invalidateDisplayListFor(reason) + + const completeLayoutNeeded:Boolean = + grid.isInvalidateDisplayListReason("verticalScrollPosition") || + grid.isInvalidateDisplayListReason("horizontalScrollPosition"); + + + // Layout the columns and item renderers; compute new values for visibleRowIndices et al. + + if (completeLayoutNeeded) + { + oldVisibleRowIndices = visibleRowIndices; + oldVisibleColumnIndices = visibleColumnIndices; + + // Determine the x/y position of the visible content. Note that the + // actual scroll positions may be negative. + + const scrollX:Number = Math.max(0, horizontalScrollPosition); + const scrollY:Number = Math.max(0, verticalScrollPosition); + + visibleGridBounds.x = scrollX; + visibleGridBounds.y = scrollY; + visibleGridBounds.width = unscaledWidth; + visibleGridBounds.height = unscaledHeight; + + layoutColumns(scrollX, scrollY, unscaledWidth); + layoutItemRenderers(rendererLayer, scrollX, scrollY, unscaledWidth, unscaledHeight); + + // Update the content size. Make sure that if the content spans partially + // over a pixel to the right/bottom, the content size includes the whole pixel. + + const columnCount:int = getColumnsLength(); + const rowCount:int = gridDimensionsView.rowCount; + + const contentWidth:Number = Math.ceil(gridDimensionsView.getContentWidth(columnCount)); + const contentHeight:Number = Math.ceil(gridDimensionsView.getContentHeight(rowCount)); + target.setContentSize(contentWidth, contentHeight); + + // If the grid's contentHeight is smaller than than the available height + // (unscaledHeight) then pad the visible rows + + var paddedRowCount:int = rowCount; + if ((scrollY == 0) && (contentHeight < unscaledHeight)) + { + const unusedHeight:Number = unscaledHeight - gridDimensionsView.getContentHeight(rowCount); + paddedRowCount += Math.ceil(unusedHeight / gridDimensionsView.defaultRowHeight); + } + + for (var rowIndex:int = rowCount; rowIndex < paddedRowCount; rowIndex++) + visibleRowIndices.push(rowIndex); + + // Layout the row backgrounds + + visibleRowBackgrounds = layoutLinearElements(grid.rowBackground, backgroundLayer, + visibleRowBackgrounds, oldVisibleRowIndices, visibleRowIndices, layoutRowBackground); + + // Layout the row and column separators. + + const lastRowIndex:int = paddedRowCount - 1; + + visibleRowSeparators = layoutLinearElements(grid.rowSeparator, overlayLayer, + visibleRowSeparators, oldVisibleRowIndices, visibleRowIndices, layoutRowSeparator, lastRowIndex); + + visibleColumnSeparators = layoutLinearElements(grid.columnSeparator, overlayLayer, + visibleColumnSeparators, oldVisibleColumnIndices, visibleColumnIndices, layoutColumnSeparator, lastVisibleColumnIndex); + + + // The old visible row,column indices are no longer needed + + oldVisibleRowIndices.length = 0; + oldVisibleColumnIndices.length = 0; + } + + // Layout the hoverIndicator, caretIndicator, and selectionIndicators + + if (completeLayoutNeeded || grid.isInvalidateDisplayListReason("hoverIndicator")) + layoutHoverIndicator(backgroundLayer); + + if (completeLayoutNeeded || grid.isInvalidateDisplayListReason("selectionIndicator")) + layoutSelectionIndicators(selectionLayer); + + if (completeLayoutNeeded || grid.isInvalidateDisplayListReason("caretIndicator")) + layoutCaretIndicator(overlayLayer); + + if (completeLayoutNeeded || grid.isInvalidateDisplayListReason("editorIndicator")) + layoutEditorIndicator(editorIndicatorLayer); + + if (!completeLayoutNeeded) + updateVisibleItemRenderers(); + + // To avoid flashing, force all of the layers to render now + + target.validateNow(); + } + + /** + * @private + * Reset the selected, showsCaret, and hovered properties for all visible item renderers. + * Run the prepare() method for renderers that have changed. + * + * This method is only called when the item renderers are not updated as part of a general + * redisplay, by layoutItemRenderers(). + */ + private function updateVisibleItemRenderers():void + { + const grid:Grid = grid; // avoid get method cost + const rowSelectionMode:Boolean = isRowSelectionMode(); + const cellSelectionMode:Boolean = isCellSelectionMode(); + + if (!rowSelectionMode && !cellSelectionMode) + return; + + for each (var renderer:IGridItemRenderer in visibleItemRenderers) + { + var rowIndex:int = renderer.rowIndex; // TBD: need grid-relative row,column indices here + var columnIndex:int = renderer.columnIndex; + + var oldSelected:Boolean = renderer.selected; + var oldShowsCaret:Boolean = renderer.showsCaret; + var oldHovered:Boolean = renderer.hovered; + + // The following initializations should match what's done in initializeItemRenderer() + if (rowSelectionMode) + { + renderer.selected = grid.selectionContainsIndex(rowIndex); + renderer.showsCaret = grid.caretRowIndex == rowIndex; + renderer.hovered = grid.hoverRowIndex == rowIndex; + } + else if (cellSelectionMode) + { + renderer.selected = grid.selectionContainsCell(rowIndex, columnIndex); + renderer.showsCaret = (grid.caretRowIndex == rowIndex) && (grid.caretColumnIndex == columnIndex); + renderer.hovered = (grid.hoverRowIndex == rowIndex) && (grid.hoverColumnIndex == columnIndex); + } + + if ((oldSelected != renderer.selected) || + (oldShowsCaret != renderer.showsCaret) || + (oldHovered != renderer.hovered)) + renderer.prepare(true); + } + } + + //-------------------------------------------------------------------------- + // + // Target GridView Access + // + //-------------------------------------------------------------------------- + + private function gridRowIndexToViewIndex(gridRowIndex:int):int + { + return (gridRowIndex == -1) ? -1 : gridRowIndex - viewRowIndex; + } + + private function gridColumnIndexToViewIndex(gridColumnIndex:int):int + { + return (gridColumnIndex == -1) ? -1 : gridColumnIndex - viewColumnIndex; + } + + private function getLayer(name:String):GridLayer + { + return target.getChildByName(name) as GridLayer; + + } + + /** + * @private + */ + private function getGridColumn(columnIndex:int):GridColumn + { + const columnsView:IList = columnsView; + if ((columnsView == null) || (columnIndex >= columnsView.length) || (columnIndex < 0)) + return null; + + return columnsView.getItemAt(columnIndex) as GridColumn; + } + + /** + * @private + */ + private function getColumnsLength():int + { + return (columnsView) ? columnsView.length : 0; + } + + + /** + * @private + * Returns the index of the next Grid.visible==true column + * after index. Returns -1 if there are no more visible columns. + * + * To find the first GridColumn.visible==true column index, use + * getNextVisibleColumnIndex(-1). + */ + private function getNextVisibleColumnIndex(index:int=-1):int + { + if (index < -1) + return -1; + + const columns:IList = columnsView; + const columnsLength:int = (columns) ? columns.length : 0; + + for (var i:int = index + 1; i < columnsLength; i++) + { + var column:GridColumn = columns.getItemAt(i) as GridColumn; + if (column && column.visible) + return i; + } + + return -1; + } + + /** + * @private + * Returns the index of the previous GridColumn.visible==true column + * before index. Returns -1 if there are no more visible columns. + * + * To find the last GridColumn.visible==true column index, use + * getPreviousVisibleColumnIndex(columns.length). + */ + private function getPreviousVisibleColumnIndex(index:int):int + { + const columns:IList = columnsView; + if (!columns || (index > columns.length)) + return -1; + + for (var i:int = index - 1; i >= 0; i--) + { + var column:GridColumn = columns.getItemAt(i) as GridColumn; + if (column && column.visible) + return i; + } + + return -1; + } + + /** + * @private + */ + private function getDataProviderItem(rowIndex:int):Object + { + const dataProviderView:IList = this.dataProviderView; + + if ((dataProviderView == null) || (rowIndex >= dataProviderView.length) || (rowIndex < 0)) + return null; + + return dataProviderView.getItemAt(rowIndex); + } + + + /** + * @private + */ + private function getDataProviderLength():int + { + const dataProviderView:IList = this.dataProviderView; + + return (dataProviderView) ? dataProviderView.length : -1; + } + + //-------------------------------------------------------------------------- + // + // Updating the GridDimensions' typicalCell sizes and columnWidths + // + //-------------------------------------------------------------------------- + + /** + * @private + * Return width clamped to the column's minWidth and maxWidth properties. + */ + private static function clampColumnWidth(width:Number, column:GridColumn):Number + { + const minColumnWidth:Number = column.minWidth; + const maxColumnWidth:Number = column.maxWidth; + + if (!isNaN(minColumnWidth)) + width = Math.max(width, minColumnWidth); + if (!isNaN(maxColumnWidth)) + width = Math.min(width, maxColumnWidth); + + return width; + } + + /** + * @private + * Use the specified GridColumn's itemRenderer (IFactory) to create a temporary + * item renderer. The returned item renderer must be freed, with freeGridElement(), + * and removed from the rendererLayer after it's used. + */ + private function createTypicalItemRenderer(columnIndex:int):IGridItemRenderer + { + const rendererLayer:GridLayer = getLayer("rendererLayer"); + if (!rendererLayer) + return null; + + var typicalItem:Object = grid.typicalItem; + if (typicalItem == null) + typicalItem = getDataProviderItem(0); + + const column:GridColumn = getGridColumn(columnIndex); + const factory:IFactory = itemToRenderer(column, typicalItem); + const renderer:IGridItemRenderer = allocateGridElement(factory) as IGridItemRenderer; + + rendererLayer.addElement(renderer); + + initializeItemRenderer(renderer, 0 /* rowIndex */, columnIndex, grid.typicalItem, false); + + // If the column's width isn't specified, then use the renderer's explicit + // width, if any. If that isn't specified, then use 4096, to avoid wrapping. + + var columnWidth:Number = column.width; + + if (isNaN(columnWidth)) + { + // Sadly, IUIComponent, UITextField, and UIFTETextField all have an + // explicitWidth property but do not share a common type. + if ("explicitWidth" in renderer) + columnWidth = Object(renderer).explicitWidth; + } + + // The default width of a UI[FTE]TextField is 100. If autoWrap is true, and + // multiline is true, the measured text will wrap if it is wider than + // the TextField's width. This is not what we want when measuring the + // width of typicalItem columns that lack an explicit column width. + + if (isNaN(columnWidth)) + columnWidth = 4096; + + layoutItemRenderer(renderer, 0, 0, columnWidth, NaN); + + return renderer; + } + + /** + * @private + * Update the typicalCellWidth,Height for all of the columns starting + * with x coordinate startX and column startIndex that fit within the + * specified width. Typical sizes are only updated if the current + * typical cell size is NaN. + * + * The typicalCellWidth for GridColumns with an explicit width, is just + * the explicit width. Otherwise an item renderer is created for the column + * and the item renderer's preferred bounds become the typical cell size. + */ + private function updateVisibleTypicalCellSizes(width:Number, scrollX:Number, firstVisibleColumnIndex:int):void + { + const rendererLayer:GridLayer = getLayer("rendererLayer"); + if (!rendererLayer) + return; + + const columnCount:int = getColumnsLength(); + const startCellX:Number = gridDimensionsView.getCellX(0 /* rowIndex */, firstVisibleColumnIndex); + const columnGap:int = gridDimensionsView.columnGap; + + for (var columnIndex:int = firstVisibleColumnIndex; + (width > 0) && (columnIndex >= 0) && (columnIndex < columnCount); + columnIndex = getNextVisibleColumnIndex(columnIndex)) + { + var cellHeight:Number = gridDimensionsView.getTypicalCellHeight(columnIndex); + var cellWidth:Number = gridDimensionsView.getTypicalCellWidth(columnIndex); + + var column:GridColumn = getGridColumn(columnIndex); + if (!isNaN(column.width)) + { + cellWidth = column.width; + gridDimensionsView.setTypicalCellWidth(columnIndex, cellWidth); + } + + if (isNaN(cellWidth) || isNaN(cellHeight)) + { + var renderer:IGridItemRenderer = createTypicalItemRenderer(columnIndex); + if (isNaN(cellWidth)) + { + cellWidth = clampColumnWidth(renderer.getPreferredBoundsWidth(), column); + gridDimensionsView.setTypicalCellWidth(columnIndex, cellWidth); + } + if (isNaN(cellHeight)) + { + cellHeight = renderer.getPreferredBoundsHeight(); + gridDimensionsView.setTypicalCellHeight(columnIndex, cellHeight); + } + + rendererLayer.removeElement(renderer); + freeGridElement(renderer); + } + + if (columnIndex == firstVisibleColumnIndex) + width -= startCellX + cellWidth - scrollX; + else + width -= cellWidth + columnGap; + } + } + + /** + * @private + * Used by the measure() method to initialize the GridDimensions typical width,height of + * requestedColumnCount columns, and the typical width of *all* columns with an explicit width. + */ + private function updateTypicalCellSizes():void + { + const rendererLayer:GridLayer = getLayer("rendererLayer"); + if (!rendererLayer) + return; + + const columnCount:int = getColumnsLength(); + const columnGap:int = gridDimensionsView.columnGap; + const requestedColumnCount:int = grid.requestedColumnCount; // TBD GridView... + var measuredColumnCount:int = 0; + + for (var columnIndex:int = 0; (columnIndex < columnCount); columnIndex++) + { + var cellHeight:Number = gridDimensionsView.getTypicalCellHeight(columnIndex); + var cellWidth:Number = gridDimensionsView.getTypicalCellWidth(columnIndex); + + var column:GridColumn = getGridColumn(columnIndex); + + // GridColumn.visible==false columns have a typical size of (0,0) + // to distinguish them from the GridColumn.visible==true columns + // that aren't in view yet. + + if (!column.visible) + { + gridDimensionsView.setTypicalCellWidth(columnIndex, 0); + gridDimensionsView.setTypicalCellHeight(columnIndex, 0); + continue; + } + + if (!isNaN(column.width)) + { + cellWidth = column.width; + gridDimensionsView.setTypicalCellWidth(columnIndex, cellWidth); + } + + var needTypicalRenderer:Boolean = (requestedColumnCount == -1) || (measuredColumnCount < requestedColumnCount); + if (needTypicalRenderer && (isNaN(cellWidth) || isNaN(cellHeight))) + { + var renderer:IGridItemRenderer = createTypicalItemRenderer(columnIndex); + if (isNaN(cellWidth)) + { + cellWidth = clampColumnWidth(renderer.getPreferredBoundsWidth(), column); + gridDimensionsView.setTypicalCellWidth(columnIndex, cellWidth); + } + if (isNaN(cellHeight)) + { + cellHeight = renderer.getPreferredBoundsHeight(); + gridDimensionsView.setTypicalCellHeight(columnIndex, cellHeight); + } + + rendererLayer.removeElement(renderer); + freeGridElement(renderer); + } + measuredColumnCount++; + } + } + + /** + * @private + * Update the column widths for the columns visible beginning at scrollX, that will fit + * within the specified width, or for all columns if width is NaN. The width of + * GridColumns that lack an explicit width is the preferred width of an item renderer + * for the grid's typicalItem. + * + * If width is specified and all columns are visible, then we'll increase the widths + * of GridDimensions columns for GridColumns without an explicit width so that all of + * the available space is consumed. + */ + private function layoutColumns(scrollX:Number, scrollY:Number, width:Number):void + { + var columnCount:int = gridDimensionsView.columnCount; + if (columnCount <= 0) + return; + + // Update the GridDimensions typicalCellWidth,Height values as needed. + + const firstVisibleColumnIndex:int = gridDimensionsView.getColumnIndexAt(scrollX, scrollY); + updateVisibleTypicalCellSizes(width, scrollX, firstVisibleColumnIndex); + + // Set the GridDimensions columnWidth for no more than columnCount columns. + + const columnGap:int = gridDimensionsView.columnGap; + const startCellX:Number = gridDimensionsView.getCellX(0 /* rowIndex */, firstVisibleColumnIndex); + var availableWidth:Number = width; + var flexibleColumnCount:uint = 0; + + for (var columnIndex:int = firstVisibleColumnIndex; + (availableWidth > 0) && (columnIndex >= 0) && (columnIndex < columnCount); + columnIndex = getNextVisibleColumnIndex(columnIndex)) + { + var columnWidth:Number = gridDimensionsView.getTypicalCellWidth(columnIndex); + var gridColumn:GridColumn = getGridColumn(columnIndex); + + if (isNaN(gridColumn.width)) // if this column's width wasn't explicitly specified + { + flexibleColumnCount += 1; + columnWidth = clampColumnWidth(columnWidth, gridColumn); + } + else + columnWidth = gridColumn.width; + + gridDimensionsView.setColumnWidth(columnIndex, columnWidth); // store the column width + + if (columnIndex == firstVisibleColumnIndex) + availableWidth -= startCellX + columnWidth - scrollX; + else + availableWidth -= columnWidth + columnGap; + } + + // If we haven't scrolled horizontally, and there's space left over, widen + // the columns whose GridColumn width isn't set explicitly, to fill the extra space. + + if ((scrollX != 0) || (availableWidth < 1.0) || (flexibleColumnCount == 0)) + return; + + const columnWidthDelta:Number = Math.ceil(availableWidth / flexibleColumnCount); + + for (columnIndex = firstVisibleColumnIndex; + (columnIndex >= 0) && (columnIndex < columnCount) && (availableWidth >= 1.0); + columnIndex = getNextVisibleColumnIndex(columnIndex)) + { + gridColumn = getGridColumn(columnIndex); + + if (isNaN(gridColumn.width)) // if this column's width wasn't explicitly specified + { + var oldColumnWidth:Number = gridDimensionsView.getColumnWidth(columnIndex); + columnWidth = oldColumnWidth + Math.min(availableWidth, columnWidthDelta); + columnWidth = clampColumnWidth(columnWidth, gridColumn); + gridDimensionsView.setColumnWidth(columnIndex, columnWidth); // store the column width + availableWidth -= (columnWidth - oldColumnWidth); + } + } + } + + //-------------------------------------------------------------------------- + // + // Item Renderer Management and Layout + // + //-------------------------------------------------------------------------- + + private const gridItemRendererClassFactories:Dictionary = new Dictionary(true); + + /** + * @private + * Return the item renderer for the specified column and dataProvider item, + * essentially column.itemToRenderer(dataItem). + * + * If this app might have embedded fonts then item renderers must be created with the Grid's + * module factory. To enable that, we wrap the real item renderer ClassFactory with + * a GridItemRendererClassFactory. Wrapped factories are cached in + * the gridItemRendererClassFactories Dictionary. + */ + private function itemToRenderer(column:GridColumn, dataItem:Object):IFactory + { + var factory:IFactory = column.itemToRenderer(dataItem); + var rendererClassFactory:IFactory = null; + + if (embeddedFontRegistryExists && (factory is ClassFactory)) + { + rendererClassFactory = gridItemRendererClassFactories[factory]; + if (!rendererClassFactory) + { + rendererClassFactory = new GridItemRendererClassFactory(grid, ClassFactory(factory)); + gridItemRendererClassFactories[factory] = rendererClassFactory; + } + } + + return (rendererClassFactory) ? rendererClassFactory : factory; + } + + private function layoutItemRenderers(rendererLayer:GridLayer, scrollX:Number, scrollY:Number, width:Number, height:Number):void + { + if (!rendererLayer) + return; + + var rowIndex:int; + var colIndex:int; + + const rowCount:int = gridDimensionsView.rowCount; + const colCount:int = getColumnsLength(); + const rowGap:int = gridDimensionsView.rowGap; + const colGap:int = gridDimensionsView.columnGap; + + // Compute the row,column index and bounds of the upper left "start" cell + + const startColIndex:int = gridDimensionsView.getColumnIndexAt(scrollX, scrollY); + const startRowIndex:int = gridDimensionsView.getRowIndexAt(scrollX, scrollY); + const startCellX:Number = gridDimensionsView.getCellX(startRowIndex, startColIndex); + const startCellY:Number = gridDimensionsView.getCellY(startRowIndex, startColIndex); + + // Compute newVisibleColumns + + const newVisibleColumnIndices:Vector. = new Vector.(); + var availableWidth:Number = width; + var column:GridColumn; + + for (colIndex = startColIndex; + (availableWidth > 0) && (colIndex >= 0) && (colIndex < colCount); + colIndex = getNextVisibleColumnIndex(colIndex)) + { + newVisibleColumnIndices.push(colIndex); + var columnWidth:Number = gridDimensionsView.getColumnWidth(colIndex); + if (colIndex == startColIndex) + availableWidth -= startCellX + columnWidth - scrollX; + else + availableWidth -= columnWidth + colGap; + } + + // compute newVisibleRowIndices, newVisibleItemRenderers, layout item renderers + + const newVisibleRowIndices:Vector. = new Vector.(); + const newVisibleItemRenderers:Vector. = new Vector.(); + + var cellX:Number = startCellX; + var cellY:Number = startCellY; + var availableHeight:Number = height; + + for (rowIndex = startRowIndex; (availableHeight > 0) && (rowIndex >= 0) && (rowIndex < rowCount); rowIndex++) + { + newVisibleRowIndices.push(rowIndex); + + var rowHeight:Number = gridDimensionsView.getRowHeight(rowIndex); + for each (colIndex in newVisibleColumnIndices) + { + var renderer:IGridItemRenderer = takeVisibleItemRenderer(rowIndex, colIndex); + if (!renderer) + { + var dataItem:Object = getDataProviderItem(rowIndex); + column = getGridColumn(colIndex); + var factory:IFactory = itemToRenderer(column, dataItem); + renderer = allocateGridElement(factory) as IGridItemRenderer; + } + if (renderer.parent != rendererLayer) + rendererLayer.addElement(renderer); + newVisibleItemRenderers.push(renderer); + + initializeItemRenderer(renderer, rowIndex, colIndex); + + var colWidth:Number = gridDimensionsView.getColumnWidth(colIndex); + layoutItemRenderer(renderer, cellX, cellY, colWidth, rowHeight); + + var preferredRowHeight:Number = renderer.getPreferredBoundsHeight() + gridDimensionsView.setCellHeight(rowIndex, colIndex, preferredRowHeight); + cellX += colWidth + colGap; + } + + // If gridDimensions.rowHeight is now larger, we need to make another + // pass to fix up the item renderer heights. + + const finalRowHeight:Number = gridDimensionsView.getRowHeight(rowIndex); + if (rowHeight != finalRowHeight) + { + const visibleColumnsLength:int = newVisibleColumnIndices.length; + rowHeight = finalRowHeight; + for each (colIndex in newVisibleColumnIndices) + { + var rowOffset:int = newVisibleRowIndices.indexOf(rowIndex); + var colOffset:int = newVisibleColumnIndices.indexOf(colIndex); + var index:int = (rowOffset * visibleColumnsLength) + colOffset; + renderer = newVisibleItemRenderers[index]; + + // We're using layoutBoundsX,Y,Width instead of x,y.width because + // the IUITextField item renderers pad their x,y,width,height properties + var rendererX:Number = renderer.getLayoutBoundsX(); + var rendererY:Number = renderer.getLayoutBoundsY(); + var rendererWidth:Number = renderer.getLayoutBoundsWidth(); + + layoutItemRenderer(renderer, rendererX, rendererY, rendererWidth, rowHeight); + gridDimensionsView.setCellHeight(rowIndex, colIndex, renderer.getPreferredBoundsHeight()); + } + } + + cellX = startCellX; + cellY += rowHeight + rowGap; + + if (rowIndex == startRowIndex) + availableHeight -= startCellY + rowHeight - scrollY; + else + availableHeight -= rowHeight + rowGap; + } + + // Free renderers that aren't in use + + for each (var oldRenderer:IGridItemRenderer in visibleItemRenderers) + freeItemRenderer(oldRenderer); + + // Update visibleItemRenderersBounds + + if ((newVisibleRowIndices.length > 0) && (newVisibleColumnIndices.length > 0)) + { + const lastRowIndex:int = newVisibleRowIndices[newVisibleRowIndices.length - 1]; + const lastColIndex:int = newVisibleColumnIndices[newVisibleColumnIndices.length - 1]; + const lastCellR:Rectangle = gridDimensionsView.getCellBounds(lastRowIndex, lastColIndex); + + visibleItemRenderersBounds.x = startCellX; + visibleItemRenderersBounds.y = startCellY; + visibleItemRenderersBounds.width = lastCellR.x + lastCellR.width - startCellX; + visibleItemRenderersBounds.height = lastCellR.y + lastCellR.height - startCellY; + } + else + { + visibleItemRenderersBounds.setEmpty(); + } + + // Update visibleItemRenderers et al + + visibleItemRenderers = newVisibleItemRenderers; + visibleRowIndices = newVisibleRowIndices; + visibleColumnIndices = newVisibleColumnIndices; + } + + /** + * Reinitialize and layout the visible renderer at rowIndex, columnIndex. If the cell's preferred + * height changes and the Grid has been configured with variableRowHeight=true, the entire grid is + * invalidated. + * + *

If row,columnIndex do not correspond to a visible cell, nothing is done.

+ * + * @param rowIndex The 0-based row index of the cell that changed. + * @param columnIndex The 0-based column index of the cell that changed. + */ + public function invalidateCell(rowIndex:int, columnIndex:int):void + { + const renderer:IGridItemRenderer = getVisibleItemRenderer(rowIndex, columnIndex); + if (!renderer) + return; + + // If the renderer at rowIndex,columnIndex is going to have to be replaced, because + // this columns itemRendererFunction now returns a different (IFactory) value, punt. + + if (itemRendererFunctionValueChanged(renderer)) + { + renderer.grid.invalidateDisplayList(); + return; + } + + initializeItemRenderer(renderer, rowIndex, columnIndex); + + // We're using layoutBoundsX,Y,Width,Height instead of x,y,width,height because + // the IUITextField item renderers pad their x,y,width,height properties + + const rendererX:Number = renderer.getLayoutBoundsX(); + const rendererY:Number = renderer.getLayoutBoundsY(); + const rendererWidth:Number = renderer.getLayoutBoundsWidth(); + const rendererHeight:Number = renderer.getLayoutBoundsHeight(); + + layoutItemRenderer(renderer, rendererX, rendererY, rendererWidth, rendererHeight); + + // If the renderer's preferredHeight has changed and variableRowHeight=true, then + // the row's height may have changed, which implies we need to layout -everything-. + // Warning: the unconditional getPreferredBoundsHeight() call also serves to + // force DefaultGridItemRenderer and UITextFieldGridItemRenderer to validate; + // similar to what happens in layoutItemRenderers() and updateTypicalCellSizes() + + const preferredRendererHeight:Number = renderer.getPreferredBoundsHeight(); + if (gridDimensionsView.variableRowHeight && (rendererHeight != preferredRendererHeight)) + grid.invalidateDisplayList(); + } + + /** + * @private + * Return true if the specified item renderer was defined by an itemRendererFunction whose + * value has changed. + */ + private function itemRendererFunctionValueChanged(renderer:IGridItemRenderer):Boolean + { + const column:GridColumn = renderer.column; + if (!column || (column.itemRendererFunction === null)) + return false; + + const factory:IFactory = itemToRenderer(column, renderer.data); + return factory !== elementToFactoryMap[renderer]; + } + + /** + * @private + */ + private function getVisibleItemRendererIndex(rowIndex:int, columnIndex:int):int + { + if ((visibleRowIndices == null) || (visibleColumnIndices == null)) + return -1; + + // TODO (hmuller) - binary search would be faster than indexOf() + + const rowOffset:int = visibleRowIndices.indexOf(rowIndex); + const colOffset:int = visibleColumnIndices.indexOf(columnIndex); + if ((rowOffset == -1) || (colOffset == -1)) + return -1; + + const index:int = (rowOffset * visibleColumnIndices.length) + colOffset; + return index; + } + + /** + * Return the visible item renderer at the specified GridView row,columnIndex. + */ + public function getVisibleItemRenderer(rowIndex:int, columnIndex:int):IGridItemRenderer + { + const index:int = getVisibleItemRendererIndex(rowIndex, columnIndex); + if (index == -1 || index >= visibleItemRenderers.length) + return null; + + const renderer:IGridItemRenderer = visibleItemRenderers[index]; + return renderer; + } + + /** + * @private + */ + private function takeVisibleItemRenderer(rowIndex:int, columnIndex:int):IGridItemRenderer + { + const index:int = getVisibleItemRendererIndex(rowIndex, columnIndex); + if (index == -1 || index >= visibleItemRenderers.length) + return null; + + const renderer:IGridItemRenderer = visibleItemRenderers[index]; + visibleItemRenderers[index] = null; + + // If the renderer at rowIndex,columnIndex is going to have to be replaced, because + // this column's itemRendererFunction now returns a different (IFactory) value, then + // get rid of the old one and return null. + + if (renderer && itemRendererFunctionValueChanged(renderer)) + { + freeItemRenderer(renderer); + return null; + } + + return renderer; + } + + /** + * @private + */ + private function initializeItemRenderer( + renderer:IGridItemRenderer, + rowIndex:int, columnIndex:int, + dataItem:Object=null, + visible:Boolean=true):void + { + renderer.visible = visible; + + const gridColumn:GridColumn = getGridColumn(columnIndex); + if (gridColumn) + { + renderer.rowIndex = rowIndex; + renderer.column = gridColumn; + if (dataItem == null) + dataItem = getDataProviderItem(rowIndex); + + renderer.label = gridColumn.itemToLabel(dataItem); + + // The following code must be kept in sync with updateVisibleItemRenderers() + if (isRowSelectionMode()) + { + renderer.selected = grid.selectionContainsIndex(rowIndex); + renderer.showsCaret = grid.caretRowIndex == rowIndex; + renderer.hovered = grid.hoverRowIndex == rowIndex; + } + else if (isCellSelectionMode()) + { + renderer.selected = grid.selectionContainsCell(rowIndex, columnIndex); + renderer.showsCaret = (grid.caretRowIndex == rowIndex) && (grid.caretColumnIndex == columnIndex); + renderer.hovered = (grid.hoverRowIndex == rowIndex) && (grid.hoverColumnIndex == columnIndex); + } + + renderer.data = dataItem; + + if (grid.dataGrid) + renderer.owner = grid.dataGrid; + + renderer.prepare(!createdGridElement); + } + } + + private function freeItemRenderer(renderer:IGridItemRenderer):void + { + if (!renderer) + return; + + freeGridElement(renderer); + renderer.discard(true); + } + + private function freeItemRenderers(renderers:Vector.):void + { + for each (var renderer:IGridItemRenderer in renderers) + freeItemRenderer(renderer); + renderers.length = 0; + } + + //-------------------------------------------------------------------------- + // + // Linear elements: row,column separators, backgrounds + // + //-------------------------------------------------------------------------- + + /** + * @private + * Common code for laying out the rowBackround, rowSeparator, columnSeparator visual elements. + * + * For row,columnSeparators, lastIndex identifies the element in the new layout for which + * no separator is drawn. If the previous layout - oldVisibleIndices - included the lastIndex, + * it needs to be freed, even though it exists in the new layout (newVisibleIndices). See + * freeLinearElements(). + */ + private function layoutLinearElements( + factory:IFactory, + layer:GridLayer, + oldVisibleElements:Vector., + oldVisibleIndices:Vector., + newVisibleIndices:Vector., + layoutFunction:Function, + lastIndex:int = -1):Vector. + { + if (!layer) + return new Vector.(0); + + // If a factory changed, free the old visual elements and set oldVisibleElements.length=0 + + discardGridElementsIfFactoryChanged(factory, layer, oldVisibleElements); + + if (factory == null) + return new Vector.(0); + + // Free and clear oldVisibleElements that are no long visible + + freeLinearElements(oldVisibleElements, oldVisibleIndices, newVisibleIndices, lastIndex); + + // Create, layout, and return newVisibleElements + + const newVisibleElementCount:uint = newVisibleIndices.length; + const newVisibleElements:Vector. = new Vector.(newVisibleElementCount); + + for (var index:int = 0; index < newVisibleElementCount; index++) + { + var newEltIndex:int = newVisibleIndices[index]; + if (newEltIndex == lastIndex) + { + newVisibleElements.length = index; + break; + } + + // If an element already exists for visibleIndex then use it, otherwise create one + + var eltOffset:int = oldVisibleIndices.indexOf(newEltIndex); + var elt:IVisualElement = (eltOffset != -1 && eltOffset < oldVisibleElements.length) ? oldVisibleElements[eltOffset] : null; + if (elt == null) + elt = allocateGridElement(factory); + + // Initialize the element, and then delegate to the layout function + + newVisibleElements[index] = elt; + + layer.addElement(elt); + + elt.visible = true; + + layoutFunction(elt, newEltIndex); + } + + return newVisibleElements; + } + + private function layoutCellElements( + factory:IFactory, + layer:GridLayer, + oldVisibleElements:Vector., + oldVisibleRowIndices:Vector., oldVisibleColumnIndices:Vector., + newVisibleRowIndices:Vector., newVisibleColumnIndices:Vector., + layoutFunction:Function):Vector. + { + if (!layer) + return new Vector.(0); + + // If a factory changed, discard the old visual elements. + + if (discardGridElementsIfFactoryChanged(factory, layer, oldVisibleElements)) + { + oldVisibleRowIndices.length = 0; + oldVisibleColumnIndices.length = 0; + } + + if (factory == null) + return new Vector.(0); + + // Create, layout, and return newVisibleElements + + const newVisibleElementCount:uint = newVisibleRowIndices.length; + const newVisibleElements:Vector. = new Vector.(newVisibleElementCount); + + // Free and clear oldVisibleElements that are no long visible. + + freeCellElements(oldVisibleElements, newVisibleElements, + oldVisibleRowIndices, newVisibleRowIndices, + oldVisibleColumnIndices, newVisibleColumnIndices); + + for (var index:int = 0; index < newVisibleElementCount; index++) + { + var newEltRowIndex:int = newVisibleRowIndices[index]; + var newEltColumnIndex:int = newVisibleColumnIndices[index]; + + // If an element already exists for visibleIndex then use it, + // otherwise create one. + + var elt:IVisualElement = newVisibleElements[index]; + if (elt === null) + { + // Initialize the element, and then delegate to the layout + // function. + elt = allocateGridElement(factory); + newVisibleElements[index] = elt; + } + + layer.addElement(elt); + + elt.visible = true; + + layoutFunction(elt, newEltRowIndex, newEltColumnIndex); + } + + return newVisibleElements; + } + + /** + * @private + * If the factory has changed, or is now null, remove and free all the old + * visual elements, if there were any. + * + * @returns True if at least one visual element was removed. + */ + private function discardGridElementsIfFactoryChanged( + factory:IFactory, + layer:GridLayer, + oldVisibleElements:Vector.):Boolean + { + if ((oldVisibleElements.length) > 0 && (factory != elementToFactoryMap[oldVisibleElements[0]])) + { + for each (var oldElt:IVisualElement in oldVisibleElements) + { + layer.removeElement(oldElt); + freeGridElement(oldElt); + } + oldVisibleElements.length = 0; + return true; + } + + return false; + } + + /** + * @private + * Free each member of elements if the corresponding member of oldIndices doesn't + * appear in newIndices. Both vectors of indices must have been sorted in increasing + * order. When an element is freed, the corresponding member of the vector parameter + * is set to null. + * + * This method is [supposed to be a] somewhat more efficient implementation of the following: + * + * for (var i:int = 0; i < elements.length; i++) + * { + * if ((oldIndices[i] == lastIndex) || (newIndices.indexOf(oldIndices[i]) == -1)) + * freeGridElement(elements[i]); + * elements[i] = null; + * } + * + * The lastIndex parameter is used to handle row and column separators, where the last + * element is left out since separators only appear in between elements. If the lastIndex + * appears in oldIndices, we're not going to need the old element. + */ + private function freeLinearElements ( + elements:Vector., + oldIndices:Vector., + newIndices:Vector., + lastIndex:int):void + { + // TODO(hmuller): rewrite this, should be one pass (no indexOf) + for (var i:int = 0; i < elements.length; i++) + { + const offset:int = newIndices.indexOf(oldIndices[i]); + if ((oldIndices[i] == lastIndex) || (offset == -1)) + { + const elt:IVisualElement = elements[i]; + if (elt) + { + freeGridElement(elt); + elements[i] = null; + } + } + } + } + + private function freeCellElements ( + elements:Vector., newElements:Vector., + oldRowIndices:Vector., newRowIndices:Vector., + oldColumnIndices:Vector., newColumnIndices:Vector.):void + { + var freeElement:Boolean = true; + + // assumes newRowIndices.length == newColumnIndices.length + const numNewCells:int = newRowIndices.length; + var newIndex:int = 0; + + for (var i:int = 0; i < elements.length; i++) + { + const elt:IVisualElement = elements[i]; + if (elt == null) + continue; + + // assumes oldIndices.length == elements.length + const oldRowIndex:int = oldRowIndices[i]; + const oldColumnIndex:int = oldColumnIndices[i]; + + for ( ; newIndex < numNewCells; newIndex++) + { + const newRowIndex:int = newRowIndices[newIndex]; + const newColumnIndex:int = newColumnIndices[newIndex]; + + if (newRowIndex == oldRowIndex) + { + if (newColumnIndex == oldColumnIndex) + { + // Same cell still selected so reuse the selection. + // Save it in the correct place in newElements. That + // way we know its location based on + // newRowIndices[newIndex], newColumnIndices[newIndex]. + newElements[newIndex] = elt; + freeElement = false; + break; + } + else if (newColumnIndex > oldColumnIndex) + { + // not found + break; + } + } + else if (newRowIndex > oldRowIndex) + { + // not found + break; + } + } + + if (freeElement) + freeGridElement(elt); + + freeElement = true; + } + + elements.length = 0; + } + + private function layoutRowBackground(rowBackground:IVisualElement, rowIndex:int):void + { + const rowCount:int = gridDimensionsView.rowCount; + const bounds:Rectangle = (rowIndex < rowCount) + ? gridDimensionsView.getRowBounds(rowIndex) + : gridDimensionsView.getPadRowBounds(rowIndex); + + if (!bounds) + return; + + if ((rowIndex < rowCount) && (bounds.width == 0)) // implies no columns + bounds.width = visibleGridBounds.width; + + // Initialize this visual element + intializeGridVisualElement(rowBackground, rowIndex); + + layoutGridElementR(rowBackground, bounds); + } + + private function layoutRowSeparator(separator:IVisualElement, rowIndex:int):void + { + // Initialize this visual element + intializeGridVisualElement(separator, rowIndex); + + const height:Number = separator.getPreferredBoundsHeight(); + const rowCount:int = gridDimensionsView.rowCount; + const bounds:Rectangle = (rowIndex < rowCount) + ? gridDimensionsView.getRowBounds(rowIndex) + : gridDimensionsView.getPadRowBounds(rowIndex); + + if (!bounds) + return; + + const x:Number = bounds.x; + const width:Number = Math.max(bounds.width, visibleGridBounds.right); + const y:Number = bounds.bottom; // TODO (klin): should center on gap here. + layoutGridElement(separator, x, y, width, height); + } + + private function layoutColumnSeparator(separator:IVisualElement, columnIndex:int):void + { + // Initialize this visual element + intializeGridVisualElement(separator, -1, columnIndex); + + const r:Rectangle = visibleItemRenderersBounds; + const width:Number = separator.getPreferredBoundsWidth(); + const height:Number = Math.max(r.height, visibleGridBounds.height); + const x:Number = gridDimensionsView.getCellX(0, columnIndex) + gridDimensionsView.getColumnWidth(columnIndex); // TODO (klin): should center on gap here. + const y:Number = r.y; + layoutGridElement(separator, x, y, width, height); + } + + //-------------------------------------------------------------------------- + // + // Selection Indicators + // + //-------------------------------------------------------------------------- + + private var visibleSelectionIndicators:Vector. = new Vector.(0); + private var visibleRowSelectionIndices:Vector. = new Vector.(0); + private var visibleColumnSelectionIndices:Vector. = new Vector.(0); + + private function isRowSelectionMode():Boolean + { + const mode:String = grid.selectionMode; + return (mode == GridSelectionMode.SINGLE_ROW) || (mode == GridSelectionMode.MULTIPLE_ROWS); + } + + private function isCellSelectionMode():Boolean + { + const mode:String = grid.selectionMode; + return mode == (GridSelectionMode.SINGLE_CELL) || (mode == GridSelectionMode.MULTIPLE_CELLS); + } + + private function layoutSelectionIndicators(layer:GridLayer):void + { + const selectionIndicatorFactory:IFactory = grid.selectionIndicator; + const viewRowIndex:int = viewRowIndex; + const viewColumnIndex:int = viewColumnIndex; + + // layout and update visibleSelectionIndicators,Indices + + if (isRowSelectionMode()) + { + // Selection is row-based so if there are existing cell selections, + // free them since they can't be reused. + if (visibleColumnSelectionIndices.length > 0) + clearSelectionIndicators(); + + var oldVisibleRowSelectionIndices:Vector. = visibleRowSelectionIndices; + + // Load this up with the currently selected rows. + visibleRowSelectionIndices = new Vector.(); + + for each (var rowIndex:int in visibleRowIndices) + { + if (grid.selectionContainsIndex(rowIndex + viewRowIndex)) + { + visibleRowSelectionIndices.push(rowIndex); + } + } + + // Display the row selections. + visibleSelectionIndicators = layoutLinearElements( + selectionIndicatorFactory, + layer, + visibleSelectionIndicators, + oldVisibleRowSelectionIndices, + visibleRowSelectionIndices, + layoutRowSelectionIndicator); + + return; + } + + // Selection is not row-based so if there are existing row selections, + // free them since they can't be reused. + if (visibleRowSelectionIndices.length > 0 && visibleColumnSelectionIndices.length == 0) + { + clearSelectionIndicators(); + } + + if (isCellSelectionMode()) + { + oldVisibleRowSelectionIndices = visibleRowSelectionIndices; + const oldVisibleColumnSelectionIndices:Vector. = visibleColumnSelectionIndices; + + // Load up the vectors with the row/column of each selected cell. + visibleRowSelectionIndices = new Vector.(); + visibleColumnSelectionIndices = new Vector.(); + for each (rowIndex in visibleRowIndices) + { + for each (var columnIndex:int in visibleColumnIndices) + { + if (grid.selectionContainsCell(rowIndex + viewRowIndex, columnIndex + viewColumnIndex)) + { + visibleRowSelectionIndices.push(rowIndex); + visibleColumnSelectionIndices.push(columnIndex); + } + } + } + + // Display the cell selections. + visibleSelectionIndicators = layoutCellElements( + selectionIndicatorFactory, + layer, + visibleSelectionIndicators, + oldVisibleRowSelectionIndices, oldVisibleColumnSelectionIndices, + visibleRowSelectionIndices, visibleColumnSelectionIndices, + layoutCellSelectionIndicator); + + return; + } + + // No selection. + + // If there are existing cell selections, + // free them since there is no selection. + if (visibleColumnSelectionIndices.length > 0) + clearSelectionIndicators(); + } + + private function layoutRowSelectionIndicator(indicator:IVisualElement, rowIndex:int):void + { + // Initialize this visual element + intializeGridVisualElement(indicator, rowIndex); + layoutGridElementR(indicator, gridDimensionsView.getRowBounds(rowIndex)); + } + + private function layoutCellSelectionIndicator(indicator:IVisualElement, + rowIndex:int, + columnIndex:int):void + { + // Initialize this visual element + intializeGridVisualElement(indicator, rowIndex, columnIndex); + layoutGridElementR(indicator, gridDimensionsView.getCellBounds(rowIndex, columnIndex)); + } + + private function clearSelectionIndicators():void + { + freeGridElements(visibleSelectionIndicators); + visibleRowSelectionIndices.length = 0; + visibleColumnSelectionIndices.length = 0; + } + + //-------------------------------------------------------------------------- + // + // Indicators: hover, caret + // + //-------------------------------------------------------------------------- + + private function layoutIndicator( + layer:GridLayer, + indicatorFactory:IFactory, + indicator:IVisualElement, + rowIndex:int, + columnIndex:int):IVisualElement + { + if (!layer) + return null; + + // If the indicatorFactory has changed for the specified non-null indicator, + // then free the old indicator. + + if (indicator && (indicatorFactory != elementToFactoryMap[indicator])) + { + removeGridElement(indicator); + indicator = null; + if (indicatorFactory == null) + return null; + } + + if (rowIndex == -1 || grid.selectionMode == GridSelectionMode.NONE || + (isCellSelectionMode() && (getNextVisibleColumnIndex(columnIndex - 1) != columnIndex))) + { + if (indicator) + indicator.visible = false; + return indicator; + } + + if (!indicator && indicatorFactory) + indicator = createGridElement(indicatorFactory); + + if (indicator) + { + const bounds:Rectangle = isRowSelectionMode() ? + gridDimensionsView.getRowBounds(rowIndex) : + gridDimensionsView.getCellBounds(rowIndex, columnIndex); + + intializeGridVisualElement(indicator, rowIndex, columnIndex); + + // TODO (klin): Remove this special case for the caret overlapping separators + // when we implement column/row gaps. + if (indicatorFactory == grid.caretIndicator && bounds) + { + // increase width and height by 1 to cover separator. + const columnsLength:int = getColumnsLength(); + if (isCellSelectionMode() && (columnIndex < columnsLength - 1)) + bounds.width += 1; + + //if ((rowIndex < grid.dataProvider.length - 1) || (visibleRowIndices.length > grid.dataProvider.length)) + // bounds.height += 1; + const dataProviderLength:int = getDataProviderLength(); [... 697 lines stripped ...]