/* Copyright (c) 2007 FlexLib Contributors. See: http://code.google.com/p/flexlib/wiki/ProjectContributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ package flexlib.controls { import flash.events.Event; import flash.events.KeyboardEvent; import flash.ui.Keyboard; import flash.xml.XMLNode; import flexlib.controls.treeGridClasses.TreeGridListData; import mx.collections.ArrayCollection; import mx.collections.ICollectionView; import mx.collections.IViewCursor; import mx.collections.ListCollectionView; import mx.collections.Sort; import mx.collections.SortField; import mx.collections.XMLListCollection; import mx.controls.DataGrid; import mx.controls.dataGridClasses.DataGridColumn; import mx.controls.listClasses.BaseListData; import mx.controls.listClasses.IListItemRenderer; import mx.controls.treeClasses.DefaultDataDescriptor; import mx.controls.treeClasses.ITreeDataDescriptor2; import mx.events.CollectionEvent; import mx.events.CollectionEventKind; import mx.events.DataGridEvent; import mx.utils.UIDUtil; import mx.controls.treeClasses.ITreeDataDescriptor; import flash.events.MouseEvent; import mx.events.ListEvent; [Style(name="disclosureOpenIcon", type="Class", format="EmbeddedFile", inherit="no")] [Style(name="disclosureClosedIcon", type="Class", format="EmbeddedFile", inherit="no")] [Style(name="folderOpenIcon", type="Class", format="EmbeddedFile", inherit="no")] [Style(name="folderClosedIcon", type="Class", format="EmbeddedFile", inherit="no")] [Style(name="defaultLeafIcon", type="Class", format="EmbeddedFile", inherit="no")] [Style(name="indentation", type="Number", inherit="no")] [Style(name="verticalTrunks", type="String", enumeration="none,normal,dotted", inherit="no")] [Style(name="trunkColor", type="uint", format="Color", inherit="yes")] /** * */ public class TreeGrid extends DataGrid { [Embed(source='../assets/defaultTreeAssets.swf#TreeDisclosureClosed')] private var defaultDisclosureClosedClass:Class; [Embed(source='../assets/defaultTreeAssets.swf#TreeDisclosureOpen')] private var defaultDisclosureOpenClass:Class; /** * @private * Used to hold a list of items that are opened or set opened. */ private var _openItems:Object = {}; /** * An object that specifies the icons for the items. * Each entry in the object has a field name that is the item UID * and a value that is an an object with the following format: *
* {iconID: Class, iconID2: Class}
*
* The iconID field value is the class of the icon for
* a closed or leaf item and the iconID2 is the class
* of the icon for an open item.
*
* This property is intended to allow initialization of item icons.
* Changes to this array after initialization are not detected
* automatically.
* Use the setItemIcon() method to change icons dynamically.
false, the Tree control does not display the root item.
* Only the decendants of the root item are displayed.
*
* This flag has no effect on non-rooted dataProviders, such as List and Array.
*
* @default true
* @see #hasRoot
*/
public function get showRoot():Boolean
{
return _showRoot;
}
/**
* @private
*/
public function set showRoot(value:Boolean):void
{
if (_showRoot != value)
{
_showRoot = value;
showRootChanged = true;
invalidateProperties();
}
}
/**
* Indicates that the current dataProvider has a root item; for example,
* a single top node in a hierarchical structure. XML and Object
* are examples of types that have a root. Lists and arrays do not.
*
* @see #showRoot
*/
public function get hasRoot():Boolean
{
return _hasRoot;
}
private function keyboardHandler(event:KeyboardEvent):void
{
switch (event.keyCode)
{
case Keyboard.LEFT:
expandSelectedNode();
event.preventDefault();
break;
case Keyboard.RIGHT:
closeSelectedNode();
event.preventDefault();
break;
}
}
private function expandSelectedNode():void
{
if (_dataDescriptor.hasChildren(selectedItem) && isItemOpen(selectedItem))
{
closeItemAt(selectedIndex, selectedItem);
}
}
private function closeSelectedNode():void
{
if (_dataDescriptor.hasChildren(selectedItem) && !isItemOpen(selectedItem))
{
openItemAt(selectedIndex, selectedItem);
}
}
override protected function commitProperties():void
{
if (showRootChanged)
{
if (!_hasRoot)
showRootChanged = false;
}
if (dataProviderChanged || showRootChanged)
{
var tmpCollection:ICollectionView;
var row:Object;
//we always reset the displayed rows on a dataprovider assignment or when the showRoot property change
_displayedModel = new ArrayCollection();
//reset flags
dataProviderChanged = false;
showRootChanged = false;
//we always reset the open and selected items on a dataprovider assignment
if (!openItemsChanged)
_openItems = {};
// are we swallowing the root?
if (_rootModel && !_showRoot && _hasRoot)
{
var rootItem:* = _rootModel.createCursor().current;
if (rootItem != null &&
_dataDescriptor.isBranch(rootItem, _rootModel) &&
_dataDescriptor.hasChildren(rootItem, _rootModel))
{
// then get rootItem children
tmpCollection = getChildren(rootItem, _rootModel);
for each (row in tmpCollection)
{
_displayedModel.addItem(row);
}
}
}
else
{
if (_hasRoot)
{
if (_rootModel != null && _rootModel.length > 0)
{
_displayedModel.addItem(_rootModel[0]);
}
}
else
{
for each (row in _rootModel)
{
_displayedModel.addItem(row);
}
}
}
/* Before calling super.dataProvider we want to store the vertical and horizontal
* scroll positions, as well as the list of selected items. After we set super.dataProvider
* we need to always rest the selcted items, and if the open items have changed then we
* need to reset the scroll positions.
*/
var vScrollPos:int = verticalScrollPosition;
var hScrollPos:int = horizontalScrollPosition;
var savedSelection:Array = selectedItems;
super.dataProvider = _displayedModel;
//TODO: maybe we can use HierarchicalCollectionView?
/*// at this point _rootModel may be null so we dont need to continue
if ( _rootModel )
{
//wrap userdata in a TreeCollection and pass that collection to the List
super.dataProvider = wrappedCollection = new HierarchicalCollectionView(
tmpCollection != null ? tmpCollection : _rootModel,
_dataDescriptor,
_openItems);
// not really a default handler, but we need to be later than the wrapper
wrappedCollection.addEventListener(CollectionEvent.COLLECTION_CHANGE,
collectionChangeHandler,
false,
EventPriority.DEFAULT_HANDLER, true);
}
else
{
super.dataProvider = null;
}*/
}
if (openItemsChanged)
{
verticalScrollPosition = vScrollPos;
horizontalScrollPosition = hScrollPos;
for each (var item:*in openItems)
{
// only expand an item iff it's a direct child from the root. openItemAt is recursive so don't worry about those here.
/*
if( _showRoot && _rootModel.contains( item )) {
var i : int = getItemIndex(item);
if( i >= 0 && i < collection.length ) {
openItemAt(i,item);
}
}
else if( !_showRoot) {
for(var j:int=0; j<_rootModel.length; j++) {
var rootChild:Object = _rootModel[j];
var children:ICollectionView = dataDescriptor.getChildren(rootChild, _rootModel);
if(children.contains(item)) {
var i : int = getItemIndex(item);
if( i >= 0 && i < collection.length ) {
openItemAt(i,item);
}
}
}
}
*/
var rootNodes:Object;
if (_showRoot || _hasRoot == false)
{
rootNodes = _rootModel;
}
else
{
rootNodes = [_rootModel[0]];
}
for (var i:int = 0; i < rootNodes.length; i++)
{
var rootChild:Object = rootNodes[i];
var itemIndex:int;
if (rootChild == item)
{
itemIndex = getItemIndex(item);
if (itemIndex >= 0 && itemIndex < collection.length)
{
openItemAt(itemIndex, item);
}
}
else
{
var children:ICollectionView = dataDescriptor.getChildren(rootChild, _rootModel);
if (children && children.contains(item))
{
itemIndex = getItemIndex(item);
if (itemIndex >= 0 && itemIndex < collection.length)
{
openItemAt(itemIndex, item);
}
}
}
}
}
openItemsChanged = false;
//setting open items resets the collection
var event:CollectionEvent =
new CollectionEvent(CollectionEvent.COLLECTION_CHANGE);
event.kind = CollectionEventKind.RESET;
collection.dispatchEvent(event);
}
super.selectedItems = savedSelection;
super.commitProperties();
}
override protected function makeListData(
data:Object,
uid:String,
rowNum:int,
columnNum:int,
column:DataGridColumn):BaseListData
{
var treeGridListData:TreeGridListData;
if (data is DataGridColumn)
{
treeGridListData = new TreeGridListData(
(column.headerText != null) ? column.headerText : column.dataField,
column.dataField,
columnNum,
uid,
this,
rowNum);
}
else
{
treeGridListData = new TreeGridListData(
column.itemToLabel(data),
column.dataField,
columnNum, uid,
this,
rowNum);
initListData(data, treeGridListData);
}
return treeGridListData;
}
protected function initListData(item:Object, treeListData:TreeGridListData):void
{
if (item == null)
return;
var open:Boolean = isItemOpen(item);
var branch:Boolean = isBranch(item);
var uid:String = itemToUID(item);
// this is hidden by non-branches but kept so we know how wide it is so things align
treeListData.disclosureIcon = getStyle(open ? "disclosureOpenIcon" :
"disclosureClosedIcon");
if (treeListData.disclosureIcon == null)
{
treeListData.disclosureIcon = open ? defaultDisclosureOpenClass : defaultDisclosureClosedClass;
}
treeListData.open = open;
treeListData.hasChildren = branch;
treeListData.depth = getItemDepth(item, treeListData.rowIndex);
treeListData.indent = (treeListData.depth - (_showRoot ? 1 : 2)) * getStyle("indentation");
treeListData.indentationGap = getStyle("indentation");
treeListData.item = item;
treeListData.icon = itemToIcon(item);
treeListData.trunk = getStyle("verticalTrunks");
if (treeListData.trunk)
{
treeListData.trunkOffsetTop = getStyle("paddingTop");
treeListData.trunkOffsetBottom = getStyle("paddingBottom");
treeListData.hasSibling = !isLastItem(treeListData);
var trunkColor:Object = getStyle("trunkColor");
treeListData.trunkColor = trunkColor ? uint(trunkColor) : 0xffffff;
}
}
private function getChildren(item:Object, view:Object):ICollectionView
{
//get the collection of children
var children:ICollectionView = _dataDescriptor.getChildren(item, view);
return children;
}
/**
* This method find if the current node is the last displayed sibling
*
* Used to draw the vertical trunk lines,
* if it is the last child then the vertical trunk line should stop in the middle of the row
**/
protected function isLastItem(listData:TreeGridListData):Boolean
{
//TODO: find a way to optimize this method, it's SLOOOWWWW maybe by using HierarchicalCollectionView? ...
/*
var rowIndex : int = getItemIndex( listData.item );
var data : Object = IList( dataProvider ).getItemAt( rowIndex );
if( IList( dataProvider ).length > rowIndex + 1 )
{
for(var i:int = rowIndex + 1; i < IList( dataProvider ).length; i++)
{
var nextData : Object = IList( dataProvider ).getItemAt( i );
var nextDataDepth : int = getItemDepth( nextData, i );
if( nextDataDepth == listData.depth )
return false;
}
}
*/
return false;
}
protected function getItemDepth(item:Object, offset:int):int
{
var depth:int;
//if the dataprovider have a root, we begin from the root.
if (hasRoot)
{
depth = searchForCurrentDepth(dataProvider[0], item);
}
//If the dataprovider don't have a root, we need to parse all items from the first level.
else
{
var i:int = 0;
do
{
depth = searchForCurrentDepth(dataProvider[i++], item);
} while (depth == -1)
}
if (depth == -1)
throw new Error("item not found");
return depth;
}
/**
*
*/
private function searchForCurrentDepth(
value:Object,
item:Object,
depth:int=1):int
{
if (value == item)
return depth;
if (value == null || _dataDescriptor.getChildren(value) == null)
return -1;
depth++;
for (var i:int = 0; i < _dataDescriptor.getChildren(value).length; i++)
{
var result:int =
searchForCurrentDepth(_dataDescriptor.getChildren(value)[i], item, depth);
if (result != -1)
return result;
}
return -1;
}
/**
* @private
*/
private function getItemIndex(item:Object):int
{
var cursor:IViewCursor = collection.createCursor();
var i:int = 0;
do
{
if (cursor.current === item)
break;
i++;
} while (cursor.moveNext());
return i;
}
//--------------------------------------------------------------------------
//
// public methods
//
//--------------------------------------------------------------------------
/**
* @private
*/
private var openItemsChanged:Boolean = false;
/**
* The items that have been opened or set opened.
*
* @default null
*/
public function get openItems():Object
{
var openItemsArray:Array = [];
for each (var item:*in _openItems)
{
openItemsArray.push(item);
}
return openItemsArray;
}
/**
* @private
*/
public function set openItems(value:Object):void
{
if (value != null)
{
for each (var item:*in value)
{
_openItems[itemToUID(item)] = item;
}
openItemsChanged = true;
invalidateProperties();
}
}
/**
*
*/
public function isBranch(item:Object):Boolean
{
if (item != null)
return _dataDescriptor.isBranch(item, iterator.view);
return false;
}
/**
*
*/
public function isItemOpen(item:Object):Boolean
{
var uid:String = itemToUID(item);
return _openItems[uid] != null;
}
/**
* @private
*/
override public function itemToIcon(item:Object):Class
{
if (item == null)
{
return null;
}
var icon:*;
var open:Boolean = isItemOpen(item);
var branch:Boolean = isBranch(item);
var uid:String = itemToUID(item);
//first lets check the component
var iconClass:Class =
itemIcons && itemIcons[uid] ?
itemIcons[uid][open ? "iconID2" : "iconID"] :
null;
if (iconClass)
{
return iconClass;
}
else if (iconFunction != null)
{
return iconFunction(item);
}
else if (branch)
{
return getStyle(open ? "folderOpenIcon" : "folderClosedIcon");
}
else
//let's check the item itself
{
if (item is XML)
{
try
{
if (item[iconField].length() != 0)
icon = String(item[iconField]);
}
catch (e:Error)
{
}
}
else if (item is Object)
{
try
{
if (iconField && item[iconField])
icon = item[iconField];
else if (item.icon)
icon = item.icon;
}
catch (e:Error)
{
}
}
}
//set default leaf icon if nothing else was found
if (icon == null)
icon = getStyle("defaultLeafIcon");
//convert to the correct type and class
if (icon is Class)
{
return icon;
}
else if (icon is String)
{
iconClass = Class(systemManager.getDefinitionByName(String(icon)));
if (iconClass)
return iconClass;
return document[icon];
}
else
{
return Class(icon);
}
}
/**
* Sets the associated icon for the item. Calling this method overrides the
* iconField and iconFunction properties for
* this item if it is a leaf item. Branch items don't use the
* iconField and iconFunction properties.
* They use the folderOpenIcon and folderClosedIcon properties.
*
* @param item Item to affect.
* @param iconID Linkage ID for the closed (or leaf) icon.
* @param iconID2 Linkage ID for the open icon.
*
* @tiptext Sets the icons for the specified item
* @helpid 3201
*/
public function setItemIcon(item:Object, iconID:Class, iconID2:Class):void
{
if (!itemIcons)
itemIcons = {};
if (!iconID2)
iconID2 = iconID;
itemIcons[itemToUID(item)] = {iconID: iconID, iconID2: iconID2};
itemsSizeChanged = true;
invalidateDisplayList();
}
/**
*
*/
public function closeAllItems():void
{
for (var i:int = 0; i < ICollectionView(_displayedModel).length; i++)
{
this.closeItemAt(i);
}
}
/**
*
*/
public function closeItemAt(rowNum:Number, item:Object=null, closeItem:Boolean=true):void
{
if (item == null)
item = ListCollectionView(_displayedModel).getItemAt(rowNum);
if (closeItem)
{
var uid:String = itemToUID(item);
delete _openItems[uid];
}
if (_dataDescriptor.getChildren(item))
{
// recursively remove the rows that were added for child records
// but don't remove item from _openItems[] to keep opened items.
for (var i:int = 0; i < _dataDescriptor.getChildren(item).length; i++)
{
if (isItemOpen(_dataDescriptor.getChildren(item)[i]))
{
closeItemAt(rowNum, _dataDescriptor.getChildren(item)[i], false);
}
ListCollectionView(_displayedModel).removeItemAt(rowNum + 1);
}
}
}
/**
* @return the number of rows added to the display model
*/
public function openItemAt(rowNum:Number, item:Object=null):Number
{
//if there was no item passed in then retrieve it via row number
if (item == null)
item = ListCollectionView(_displayedModel).getItemAt(rowNum);
//if the item is not already open
//if (!isItemOpen(item))
//{
//add the item to the list of open items
var uid:String = itemToUID(item);
_openItems[uid] = item;
var savedSelection:Array = this.selectedItems;
this.clearSelected();
var children:ICollectionView = _dataDescriptor.getChildren(item);
//if there are children then go and open them up, otherwise skip to the bottom and return 0
if (children)
{
// add the rows for the children at this level
for (var i:int = 0; i < children.length; i++)
{
var child:Object = children[i];
if (ListCollectionView(_displayedModel).contains(child) == false)
{
ListCollectionView(_displayedModel).addItemAt(child, rowNum + i + 1);
}
}
var offset:Number = 0;
for (i = 0; i < children.length; i++)
{
var vChild:Object = children[i];
if (isItemOpen(vChild))
{
offset += openItemAt(rowNum + i + 1 + offset, vChild);
}
}
}
super.selectedItems = savedSelection;
//return the number of children that were opened
return _dataDescriptor.getChildren(item).length + offset;
//}
//else {
//if the item was already open then return 0
// return 0;
//}
}
} // end class
} // end package