com.alee.extended.tree.ExTreeModel Maven / Gradle / Ivy
/*
* This file is part of WebLookAndFeel library.
*
* WebLookAndFeel library is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* WebLookAndFeel library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with WebLookAndFeel library. If not, see .
*/
package com.alee.extended.tree;
import com.alee.laf.tree.TreeState;
import com.alee.laf.tree.UniqueNode;
import com.alee.laf.tree.WebTree;
import com.alee.laf.tree.WebTreeModel;
import com.alee.utils.CollectionUtils;
import com.alee.utils.MapUtils;
import com.alee.utils.SwingUtils;
import com.alee.utils.collection.DoubleMap;
import com.alee.utils.compare.Filter;
import javax.swing.tree.MutableTreeNode;
import javax.swing.tree.TreeNode;
import java.util.*;
/**
* @author Mikle Garin
* @see com.alee.extended.tree.WebExTree
* @see com.alee.extended.tree.ExTreeDataProvider
*/
public class ExTreeModel extends WebTreeModel
{
/**
* Ex tree that uses this model.
*/
protected final WebTree tree;
/**
* Ex tree data provider.
*/
protected final ExTreeDataProvider dataProvider;
/**
* Root node cache.
* Cached when root is requested for the first time.
*/
protected E rootNode = null;
/**
* Lock object for cache changes.
*/
protected final Object cacheLock = new Object ();
/**
* Nodes cached states (parent ID -> children cached state).
* If child nodes for some parent node are cached then this map contains "true" value under that parent node ID as a key.
*/
protected final Map nodeCached = new HashMap ();
/**
* Cache for children nodes returned by data provider (parent ID -> list of raw child nodes).
* This map contains raw children which weren't affected by sorting and filtering operations.
* If children needs to be re-sorted or re-filtered they are simply taken from the cache and re-organized once again.
*/
protected final Map> rawNodeChildrenCache = new HashMap> ();
/**
* Direct nodes cache (node ID -> node).
* Used for quick node search within the tree.
*/
protected final DoubleMap nodeById = new DoubleMap ();
/**
* Constructs default ex tree model using custom data provider.
*
* @param tree asynchronous tree
* @param dataProvider data provider
*/
public ExTreeModel ( final WebTree tree, final ExTreeDataProvider dataProvider )
{
super ( null );
this.tree = tree;
this.dataProvider = dataProvider;
loadTreeData ( getRootNode () );
}
/**
* Returns ex tree data provider.
*
* @return data provider
*/
public ExTreeDataProvider getDataProvider ()
{
return dataProvider;
}
/**
* Returns tree root node.
*
* @return root node
*/
@Override
public E getRoot ()
{
if ( rootNode == null )
{
// Retrieving and caching root node
rootNode = dataProvider.getRoot ();
// Caching root node by ID
cacheNodeById ( rootNode );
}
return rootNode;
}
/**
* Returns whether the specified node is leaf or not.
*
* @param node node
* @return true if node is leaf, false otherwise
*/
@Override
public boolean isLeaf ( final Object node )
{
return dataProvider.isLeaf ( ( E ) node );
}
/**
* Returns children count for specified node.
*
* @param parent parent node
* @return children count
*/
@Override
public int getChildCount ( final Object parent )
{
final E node = ( E ) parent;
if ( isLeaf ( node ) )
{
return 0;
}
else if ( areChildrenLoaded ( node ) )
{
return super.getChildCount ( parent );
}
else
{
return loadChildren ( node );
}
}
/**
* Returns child node for parent node at the specified index.
*
* @param parent parent node
* @param index child node index
* @return child node
*/
@Override
public E getChild ( final Object parent, final int index )
{
return ( E ) super.getChild ( parent, index );
}
/**
* Returns whether children for the specified node are already loaded or not.
*
* @param node node to process
* @return true if children for the specified node are already loaded, false otherwise
*/
public boolean areChildrenLoaded ( final E node )
{
synchronized ( cacheLock )
{
final Boolean cached = nodeCached.get ( node.getId () );
return cached != null && cached;
}
}
/**
* Forces model to cache the whole structure so any node can be accessed right away.
* Note that this might take some time in case tree structure is large as it will be fully loaded.
* Though this doesn't force any repaints or other visual updates, so the speed depends only on ExTreeDataProvider.
*
* This method is mostly used to ensure that at any given time {@link com.alee.extended.tree.WebExTree} has all of its nodes.
* That heavily simplifies work with the tree in case you need to access random nodes in the tree directly.
* In case this is not your goal it is probably better to use {@link com.alee.extended.tree.WebAsyncTree}.
*
* @param node node to load data for
*/
protected void loadTreeData ( final E node )
{
// Simply retrieving children count
// This method uses cache so it won't force children reload when it is not needed
getChildCount ( node );
}
/**
* Reloads node children.
*
* @param node node
*/
@Override
public void reload ( final TreeNode node )
{
final E reloadedNode = ( E ) node;
// Cancels tree editing
tree.cancelEditing ();
// Cleaning up nodes cache
clearNodeChildrenCache ( reloadedNode, false );
// Removing all old children if such exist
// We don't need to inform about child nodes removal here due to later structural update call
reloadedNode.removeAllChildren ();
// Forcing children reload
super.reload ( reloadedNode );
// Forcing structure reload
loadTreeData ( reloadedNode );
}
/**
* Clears node and all of its child nodes children cached states.
*
* @param node node to clear cache for
* @param clearNode whether should clear node cache or not
*/
protected void clearNodeChildrenCache ( final E node, final boolean clearNode )
{
synchronized ( cacheLock )
{
// Clears node cache
if ( clearNode )
{
nodeById.remove ( node.getId () );
}
// Clears node children cached state
nodeCached.remove ( node.getId () );
// Clears node raw children cache
final List children = rawNodeChildrenCache.remove ( node.getId () );
// Clears chld nodes cache
if ( children != null )
{
clearNodeChildrenCache ( children, true );
}
}
}
/**
* Clears nodes children cached states.
*
* @param nodes nodes to clear cache for
* @param clearNodes whether should clear nodes cache or not
*/
protected void clearNodeChildrenCache ( final List nodes, final boolean clearNodes )
{
synchronized ( cacheLock )
{
for ( final E node : nodes )
{
clearNodeChildrenCache ( node, clearNodes );
}
}
}
/**
* Clears nodes children cached states.
*
* @param nodes nodes to clear cache for
* @param clearNodes whether should clear nodes cache or not
*/
protected void clearNodeChildrenCache ( final E[] nodes, final boolean clearNodes )
{
synchronized ( cacheLock )
{
for ( final E node : nodes )
{
clearNodeChildrenCache ( node, clearNodes );
}
}
}
/**
* Caches node by its IDs.
*
* @param node node to cache
*/
protected void cacheNodeById ( final E node )
{
synchronized ( cacheLock )
{
nodeById.put ( node.getId (), node );
}
}
/**
* Caches nodes by their IDs.
*
* @param nodes list of nodes to cache
*/
protected void cacheNodesById ( final List nodes )
{
synchronized ( cacheLock )
{
for ( final E node : nodes )
{
nodeById.put ( node.getId (), node );
}
}
}
/**
* Loads (or reloads) node children and returns zero or children count if async mode is off.
* This is base method that uses installed AsyncTreeDataProvider to retrieve tree node children.
*
* @param parent node to load children for
* @return zero or children count if async mode is off
* @see AsyncTreeDataProvider#loadChildren(AsyncUniqueNode, ChildrenListener)
*/
protected int loadChildren ( final E parent )
{
// Loading children
final List children = dataProvider.getChildren ( parent );
// Caching raw children
synchronized ( cacheLock )
{
rawNodeChildrenCache.put ( parent.getId (), children );
cacheNodesById ( children );
}
// Filtering and sorting raw children
final List realChildren = filterAndSort ( parent, children );
// Updating cache
synchronized ( cacheLock )
{
nodeCached.put ( parent.getId (), true );
}
// Checking if any nodes loaded
if ( realChildren != null && realChildren.size () > 0 )
{
// Inserting loaded nodes
insertNodesIntoImpl ( realChildren, parent, 0 );
}
return parent.getChildCount ();
}
/**
* Sets child nodes for the specified node.
* This method might be used to manually change tree node children without causing any structure corruptions.
*
* @param parent node to process
* @param children new node children
*/
public void setChildNodes ( final E parent, final List children )
{
// Caching raw children
synchronized ( cacheLock )
{
rawNodeChildrenCache.put ( parent.getId (), children );
cacheNodesById ( children );
}
// Filtering and sorting raw children
final List realChildren = filterAndSort ( parent, children );
// Updating cache
synchronized ( cacheLock )
{
nodeCached.put ( parent.getId (), true );
}
// Performing UI updates in EDT
SwingUtils.invokeLater ( new Runnable ()
{
@Override
public void run ()
{
// Checking if any nodes loaded
if ( realChildren != null && realChildren.size () > 0 )
{
// Clearing raw nodes cache
// That might be required in case nodes were moved inside of the tree
clearNodeChildrenCache ( children, false );
// Inserting nodes
insertNodesIntoImpl ( realChildren, parent, 0 );
}
}
} );
}
/**
* Adds child nodes for the specified node.
* This method might be used to manually change tree node children without causing any structure corruptions.
*
* @param parent node to process
* @param children new node children
*/
public void addChildNodes ( final E parent, final List children )
{
// Adding new raw children
synchronized ( cacheLock )
{
List cachedChildren = rawNodeChildrenCache.get ( parent.getId () );
if ( cachedChildren == null )
{
cachedChildren = new ArrayList ( children.size () );
rawNodeChildrenCache.put ( parent.getId (), cachedChildren );
}
cachedChildren.addAll ( children );
cacheNodesById ( children );
}
// Clearing nodes cache
// That might be required in case nodes were moved inside of the tree
clearNodeChildrenCache ( children, false );
// Inserting nodes
insertNodesIntoImpl ( children, parent, parent.getChildCount () );
// Updating parent node sorting and filtering
updateSortingAndFiltering ( parent );
}
/**
* Removes specified node from parent node.
*
* @param node node to remove
*/
@Override
public void removeNodeFromParent ( final MutableTreeNode node )
{
// Simply ignore null nodes
if ( node == null )
{
return;
}
final E childNode = ( E ) node;
final E parentNode = ( E ) childNode.getParent ();
// Simply ignore if parent node is null
if ( parentNode == null )
{
return;
}
// Removing raw children
synchronized ( cacheLock )
{
final List children = rawNodeChildrenCache.get ( parentNode.getId () );
if ( children != null )
{
children.remove ( childNode );
}
}
// Clearing node cache
clearNodeChildrenCache ( childNode, true );
// Removing node children so they won't mess up anything when we place node back into tree
childNode.removeAllChildren ();
// Removing node from parent
super.removeNodeFromParent ( node );
// Updating parent node sorting and filtering
updateSortingAndFiltering ( parentNode );
}
// todo Implement when those methods will be separate from single one
// public void removeNodesFromParent ( List nodes )
// {
// super.removeNodesFromParent ( nodes );
// }
//
// public void removeNodesFromParent ( E[] nodes )
// {
// super.removeNodesFromParent ( nodes );
// }
/**
* Inserts new child node into parent node at the specified index.
*
* @param newChild new child node
* @param parent parent node
* @param index insert index
*/
@Override
public void insertNodeInto ( final MutableTreeNode newChild, final MutableTreeNode parent, final int index )
{
final E childNode = ( E ) newChild;
final E parentNode = ( E ) parent;
// Inserting new raw children
synchronized ( cacheLock )
{
List children = rawNodeChildrenCache.get ( parentNode.getId () );
if ( children == null )
{
children = new ArrayList ( 1 );
rawNodeChildrenCache.put ( parentNode.getId (), children );
}
children.add ( index, childNode );
cacheNodeById ( childNode );
}
// Clearing node cache
// That might be required in case nodes were moved inside of the tree
clearNodeChildrenCache ( childNode, false );
// Inserting node
insertNodeIntoImpl ( childNode, parentNode, index );
// Updating parent node sorting and filtering
updateSortingAndFiltering ( parentNode );
}
/**
* Inserts a list of child nodes into parent node.
*
* @param children list of new child nodes
* @param parent parent node
* @param index insert index
*/
@Override
public void insertNodesInto ( final List children, final E parent, final int index )
{
// Inserting new raw children
synchronized ( cacheLock )
{
List cachedChildren = rawNodeChildrenCache.get ( parent.getId () );
if ( cachedChildren == null )
{
cachedChildren = new ArrayList ( 1 );
rawNodeChildrenCache.put ( parent.getId (), cachedChildren );
}
cachedChildren.addAll ( index, children );
cacheNodesById ( children );
}
// Clearing nodes cache
// That might be required in case nodes were moved inside of the tree
clearNodeChildrenCache ( children, false );
// Performing actual nodes insertion
insertNodesIntoImpl ( children, parent, index );
// Updating parent node sorting and filtering
updateSortingAndFiltering ( parent );
}
/**
* Inserts an array of child nodes into parent node.
*
* @param children array of new child nodes
* @param parent parent node
* @param index insert index
*/
@Override
public void insertNodesInto ( final E[] children, final E parent, final int index )
{
// Inserting new raw children
synchronized ( cacheLock )
{
List cachedChildren = rawNodeChildrenCache.get ( parent.getId () );
if ( cachedChildren == null )
{
cachedChildren = new ArrayList ( 1 );
rawNodeChildrenCache.put ( parent.getId (), cachedChildren );
}
for ( int i = children.length - 1; i >= 0; i-- )
{
cachedChildren.add ( index, children[ i ] );
}
cacheNodesById ( Arrays.asList ( children ) );
}
// Clearing nodes cache
// That might be required in case nodes were moved inside of the tree
clearNodeChildrenCache ( children, false );
// Inserting nodes
insertNodesIntoImpl ( children, parent, index );
// Updating parent node sorting and filtering
updateSortingAndFiltering ( parent );
}
/**
* Inserts a child node into parent node.
*
* @param child new child node
* @param parent parent node
* @param index insert index
*/
protected void insertNodeIntoImpl ( final E child, final E parent, final int index )
{
super.insertNodeInto ( child, parent, index );
// Forcing child node to load its structure
loadTreeData ( child );
}
/**
* Inserts a list of child nodes into parent node.
*
* @param children list of new child nodes
* @param parent parent node
* @param index insert index
*/
protected void insertNodesIntoImpl ( final List children, final E parent, final int index )
{
super.insertNodesInto ( children, parent, index );
// Forcing child nodes to load their structures
for ( final E child : children )
{
loadTreeData ( child );
}
}
/**
* Inserts an array of child nodes into parent node.
*
* @param children array of new child nodes
* @param parent parent node
* @param index insert index
*/
protected void insertNodesIntoImpl ( final E[] children, final E parent, final int index )
{
super.insertNodesInto ( children, parent, index );
// Forcing child nodes to load their structures
for ( final E child : children )
{
loadTreeData ( child );
}
}
/**
* Updates nodes sorting and filtering for all nodes.
*/
public void updateSortingAndFiltering ()
{
updateSortingAndFiltering ( getRoot (), true );
}
/**
* Updates sorting and filtering for the specified node children.
*
* @param parentNode node which children sorting and filtering should be updated
*/
public void updateSortingAndFiltering ( final E parentNode )
{
updateSortingAndFiltering ( parentNode, false );
}
/**
* Updates sorting and filtering for the specified node children.
*
* @param parentNode node which children sorting and filtering should be updated
* @param recursively whether should update the whole children structure recursively or not
*/
public void updateSortingAndFiltering ( final E parentNode, final boolean recursively )
{
// Process only this is not a root node
// We don't need to update root sorting as there is always one root in the tree
if ( parentNode != null )
{
performSortingAndFiltering ( parentNode, recursively );
}
}
/**
* Updates node children using current comparator and filter.
* Updates the whole node children structure if recursive update requested.
*
* @param parentNode node which children sorting and filtering should be updated
* @param recursively whether should update the whole children structure recursively or not
*/
protected void performSortingAndFiltering ( final E parentNode, final boolean recursively )
{
// todo Restore tree state only for the updated node
// Saving tree state to restore it right after children update
final TreeState treeState = tree.getTreeState ();
// Updating root node children
if ( recursively )
{
performSortingAndFilteringRecursivelyImpl ( parentNode );
}
else
{
performSortingAndFilteringImpl ( parentNode );
}
nodeStructureChanged ( parentNode );
// Restoring tree state including all selections and expansions
tree.setTreeState ( treeState );
}
/**
* Updates node children using current comparator and filter.
*
* @param parentNode node to update
*/
protected void performSortingAndFilteringRecursivelyImpl ( final E parentNode )
{
performSortingAndFilteringImpl ( parentNode );
for ( int i = 0; i < parentNode.getChildCount (); i++ )
{
performSortingAndFilteringRecursivelyImpl ( ( E ) parentNode.getChildAt ( i ) );
}
}
/**
* Updates node children recursively using current comparator and filter.
*
* @param parentNode node to update
*/
protected void performSortingAndFilteringImpl ( final E parentNode )
{
// Retrieving raw children
final List children = rawNodeChildrenCache.get ( parentNode.getId () );
// Process this action only if node children are already loaded and cached
if ( children != null )
{
// Removing old children
parentNode.removeAllChildren ();
// Filtering and sorting raw children
final List realChildren = filterAndSort ( parentNode, children );
// Inserting new children
for ( final E child : realChildren )
{
parentNode.add ( child );
}
}
}
/**
* Returns list of filtered and sorted raw children.
*
* @param parentNode parent node
* @param children children to filter and sort
* @return list of filtered and sorted children
*/
protected List filterAndSort ( final E parentNode, List children )
{
// Simply return an empty array if there is no children
if ( children == null || children.size () == 0 )
{
return new ArrayList ( 0 );
}
// Filter and sort children
final Filter filter = dataProvider.getChildrenFilter ( parentNode );
final Comparator comparator = dataProvider.getChildrenComparator ( parentNode );
if ( filter != null )
{
final List filtered = CollectionUtils.filter ( children, filter );
if ( comparator != null )
{
Collections.sort ( filtered, comparator );
}
return filtered;
}
else
{
if ( comparator != null )
{
children = CollectionUtils.copy ( children );
Collections.sort ( children, comparator );
}
return children;
}
}
/**
* Looks for the node with the specified ID in the tree model and returns it or null if it was not found.
*
* @param nodeId node ID
* @return node with the specified ID or null if it was not found
*/
public E findNode ( final String nodeId )
{
return nodeById.get ( nodeId );
}
/**
* Returns nodes cache map copy.
*
* @return nodes cache map copy
*/
public DoubleMap getNodesCache ()
{
return MapUtils.copyDoubleMap ( nodeById );
}
}