blob: 9c975b23ece6d19f3e3d8ca81332454021d3bc31 [file] [log] [blame]
////////////////////////////////////////////////////////////////////////////////
//
// 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.geom.Rectangle;
import mx.collections.IList;
import mx.events.CollectionEvent;
import mx.events.CollectionEventKind;
import mx.events.PropertyChangeEvent;
[ExcludeClass]
/**
* A sparse data structure that represents the widths and heights of a grid.
*
* <p>Provides efficient support for finding the cumulative y distance to the
* start of a particular cell as well as finding the index of a particular
* cell at a certain y value.
* GridDimensions optimizes these operations by bookmarking the most recently
* visited rows.</p>
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 2.0
* @productversion Flex 4.5
*/
public class GridDimensions
{
include "../../core/Version.as";
//--------------------------------------------------------------------------
//
// Class methods
//
//--------------------------------------------------------------------------
/**
* Inserts specified elements starting from startIndex.
*
* <pre>Our implementation of:
* var argArray:Array = [];
* for each (var x:Number in values)
* {
* argArray.push(x);
* }
* argArray.splice(0, 0, startIndex, 0);
* vec.splice.apply(vec, argArray);</pre>
*
* @param vec The vector to insert into.
* @param startIndex The index to insert at.
* @param elements The elements to be inserted.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 2.0
* @productversion Flex 4.5
*/
public static function insertElementsToVector(vec:Vector.<Number>, startIndex:int, elements:Vector.<Number>):void
{
const oldLength:int = vec.length;
const count:int = elements.length;
vec.length += count;
const vecLength:int = vec.length;
var i:int;
// Shift elements down by count.
for (i = oldLength - 1; i >= startIndex; i--)
vec[i + count] = vec[i];
const endIndex:int = startIndex + elements.length;
var j:int = 0;
for (i = startIndex; i < endIndex; i++)
vec[i] = elements[j++];
}
/**
* Insert count elements of the specified value starting from
* startIndex.
*
* @param vec The vector to insert into.
* @param startIndex The index to insert at.
* @param count The number of elements to insert
* @param value The value of the inserted elements
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 2.0
* @productversion Flex 4.5
*/
public static function insertValueToVector(vec:Vector.<Number>, startIndex:int, count:int, value:Number):void
{
const oldLength:int = vec.length;
vec.length += count;
const vecLength:int = vec.length;
// Shift elements down by count.
for (var i:int = oldLength - 1; i >= startIndex; i--)
vec[i + count] = vec[i];
clearVector(vec, value, startIndex, count);
}
/**
* Sets all of the numbers in a vector to be value unless otherwise
* specified.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 2.0
* @productversion Flex 4.5
*/
public static function clearVector(vec:Vector.<Number>, value:Number, startIndex:int = 0, count:int = -1):void
{
const endIndex:int = (count == -1) ? vec.length : startIndex + count;
for (var i:int = startIndex; i < endIndex; i++)
vec[i] = value;
}
/**
* @private
* Restrict a number to a particular min and max.
*/
private static function bound(a:Number, min:Number, max:Number):Number
{
if (a < min)
a = min;
else if (a > max)
a = max;
return a;
}
//--------------------------------------------------------------------------
//
// Variables
//
//--------------------------------------------------------------------------
private var rowList:GridRowList = new GridRowList();
private var _columnWidths:Vector.<Number> = new Vector.<Number>();
// Bookmark recently visited rows by caching the node representing the row
// and the y-value corresponding to the start of the node.
// startY/recentNode is the bookmark used by getRowIndexAt()
private var startY:Number = 0;
private var recentNode:GridRowNode = null;
// startY2/recentNode2 is bookmark used by getCellY()
private var startY2:Number = 0;
private var recentNode2:GridRowNode = null;
private const typicalCellWidths:Vector.<Number> = new Vector.<Number>();
private const typicalCellHeights:Vector.<Number> = new Vector.<Number>();
private var maxTypicalCellHeight:Number = NaN;
private var useMaxTypicalCellHeight:Boolean = true;
//--------------------------------------------------------------------------
//
// Constructor
//
//--------------------------------------------------------------------------
/**
* Constructor.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 2.0
* @productversion Flex 4.5
*/
public function GridDimensions()
{
super();
}
//--------------------------------------------------------------------------
//
// Properties
//
//--------------------------------------------------------------------------
//----------------------------------
// rowCount
//----------------------------------
private var _rowCount:int = 0;
/**
* The number of rows in the Grid. If this is decreased, the
* excess rows will be removed.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 2.0
* @productversion Flex 4.5
*/
public function get rowCount():int
{
return _rowCount;
}
/**
* @private
*/
public function set rowCount(value:int):void
{
if (value == _rowCount)
return;
// remove rows greater than value.
// This also clears the cached nodes.
if (value < _rowCount)
removeRowsAt(value, value - _rowCount)
_rowCount = value;
}
//----------------------------------
// columnCount
//----------------------------------
private var _columnCount:int = 0;
/**
* The number of columns in the Grid.
* Setting this property will clear the cache.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 2.0
* @productversion Flex 4.5
*/
public function get columnCount():int
{
return _columnCount;
}
/**
* @private
*/
public function set columnCount(value:int):void
{
// Don't short-circuit return if the column count didn't change.
// There might be a new set of columns which is the same length
// as the old set of columns.
// clear cached information
clearHeights();
// fix up the number of columns
_columnCount = value;
_columnWidths.length = value;
typicalCellHeights.length = value;
typicalCellWidths.length = value;
rowList.numColumns = value;
// clear the rest of the vectors
clearTypicalCellWidthsAndHeights();
clearVector(_columnWidths, NaN, 0, _columnCount);
}
//----------------------------------
// rowGap
//----------------------------------
private var _rowGap:Number = 0;
/**
* The gap between rows.
*
* @default 0
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 2.0
* @productversion Flex 4.5
*/
public function get rowGap():Number
{
return _rowGap;
}
/**
* @private
*/
public function set rowGap(value:Number):void
{
if (value == _rowGap)
return;
_rowGap = value;
clearCachedNodes();
}
//----------------------------------
// columnGap
//----------------------------------
private var _columnGap:Number = 0;
/**
* The gap between columns.
*
* @default 0
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 2.0
* @productversion Flex 4.5
*/
public function get columnGap():Number
{
return _columnGap;
}
/**
* @private
*/
public function set columnGap(value:Number):void
{
if (value == _columnGap)
return;
_columnGap = value;
clearCachedNodes();
}
//----------------------------------
// defaultRowHeight
//----------------------------------
private var _defaultRowHeight:Number = NaN;
/**
* The default height of a row.
*
* <p>If this property is not set explicitly, it will use the maximum
* of the typical cell heights.</p>
*
* <p>When variableRowHeight is false, all rows have a height of
* defaultRowHeight.</p>
*
* @default NaN
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 2.0
* @productversion Flex 4.5
*/
public function get defaultRowHeight():Number
{
return useMaxTypicalCellHeight ? maxTypicalCellHeight : _defaultRowHeight;
}
/**
* @private
*/
public function set defaultRowHeight(value:Number):void
{
if (value == _defaultRowHeight)
return;
_defaultRowHeight = bound(value, _minRowHeight, _maxRowHeight);
useMaxTypicalCellHeight = isNaN(_defaultRowHeight);
clearCachedNodes();
}
//----------------------------------
// defaultColumnWidth
//----------------------------------
private var _defaultColumnWidth:Number = 150;
/**
* The default width of a column.
* If this changes, update the ASDoc for GridLayout/getItemRendererAt().
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 2.0
* @productversion Flex 4.5
*/
public function get defaultColumnWidth():Number
{
return _defaultColumnWidth;
}
/**
* @private
*/
public function set defaultColumnWidth(value:Number):void
{
if (value == _defaultColumnWidth)
return;
_defaultColumnWidth = value;
}
//----------------------------------
// variableRowHeight
//----------------------------------
private var _variableRowHeight:Boolean = false; // default value must match Grid property default value
/**
* If variableRowHeight is false, calling getRowHeight
* will return the value of defaultRowHeight.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 2.0
* @productversion Flex 4.5
*/
public function get variableRowHeight():Boolean
{
return _variableRowHeight;
}
/**
* @private
*/
public function set variableRowHeight(value:Boolean):void
{
if (value == _variableRowHeight)
return;
_variableRowHeight = value;
}
//----------------------------------
// minRowHeight
//----------------------------------
private var _minRowHeight:Number = 0;
/**
* The minimum height of each row.
*
* @default 0
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 2.0
* @productversion Flex 4.5
*/
public function get minRowHeight():Number
{
return _minRowHeight;
}
/**
* @private
*/
public function set minRowHeight(value:Number):void
{
if (value == _minRowHeight)
return;
_minRowHeight = value;
_defaultRowHeight = Math.max(_defaultRowHeight, _minRowHeight);
}
//----------------------------------
// maxRowHeight
//----------------------------------
private var _maxRowHeight:Number = 10000;
/**
* The maximum height of each row.
*
* @default 10000
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 2.0
* @productversion Flex 4.5
*/
public function get maxRowHeight():Number
{
return _maxRowHeight;
}
/**
* @private
*/
public function set maxRowHeight(value:Number):void
{
if (value == _maxRowHeight)
return;
_maxRowHeight = value;
_defaultRowHeight = Math.min(_defaultRowHeight, _maxRowHeight);
}
//--------------------------------------------------------------------------
//
// Methods
//
//--------------------------------------------------------------------------
/**
* Clears bookmarked nodes.
* Each bookmarked node is associated with a cumulative y-value representing
* the total height of the grid up to the start of the row.
* These values are cleared as well.
*/
private function clearCachedNodes():void
{
recentNode = null;
startY = 0;
recentNode2 = null;
startY2 = 0;
}
/**
* Returns the height of the row at the given index. If variableRowHeight
* is true, then the height in precendence order is: the height set by setRowHeight,
* the natural height of the row (determined by the maximum of its cell heights),
* and defaultRowHeight. If variableRowHeight is false, then the height returned
* is the defaultRowHeight. The returned height is always bounded by the minRowHeight
* and maxRowHeight.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 2.0
* @productversion Flex 4.5
*/
public function getRowHeight(row:int):Number
{
// Unless setRowHeight is called, return the max cell height for this row
var height:Number = defaultRowHeight;
if (variableRowHeight)
{
var node:GridRowNode = rowList.find(row);
if (node)
{
if (node.fixedHeight >= 0)
height = node.fixedHeight;
else if (node.maxCellHeight >= 0)
height = node.maxCellHeight;
}
}
return (!isNaN(height)) ? bound(height, minRowHeight, maxRowHeight) : height;
}
/**
* Sets the height of a given row. This height takes precedence over
* the natural height of the row (determined by the maximum of its
* cell heights) and the defaultRowHeight. However, if variableRowHeight
* is true, this method has no effect.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 2.0
* @productversion Flex 4.5
*/
public function setRowHeight(row:int, height:Number):void
{
if (!variableRowHeight)
return;
var node:GridRowNode = rowList.find(row);
if (node)
{
node.fixedHeight = bound(height, minRowHeight, maxRowHeight);
}
else
{
node = rowList.insert(row);
if (node)
node.fixedHeight = bound(height, minRowHeight, maxRowHeight);
}
clearCachedNodes();
}
/**
* Returns the width of the column at the given index. Returns
* the width specified by setColumnWidth. If no width has been
* specified, returns the typical width. If no typical width has
* been set, it returns the defaultColumnWidth.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 2.0
* @productversion Flex 4.5
*/
public function getColumnWidth(col:int):Number
{
var w:Number = NaN;
w = _columnWidths[col];
if (isNaN(w))
w = typicalCellWidths[col];
if (isNaN(w))
w = this.defaultColumnWidth;
return w;
}
/**
* Sets the width of a given column.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 2.0
* @productversion Flex 4.5
*/
public function setColumnWidth(col:int, width:Number):void
{
_columnWidths[col] = width;
}
/**
* Returns the height of the specified cell. Returns the height
* specified by setCellHeight. If the height has not been specified,
* returns NaN.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 2.0
* @productversion Flex 4.5
*/
public function getCellHeight(row:int, col:int):Number
{
var node:GridRowNode = rowList.find(row);
if (node)
return node.getCellHeight(col);
return NaN;
}
/**
* Sets the height of the specified cell.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 2.0
* @productversion Flex 4.5
*/
public function setCellHeight(row:int, col:int, height:Number):void
{
if (!variableRowHeight)
return;
var node:GridRowNode = rowList.find(row);
var oldHeight:Number = defaultRowHeight;
if (node == null)
node = rowList.insert(row);
else
oldHeight = node.maxCellHeight;
if (node && node.setCellHeight(col, height))
{
if (recentNode && node.rowIndex < recentNode.rowIndex)
startY += node.maxCellHeight - oldHeight;
if (recentNode2 && node.rowIndex < recentNode2.rowIndex)
startY2 += node.maxCellHeight - oldHeight;
}
}
/**
* Returns the layout bounds of the specified cell. The cell height
* and width are determined by its row's height and column's width.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 2.0
* @productversion Flex 4.5
*/
public function getCellBounds(row:int, col:int):Rectangle
{
// TODO (klin): provide optional return value (Rectangle) parameter
if (row < 0 || row >= rowCount || col < 0 || col >= columnCount)
return null;
var x:Number = getCellX(row, col);
var y:Number = getCellY(row, col);
var width:Number = getColumnWidth(col);
var height:Number = getRowHeight(row);
return new Rectangle(x, y, width, height);
}
/**
* Returns the X coordinate of the origin of the specified cell.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 2.0
* @productversion Flex 4.5
*/
public function getCellX(row:int, col:int):Number
{
var x:Number = 0;
for (var i:int = 0; i < col; i++)
{
x += getColumnWidth(i) + columnGap;
}
return x;
}
/**
* Returns the Y coordinate of the origin of the specified cell.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 2.0
* @productversion Flex 4.5
*/
public function getCellY(row:int, col:int):Number
{
// no cache so we use default heights for each row.
if (!variableRowHeight || rowList.length == 0)
return row * (defaultRowHeight + rowGap);
if (row == 0)
return 0;
// initialize first node.
if (!recentNode2)
{
recentNode2 = rowList.first;
startY2 = recentNode2.rowIndex * (defaultRowHeight + rowGap);
}
var y:Number;
var recentIndex:int = recentNode2.rowIndex;
if (row == recentIndex)
y = startY2;
else if (row < recentIndex)
y = getPrevYAt(row, recentNode2, startY2);
else
y = getNextYAt(row, recentNode2, startY2);
return y;
}
/**
* @private
* Returns the starting y value of the specified row. The row must be before
* startNode.
*
* @param row the target row
* @param startNode the node to search from
* @param startY the cumulative y value from the first row to the beginning
* of startNode's row.
*
* @return the y value of the start of the row.
*/
private function getPrevYAt(row:int, startNode:GridRowNode, startY:Number):Number
{
var node:GridRowNode = startNode;
var nodeY:Number = startY;
var prevNode:GridRowNode;
var currentY:Number = startY;
var indDiff:int;
while (node)
{
if (node.rowIndex == row)
break;
prevNode = node.prev;
if (!prevNode || (row < node.rowIndex && row > prevNode.rowIndex))
{
// at the beginning or somewhere between nodes
// so we've found the target row.
indDiff = node.rowIndex - row;
currentY -= indDiff * (defaultRowHeight + rowGap);
break;
}
// subtract previous node's height and its gap.
indDiff = node.rowIndex - prevNode.rowIndex - 1;
currentY = currentY - indDiff * (defaultRowHeight + rowGap) - (getRowHeight(prevNode.rowIndex) + rowGap);
nodeY = currentY;
node = prevNode;
}
this.recentNode2 = node;
this.startY2 = nodeY;
return currentY;
}
/**
* @private
* Returns the starting y value of the specified row. The row must be after
* startNode.
*
* @param row the target row
* @param startNode the node to search from
* @param startY the cumulative y value from the first row to beginning of
* startNode's row.
*
* @return the y value of the start of the row.
*/
private function getNextYAt(row:int, startNode:GridRowNode, startY:Number):Number
{
var node:GridRowNode = startNode;
var nodeY:Number = startY;
var nextNode:GridRowNode;
var currentY:Number = startY;
var indDiff:int;
while (node)
{
if (node.rowIndex == row)
break;
// add next row's height and rowGap
currentY += getRowHeight(node.rowIndex);
if (node.rowIndex < _rowCount - 1)
currentY += rowGap;
nextNode = node.next;
if (!nextNode || (row > node.rowIndex && row < nextNode.rowIndex))
{
// at the beginning or somewhere between nodes
// so we've found the target row.
indDiff = row - node.rowIndex - 1;
currentY += indDiff * (defaultRowHeight + rowGap);
break;
}
// add estimated heights of rows in between measured rows.
indDiff = nextNode.rowIndex - node.rowIndex - 1;
currentY = currentY + indDiff * (defaultRowHeight + rowGap);
nodeY = currentY;
node = nextNode;
}
this.recentNode2 = node;
this.startY2 = nodeY;
return currentY;
}
/**
* Returns the layout bounds of the specified row.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 2.0
* @productversion Flex 4.5
*/
public function getRowBounds(row:int):Rectangle
{
// TODO (klin): provide optional return value (Rectangle) parameter
if ((row < 0) || (row >= _rowCount))
return null;
if (_columnCount == 0 || _rowCount == 0)
return new Rectangle(0, 0, 0, 0);
const x:Number = getCellX(row, 0);
const y:Number = getCellY(row, 0);
const rowWidth:Number = getCellX(row, _columnCount - 1) + getColumnWidth(_columnCount - 1) - x;
const rowHeight:Number = getRowHeight(row);
return new Rectangle(x, y, rowWidth, rowHeight);
}
/**
* Return the dimensions of a row that's being used to "pad" the grid
* by filling unused space below the last row in a layout where all rows
* are visible. Pad rows have index >= rowCount, height = defaultRowHeight.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 2.0
* @productversion Flex 4.5
*/
public function getPadRowBounds(row:int):Rectangle
{
if (row < 0)
return null;
if (row < rowCount)
return getRowBounds(row);
const lastRow:int = rowCount - 1;
const lastCol:int = columnCount - 1;
const x:Number = (lastRow >= 0) ? getCellX(lastRow, 0) : 0;
const lastRowBottom:Number = (lastRow >= 0) ? getCellY(lastRow, 0) + getRowHeight(lastRow) : 0;
const padRowCount:int = row - rowCount;
const padRowTotalGap:Number = (padRowCount > 0) ? (padRowCount - 1) * rowGap : 0;
const y:Number = lastRowBottom + (padRowCount * defaultRowHeight) + padRowTotalGap;
var rowWidth:Number = 0;
if ((lastCol >= 0) && (lastRow >= 0))
rowWidth = getCellX(lastRow, lastCol) + getColumnWidth(lastCol) - x;
else if (lastCol >= 0)
rowWidth = getCellX(0, lastCol) + getColumnWidth(lastCol) - x;
else if (lastRow >= 0)
rowWidth = getCellX(lastRow, 0) + getColumnWidth(0) - x;
return new Rectangle(x, y, rowWidth, defaultRowHeight);
}
/**
* Returns the layout bounds of the specified column.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 2.0
* @productversion Flex 4.5
*/
public function getColumnBounds(col:int):Rectangle
{
// TODO (klin): provide optional return value (Rectangle) parameter
if ((col < 0) || (col >= _columnCount))
return null;
if (_columnCount == 0)
return new Rectangle(0, 0, 0, 0);
const x:Number = getCellX(0, col);
const y:Number = getCellY(0, col);
const colWidth:Number = getColumnWidth(col);
var colHeight:Number = 0;
if (_rowCount > 0)
colHeight = getCellY(_rowCount - 1, col) + getRowHeight(_rowCount - 1) - y;
return new Rectangle(x, y, colWidth, colHeight);
}
/**
* Returns the index of the row at the specified coordinates. If
* the coordinates lie in a gap area, the index returned is the
* previous row.
*
* @return The index of the row at the coordinates provided. If the
* coordinates are out of bounds, return -1.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 2.0
* @productversion Flex 4.5
*/
public function getRowIndexAt(x:Number, y:Number):int
{
if (y < 0)
return -1;
var index:int;
if (!variableRowHeight || rowList.length == 0)
{
index = y / (defaultRowHeight + rowGap);
return index < _rowCount ? index : -1;
}
if (y == 0)
return _rowCount > 0 ? 0 : -1;
// initialize first node.
if (!recentNode)
{
recentNode = rowList.first;
startY = recentNode.rowIndex * (defaultRowHeight + rowGap);
}
// if we are already at the right row, then use the index.
if (isYInRow(y, startY, recentNode))
index = recentNode.rowIndex;
else if (y < startY)
index = getPrevRowIndexAt(y, recentNode, startY);
else
index = getNextRowIndexAt(y, recentNode, startY);
return index < _rowCount ? index : -1;
}
/**
* @private
* Checks if a certain y value lies in a row's bounds.
*/
private function isYInRow(y:Number, startY:Number, node:GridRowNode):Boolean
{
var end:Number = startY + getRowHeight(node.rowIndex);
// don't add gap for last row.
if (node.rowIndex != rowCount - 1)
end += rowGap;
// if y is between cumY and cumY + rowHeight - 1 then y is in the row.
if (y >= startY && y < end)
return true;
return false;
}
/**
* @private
* Returns the index of the row that contains the specified y value.
* The row will be before startNode.
*
* @param y the target y value
* @param startNode the node to search from
* @param startY the cumulative y value from the first row to startNode
*
* @return the index of the row that contains the y value.
*/
private function getPrevRowIndexAt(y:Number, startNode:GridRowNode, startY:Number):int
{
var node:GridRowNode = startNode;
var prevNode:GridRowNode = null;
var index:int = node.rowIndex;
var currentY:Number = startY;
var prevY:Number;
var targetY:Number = y;
while (node)
{
// check the current node.
if (isYInRow(targetY, currentY, node))
break;
// calculate previous y.
prevNode = node.prev;
if (!prevNode)
{
prevY = 0;
}
else
{
prevY = currentY;
var indDiff:int = node.rowIndex - prevNode.rowIndex;
// subtract default row heights if difference is greater than one.
if (indDiff > 1)
prevY -= (indDiff - 1) * (defaultRowHeight + rowGap);
}
// check if target Y is in range.
if (targetY < currentY && targetY >= prevY)
{
index = index - Math.ceil(Number(currentY - targetY)/(defaultRowHeight + rowGap));
break;
}
// subtract previous node's height and its gap.
currentY = prevY - getRowHeight(prevNode.rowIndex) - rowGap;
node = node.prev;
index = node.rowIndex;
}
this.recentNode = node;
this.startY = currentY;
return index;
}
/**
* @private
* Returns the index of the row that contains the specified y value.
* The row will be after startNode.
*
* @param y the target y value
* @param startNode the node to search from
* @param startY the cumulative y value from the first row to startNode
*
* @return the index of the row that contains the y value.
*/
private function getNextRowIndexAt(y:Number, startNode:GridRowNode, startY:Number):int
{
var node:GridRowNode = startNode;
var nextNode:GridRowNode = null;
var index:int = node.rowIndex;
var nodeY:Number = startY;
var currentY:Number = startY;
var nextY:Number;
var targetY:Number = y;
while (node)
{
// check the current node.
if (isYInRow(targetY, nodeY, node))
break;
// currentY increments to end of the current node.
currentY += getRowHeight(node.rowIndex);
if (node.rowIndex != rowCount - 1)
currentY += rowGap;
// calculate end of next section.
nextNode = node.next;
nextY = currentY;
var indDiff:int;
if (!nextNode)
{
indDiff = rowCount - 1 - node.rowIndex;
// the y at the end doesn't have a rowGap, but includes the final pixel.
nextY += indDiff * (defaultRowHeight + rowGap) - rowGap + 1;
}
else
{
indDiff = nextNode.rowIndex - node.rowIndex;
nextY += (indDiff - 1) * (defaultRowHeight + rowGap);
}
// check if target Y is within default row heights range.
if (targetY >= currentY && targetY < nextY)
{
index = index + Math.ceil(Number(targetY - currentY)/(defaultRowHeight + rowGap));
break;
}
// if no next node and we didn't find the target, then the row doesn't exist.
if (!nextNode)
{
index = -1;
break;
}
// move y values ahead to next node.
nodeY = currentY = nextY;
node = node.next;
index = node.rowIndex;
}
this.recentNode = node;
this.startY = nodeY;
return index;
}
/**
* Returns the index of the column at the specified coordinates. If
* the coordinates lie in a gap area, the index returned is the
* previous column. Returns -1 if the coordinates are out of bounds.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 2.0
* @productversion Flex 4.5
*/
public function getColumnIndexAt(x:Number, y:Number):int
{
var current:Number = x;
var i:int;
if(current < 0)
return -1;
for (i = 0; i < _columnCount; i++)
{
var columnWidth:Number = _columnWidths[i];
// fall back on typical widths if the actual width isn't set.
if (isNaN(columnWidth))
{
columnWidth = typicalCellWidths[i];
if (columnWidth == 0) // invisible column
continue;
}
// fall back on defaultColumnWidth
if (isNaN(columnWidth))
columnWidth = defaultColumnWidth;
current -= columnWidth + columnGap;
if (current < 0)
return i;
}
return -1;
}
/**
* Returns the total layout width of the content including gaps. If
* columnCountOverride is specified, then the overall width of as many columns
* is returned.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 2.0
* @productversion Flex 4.5
*/
public function getContentWidth(columnCountOverride:int = -1, startColumnIndex:int = 0):Number
{
const nCols:int = (columnCountOverride == -1) ? columnCount - startColumnIndex : columnCountOverride;
var contentWidth:Number = 0;
var measuredColCount:int = 0;
for (var columnIndex:int = startColumnIndex; (columnIndex < columnCount) && (measuredColCount < nCols); columnIndex++)
{
if (columnIndex >= _columnWidths.length)
{
contentWidth += defaultColumnWidth;
measuredColCount++;
continue;
}
var width:Number = _columnWidths[columnIndex];
// fall back on typical width
if (isNaN(width))
{
width = typicalCellWidths[columnIndex];
// column.visible==false, skip this column.
if (width == 0)
continue;
}
// fall back on defaultColumnWidth
if (isNaN(width))
width = defaultColumnWidth;
contentWidth += width;
measuredColCount++;
}
if (measuredColCount > 1)
contentWidth += (measuredColCount - 1) * columnGap;
return contentWidth;
}
/**
* Returns the total layout height of the content including gaps. If
* rowCountOverride is specified, then the overall height of as many rows
* is returned.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 2.0
* @productversion Flex 4.5
*/
public function getContentHeight(rowCountOverride:int = -1, startRowIndex:int = 0):Number
{
const nRows:int = (rowCountOverride == -1) ? rowCount - startRowIndex : rowCountOverride;
const maxRow:int = (rowCountOverride == -1) ? rowCount : startRowIndex + rowCountOverride;
var contentHeight:Number = 0;
if (nRows > 1)
contentHeight += (nRows - 1) * rowGap;
if (!variableRowHeight || rowList.length == 0)
return contentHeight + nRows * defaultRowHeight;
var node:GridRowNode = (startRowIndex == 0) ? rowList.first : rowList.findNearestLTE(startRowIndex);
var numRows:int = 0;
while (node && node.rowIndex < maxRow)
{
if (node.rowIndex < startRowIndex)
{
node = node.next;
continue;
}
contentHeight += getRowHeight(node.rowIndex);
numRows++;
node = node.next;
}
contentHeight += (nRows - numRows) * defaultRowHeight;
return contentHeight;
}
/**
* Returns the sum of the typical cell widths including gaps. If
* columnCountOverride is specified, then the overall typicalCellWidth
* of as many columns is returned.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 2.0
* @productversion Flex 4.5
*/
public function getTypicalContentWidth(columnCountOverride:int = -1, startColumnIndex:int = 0):Number
{
const nCols:int = (columnCountOverride == -1) ? columnCount - startColumnIndex : columnCountOverride;
var contentWidth:Number = 0;
var measuredColCount:int = 0;
for (var columnIndex:int = startColumnIndex; (columnIndex < columnCount) && (measuredColCount < nCols); columnIndex++)
{
// column.visible==false columns will have a typicalCellWidth of 0, so skip them.
var width:Number = columnIndex < columnCount ? typicalCellWidths[columnIndex] : NaN;
if (width == 0)
continue;
if (isNaN(width))
width = defaultColumnWidth;
contentWidth += width;
measuredColCount++;
}
if (measuredColCount > 1)
contentWidth += (measuredColCount - 1) * columnGap;
return contentWidth;
}
/**
* Returns the content height which is maximum cell height of the
* typical item times the total number of rows including gaps.
* If rowCountOverride is specified, then we only include that many rows.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 2.0
* @productversion Flex 4.5
*/
public function getTypicalContentHeight(rowCountOverride:int = -1, startRowIndex:int = 0):Number
{
const nRows:int = (rowCountOverride == -1) ? rowCount - startRowIndex : rowCountOverride;
var contentHeight:Number = 0;
if (nRows > 1)
contentHeight += (nRows - 1) * rowGap;
if (!isNaN(defaultRowHeight))
return contentHeight + nRows * defaultRowHeight;
return 0;
}
/**
* Return the preferred bounds width of the grid's typicalItem when rendered with the item renderer
* for the specified column. If no value has yet been specified, return NaN.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 2.0
* @productversion Flex 4.5
*/
public function getTypicalCellWidth(columnIndex:int):Number
{
return typicalCellWidths[columnIndex];
}
/**
* Sets the preferred bounds width of the grid's typicalItem for the specified column.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 2.0
* @productversion Flex 4.5
*/
public function setTypicalCellWidth(columnIndex:int, value:Number):void
{
typicalCellWidths[columnIndex] = value;
}
/**
* Return the preferred bounds height of the grid's typicalItem when rendered with the item renderer
* for the specified column. If no value has yet been specified, return NaN.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 2.0
* @productversion Flex 4.5
*/
public function getTypicalCellHeight(columnIndex:int):Number
{
return typicalCellHeights[columnIndex];
}
/**
* Sets the preferred bounds height of the grid's typicalItem for the specified column.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 2.0
* @productversion Flex 4.5
*/
public function setTypicalCellHeight(columnIndex:int, value:Number):void
{
typicalCellHeights[columnIndex] = value;
var max:Number = 0;
const typicalCellHeightsLength:int = typicalCellHeights.length;
for (var i:int = 0; i < typicalCellHeightsLength; i++)
{
if (!isNaN(typicalCellHeights[i]))
max = Math.max(max, typicalCellHeights[i]);
}
this.maxTypicalCellHeight = max;
}
/**
* Clears the typical cell for every column and row.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 2.0
* @productversion Flex 4.5
*/
public function clearTypicalCellWidthsAndHeights():void
{
clearVector(typicalCellWidths, NaN);
clearVector(typicalCellHeights, NaN);
maxTypicalCellHeight = NaN;
}
//--------------------------------------------------------------------------
//
// Methods for handling dataProvider and column list changes
//
//--------------------------------------------------------------------------
/**
* Inserts count number of rows starting from startRow. This shifts
* any rows after startRow down by count and will increment
* rowCount.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 2.0
* @productversion Flex 4.5
*/
public function insertRows(startRow:int, count:int):void
{
insertRowsAt(startRow, count);
}
/**
* Inserts count number of columns starting from startColumn. This
* shifts any columns after startColumn down by count and will
* increment columnCount.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 2.0
* @productversion Flex 4.5
*/
public function insertColumns(startColumn:int, count:int):void
{
const oldColumnCount:int = _columnCount;
const newColumnCount:int = _columnCount + count;
if (startColumn < 0 || startColumn > oldColumnCount)
return;
// change column count of all nodes in GridRowList.
rowList.insertColumns(startColumn, count);
// add to columnCount
_columnCount = newColumnCount;
// insert new values into the arrays.
insertValueToVector(_columnWidths, startColumn, count, NaN);
insertValueToVector(typicalCellWidths, startColumn, count, NaN);
insertValueToVector(typicalCellHeights, startColumn, count, NaN);
}
/**
* Removes count number of rows starting from startRow. This
* shifts any rows after startRow up by count and will
* decrement rowCount.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 2.0
* @productversion Flex 4.5
*/
public function removeRows(startRow:int, count:int):void
{
removeRowsAt(startRow, count);
}
/**
* Removes count number of columns starting from startColumn. This
* shifts any columns after startColumn up by count and will
* decrement columnCount.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 2.0
* @productversion Flex 4.5
*/
public function removeColumns(startColumn:int, count:int):void
{
const oldColumnCount:int = _columnCount;
const newColumnCount:int = _columnCount - count;
if (startColumn < 0 || startColumn >= oldColumnCount)
return;
// if we remove all the columns, clear everything.
if (newColumnCount <= 0)
{
columnCount = 0;
return;
}
// Otherwise, lets remove the specified columns.
// change column count of all nodes in GridRowList.
rowList.removeColumns(startColumn, count)
// lower columnCount without clearing anything else.
_columnCount = newColumnCount;
// remove values from the needed vectors
_columnWidths.splice(startColumn, count);
typicalCellWidths.splice(startColumn, count);
typicalCellHeights.splice(startColumn, count);
// bookmarks are invalid because row heights may have changed.
clearCachedNodes();
}
/**
* Removes any nodes that occupy the indices between startRow
* and startRow + count.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 2.0
* @productversion Flex 4.5
*/
public function clearRows(startRow:int, count:int):void
{
if (startRow < 0 || count <= 0)
return;
var node:GridRowNode = rowList.findNearestLTE(startRow);
var endRow:int = startRow + count;
var oldNode:GridRowNode;
if (node && node.rowIndex < startRow)
node = node.next;
while (node && node.rowIndex < endRow)
{
oldNode = node;
node = node.next;
rowList.removeNode(oldNode);
}
// bookmarks are invalid because row heights may have changed.
clearCachedNodes();
}
/**
* Clears any columns that occupy the indices between startColumn
* and startColumn + count.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 2.0
* @productversion Flex 4.5
*/
public function clearColumns(startColumn:int, count:int):void
{
if (startColumn < 0 || startColumn >= _columnCount)
return;
rowList.clearColumns(startColumn, count);
clearVector(typicalCellWidths, NaN, startColumn, count);
clearVector(typicalCellHeights, NaN, startColumn, count);
clearVector(_columnWidths, NaN, startColumn, count);
// bookmarks are invalid because row heights may have changed.
clearCachedNodes();
}
/**
* Moves count number of rows from the fromRow index to the toRow
* index. This operation will not affect rowCount.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 2.0
* @productversion Flex 4.5
*/
public function moveRows(fromRow:int, toRow:int, count:int):void
{
var rows:Vector.<GridRowNode> = removeRowsAt(fromRow, count);
// Set the row indices of the nodes that are moving.
var diff:int = toRow - fromRow;
for each (var node:GridRowNode in rows)
{
node.rowIndex = node.rowIndex + diff;
}
insertRowsAt(toRow, count, rows);
}
/**
* Moves count number of columns from the fromCol index to the toCol
* index. This operation will not affect colCount.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 2.0
* @productversion Flex 4.5
*/
public function moveColumns(fromCol:int, toCol:int, count:int):void
{
if (fromCol < 0 || fromCol >= _columnCount || toCol < 0 || toCol > _columnCount)
return;
rowList.moveColumns(fromCol, toCol, count);
insertElementsToVector(_columnWidths, toCol, _columnWidths.splice(fromCol, count));
insertElementsToVector(typicalCellWidths, toCol, typicalCellWidths.splice(fromCol, count));
insertElementsToVector(typicalCellHeights, toCol, typicalCellHeights.splice(fromCol, count));
}
/**
* Clears all cached heights.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 2.0
* @productversion Flex 4.5
*/
public function clearHeights():void
{
rowList.removeAll();
clearCachedNodes();
}
/**
* @private
* Inserts count number of rows starting from startRow. This will increment
* rowCount. If the nodes parameter is set, the nodes will be inserted
* to the rowList.
*/
private function insertRowsAt(startRow:int, count:int, nodes:Vector.<GridRowNode> = null):void
{
// TODO (klin): Push this to GridRowList.
if (startRow < 0 || count <= 0)
return;
var startNode:GridRowNode = rowList.findNearestLTE(startRow);
var node:GridRowNode;
if (startNode && startNode.rowIndex < startRow)
startNode = startNode.next;
// first we insert the nodes before this node if it exists,
// if not, we just push it at the end.
if (nodes)
{
if (startNode)
{
for each (node in nodes)
rowList.insertBefore(startNode, node);
}
else
{
for each (node in nodes)
rowList.push(node);
}
}
// increment the index of nodes after the last node.
node = startNode;
while (node)
{
node.rowIndex += count;
node = node.next;
}
this.rowCount += count;
// bookmarks are invalid because row heights may have changed.
clearCachedNodes();
}
/**
* @private
* Removes count number of rows starting from startRow. This will
* decrement rowCount. Returns any removed nodes.
*/
private function removeRowsAt(startRow:int, count:int):Vector.<GridRowNode>
{
var vec:Vector.<GridRowNode> = new Vector.<GridRowNode>();
if (startRow < 0 || count <= 0)
return vec;
var node:GridRowNode = rowList.findNearestLTE(startRow);
var endRow:int = startRow + count;
var oldNode:GridRowNode;
if (node && node.rowIndex < startRow)
node = node.next;
while (node && node.rowIndex < endRow)
{
oldNode = node;
vec.push(oldNode);
node = node.next;
rowList.removeNode(oldNode);
}
while (node)
{
node.rowIndex -= count;
node = node.next;
}
_rowCount -= count;
// bookmarks are invalid because row heights may have changed.
clearCachedNodes();
return vec;
}
/**
* Handles changes in the dataProvider.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 2.0
* @productversion Flex 4.5
*/
public function dataProviderCollectionChanged(event:CollectionEvent):void
{
switch (event.kind)
{
case CollectionEventKind.ADD:
{
insertRows(event.location, event.items.length);
break;
}
case CollectionEventKind.REMOVE:
{
removeRows(event.location, event.items.length);
break;
}
case CollectionEventKind.MOVE:
{
moveRows(event.oldLocation, event.location, event.items.length);
break;
}
case CollectionEventKind.REFRESH:
{
clearHeights();
break;
}
case CollectionEventKind.RESET:
{
clearHeights();
clearTypicalCellWidthsAndHeights();
break;
}
case CollectionEventKind.UPDATE:
{
// handled by GridLayout
break;
}
case CollectionEventKind.REPLACE:
{
clearRows(event.location, event.items.length);
break;
}
}
}
/**
* Handles changes in columns.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 2.0
* @productversion Flex 4.5
*/
public function columnsCollectionChanged(event:CollectionEvent):void
{
switch (event.kind)
{
case CollectionEventKind.ADD:
{
insertColumns(event.location, event.items.length);
break;
}
case CollectionEventKind.REMOVE:
{
removeColumns(event.location, event.items.length);
break;
}
case CollectionEventKind.MOVE:
{
moveColumns(event.oldLocation, event.location, event.items.length);
break;
}
case CollectionEventKind.REFRESH:
case CollectionEventKind.RESET:
{
columnCount = IList(event.target).length;
break;
}
case CollectionEventKind.UPDATE:
{
// column may have changed visiblity
var pcEvent:PropertyChangeEvent;
const itemsLength:int = event.items ? event.items.length : 0;
for (var i:int = 0; i < itemsLength; i++)
{
pcEvent = event.items[i] as PropertyChangeEvent;
if (pcEvent && pcEvent.property == "visible")
columns_visibleChangedHandler(pcEvent);
}
break;
}
case CollectionEventKind.REPLACE:
{
clearColumns(event.location, event.items.length);
break;
}
}
}
/**
* @private
* Handle CollectionEventKind.UPDATE for a column whose 'visibility'
* property changed.
*/
private function columns_visibleChangedHandler(pcEvent:PropertyChangeEvent):void
{
const column:GridColumn = pcEvent.source as GridColumn;
const columnIndex:int = column.columnIndex;
if (!column || columnIndex < 0 || columnIndex >= _columnCount)
return;
clearColumns(columnIndex, 1);
// column.visible==true columns need to have their typical sizes and
// actual column width updated, while column.visible==false column
// have their typical sizes updated to 0 and actual column width
// set to NaN.
if (column.visible)
{
setTypicalCellWidth(columnIndex, NaN);
setTypicalCellHeight(columnIndex, NaN);
if (!isNaN(column.width))
setColumnWidth(columnIndex, column.width);
}
else
{
setTypicalCellWidth(columnIndex, 0);
setTypicalCellHeight(columnIndex, 0);
setColumnWidth(columnIndex, NaN);
}
}
/**
* @private
* For debugging purposes.
*/
public function toString():String
{
return rowList.toString();
}
}
}