All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.alee.laf.tree.TreeUtils Maven / Gradle / Ivy

There is a newer version: 1.2.14
Show newest version
/*
 * 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.laf.tree;

import com.alee.api.annotations.NotNull;
import com.alee.api.annotations.Nullable;
import com.alee.extended.tree.AsyncTreeModel;
import com.alee.extended.tree.ExTreeModel;
import com.alee.extended.tree.walker.AsyncTreeWalker;
import com.alee.laf.tree.walker.SimpleTreeWalker;
import com.alee.laf.tree.walker.TreeWalker;

import javax.swing.*;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;

/**
 * This class provides a set of utilities for trees.
 * This is a library utility class and its not intended for use outside of the trees.
 *
 * @author Mikle Garin
 */
public final class TreeUtils
{
    /**
     * todo 1. Change this static utility class into {@link TreeState}-related class
     * todo 2. Proper state restoration for {@link com.alee.extended.tree.WebAsyncTree}
     */

    /**
     * Returns tree expansion and selection states.
     * Tree nodes must be instances of UniqueNode class.
     *
     * @param tree tree to process
     * @return tree expansion and selection states
     */
    @NotNull
    public static TreeState getTreeState ( @NotNull final JTree tree )
    {
        return getTreeState ( tree, true );
    }

    /**
     * Returns tree expansion and selection states.
     * Tree nodes must be instances of UniqueNode class.
     *
     * @param tree          tree to process
     * @param saveSelection whether to save selection states or not
     * @return tree expansion and selection states
     */
    @NotNull
    public static TreeState getTreeState ( @NotNull final JTree tree, final boolean saveSelection )
    {
        return getTreeState ( tree, tree.getModel ().getRoot (), saveSelection );
    }

    /**
     * Returns tree expansion and selection states.
     * Tree nodes must be instances of UniqueNode class.
     *
     * @param tree tree to process
     * @param root node to save state for
     * @return tree expansion and selection states
     */
    @NotNull
    public static TreeState getTreeState ( @NotNull final JTree tree, @Nullable final Object root )
    {
        return getTreeState ( tree, root, true );
    }

    /**
     * Returns tree expansion and selection states.
     * Tree nodes must be instances of UniqueNode class.
     *
     * @param tree          tree to process
     * @param saveSelection whether to save selection states or not
     * @param root          node to save state for
     * @return tree expansion and selection states
     */
    @NotNull
    public static TreeState getTreeState ( @NotNull final JTree tree, @Nullable final Object root, final boolean saveSelection )
    {
        final TreeState state = new TreeState ();
        if ( root != null )
        {
            if ( !( root instanceof UniqueNode ) )
            {
                throw new RuntimeException ( "To get tree state you must use UniqueNode or any class that extends it as tree elements" );
            }
            saveTreeStateImpl ( tree, state, ( UniqueNode ) root, saveSelection );
        }
        return state;
    }

    /**
     * Saves tree expansion and selection states into {@link TreeState}.
     *
     * @param tree          tree to process
     * @param state         {@link TreeState} to save states into
     * @param parent        node to save states for
     * @param saveSelection whether to save selection states or not
     */
    private static void saveTreeStateImpl ( @NotNull final JTree tree, @NotNull final TreeState state, @NotNull final UniqueNode parent,
                                            final boolean saveSelection )
    {
        // Saving children states first
        for ( int i = 0; i < parent.getChildCount (); i++ )
        {
            saveTreeStateImpl ( tree, state, ( UniqueNode ) parent.getChildAt ( i ), saveSelection );
        }

        // Saving parent state
        // We make sure not to save collapsed state for hidden tree root
        // We also make sure not to save selection state if it not explicitly requested
        final TreePath path = new TreePath ( parent.getPath () );
        state.addState ( parent.getId (), new NodeState (
                tree.getModel ().getRoot () == parent && !tree.isRootVisible () || tree.isExpanded ( path ),
                saveSelection && tree.isPathSelected ( path )
        ) );
    }

    /**
     * Restores tree expansion and selection states.
     * Tree nodes must be instances of UniqueNode class.
     *
     * @param tree      tree to process
     * @param treeState tree expansion and selection states
     */
    public static void setTreeState ( @NotNull final JTree tree, @Nullable final TreeState treeState )
    {
        setTreeState ( tree, treeState, tree.getModel ().getRoot (), true );
    }

    /**
     * Restores tree expansion and selection states.
     * Tree nodes must be instances of UniqueNode class.
     *
     * @param tree             tree to process
     * @param treeState        tree expansion and selection states
     * @param restoreSelection whether to restore selection states or not
     */
    public static void setTreeState ( @NotNull final JTree tree, @Nullable final TreeState treeState, final boolean restoreSelection )
    {
        setTreeState ( tree, treeState, tree.getModel ().getRoot (), restoreSelection );
    }

    /**
     * Restores tree expansion and selection states.
     * Tree nodes must be instances of UniqueNode class.
     *
     * @param tree      tree to process
     * @param treeState tree expansion and selection states
     * @param root      node to restore state for
     */
    public static void setTreeState ( @NotNull final JTree tree, @Nullable final TreeState treeState, @Nullable final Object root )
    {
        setTreeState ( tree, treeState, root, true );
    }

    /**
     * Restores tree expansion and selection states.
     * Tree nodes must be instances of UniqueNode class.
     *
     * @param tree             tree to process
     * @param treeState        tree expansion and selection states
     * @param root             node to restore state for
     * @param restoreSelection whether to restore selection states or not
     */
    public static void setTreeState ( @NotNull final JTree tree, @Nullable final TreeState treeState,
                                      @Nullable final Object root, final boolean restoreSelection )
    {
        if ( root != null )
        {
            if ( !( root instanceof UniqueNode ) )
            {
                throw new RuntimeException ( "To set tree state you must use UniqueNode or any class that extends it as tree elements" );
            }
            if ( treeState != null )
            {
                restoreTreeStateImpl ( tree, treeState, ( UniqueNode ) root, restoreSelection );
            }
        }
    }

    /**
     * Restores tree expansion and selection states from {@link TreeState}.
     * todo This method's body can potentially be performed "later" recursively, but it might not always be a desired behavior
     * todo Probably might be added as an option in the tree state restore methods
     *
     * @param tree             tree to process
     * @param treeState        tree expansion and selection states
     * @param parent           node to restore states for
     * @param restoreSelection whether to restore selection states or not
     */
    private static void restoreTreeStateImpl ( @NotNull final JTree tree, @NotNull final TreeState treeState,
                                               @NotNull final UniqueNode parent, final boolean restoreSelection )
    {
        // Restoring children states first
        for ( int i = 0; i < parent.getChildCount (); i++ )
        {
            restoreTreeStateImpl ( tree, treeState, ( UniqueNode ) parent.getChildAt ( i ), restoreSelection );
        }

        // Restoring parent state
        final TreePath path = new TreePath ( parent.getPath () );
        if ( treeState.isExpanded ( parent.getId () ) )
        {
            if ( !tree.isExpanded ( path ) )
            {
                tree.expandPath ( path );
            }
        }
        else
        {
            if ( tree.isExpanded ( path ) )
            {
                tree.collapsePath ( path );
            }
        }
        if ( restoreSelection )
        {
            if ( treeState.isSelected ( parent.getId () ) )
            {
                if ( !tree.isPathSelected ( path ) )
                {
                    tree.addSelectionPath ( path );
                }
            }
            else
            {
                if ( tree.isPathSelected ( path ) )
                {
                    tree.removeSelectionPath ( path );
                }
            }
        }
    }

    /**
     * Returns appropriate {@link TreeWalker} implementation for the specified {@link JTree}.
     *
     * @param tree {@link JTree} to return appropriate {@link TreeWalker} implementation for
     * @param   tree node type
     * @return appropriate {@link TreeWalker} implementation for the specified {@link JTree}
     */
    @NotNull
    public static  TreeWalker getTreeWalker ( @NotNull final JTree tree )
    {
        final TreeWalker treeWalker;
        if ( tree.getModel () instanceof AsyncTreeModel )
        {
            treeWalker = new AsyncTreeWalker ( tree );
        }
        else
        {
            treeWalker = new SimpleTreeWalker ( tree );
        }
        return ( TreeWalker ) treeWalker;
    }

    /**
     * Returns {@link TreePath} from the root to the specified {@link TreeNode}.
     * The last element in the path is the specified {@link TreeNode}.
     *
     * @param node {@link TreeNode} to get {@link TreePath} for
     * @return {@link TreePath} from the root to the specified {@link TreeNode}
     */
    @Nullable
    public static TreePath getTreePath ( @Nullable final TreeNode node )
    {
        final TreePath treePath;
        if ( node != null )
        {
            final TreeNode[] path = getPath ( node );
            if ( path != null )
            {
                treePath = new TreePath ( path );
            }
            else
            {
                treePath = null;
            }
        }
        else
        {
            treePath = null;
        }
        return treePath;
    }

    /**
     * Returns the path from the root, to get to this node.
     * First element in the path is the root and the last element in the path is this node.
     *
     * @param node {@link TreeNode} to get the path for
     * @return array of {@link TreeNode}s representing the path
     */
    @Nullable
    public static TreeNode[] getPath ( @Nullable final TreeNode node )
    {
        return getPathToRoot ( node, 0 );
    }

    /**
     * Builds the parents of node up to and including the root node, where the original node is the last element in the returned array.
     * The length of the returned array gives the node's depth in the tree.
     *
     * @param node  {@link TreeNode} to get the path for
     * @param depth an int giving the number of steps already taken towards the root (on recursive calls), used to size the returned array
     * @return array of {@link TreeNode}s representing the path from the root to the specified {@link TreeNode}
     */
    @Nullable
    private static TreeNode[] getPathToRoot ( @Nullable final TreeNode node, int depth )
    {
        final TreeNode[] path;
        if ( node == null )
        {
            if ( depth != 0 )
            {
                path = new TreeNode[ depth ];
            }
            else
            {
                path = null;
            }
        }
        else
        {
            depth++;
            path = getPathToRoot ( node.getParent (), depth );
            if ( path != null )
            {
                path[ path.length - depth ] = node;
            }
        }
        return path;
    }

    /**
     * Returns whether or not {@code anotherNode} is an ancestor of {@code node}.
     * If {@code anotherNode} is {@code null}, this method returns {@code false}.
     * Note that any node is considered as an ancestor of itself.
     * This operation is at worst O(h) where h is the distance from the root to {@code node}.
     *
     * @param node        tested {@code node}
     * @param anotherNode node to test as an ancestor of {@code node}
     * @return {@code true} if {@code anotherNode} is an ancestor of {@code node}, {@code false} otherwise
     */
    public static boolean isNodeAncestor ( @NotNull final TreeNode node, @Nullable final TreeNode anotherNode )
    {
        boolean nodeAncestor = false;
        if ( anotherNode != null )
        {
            TreeNode ancestor = node;
            do
            {
                if ( ancestor == anotherNode )
                {
                    nodeAncestor = true;
                    break;
                }
            }
            while ( ( ancestor = ancestor.getParent () ) != null );
        }
        return nodeAncestor;
    }

    /**
     * Returns whether or not {@code anotherNode} is an ancestor of {@code node}.
     * If {@code anotherNode} is {@code null}, this method returns {@code false}.
     * Note that any node is considered as an ancestor of itself.
     * This operation is at worst O(h) where h is the distance from the root to {@code node}.
     *
     * Als note that unlike {@link #isNodeAncestor(TreeNode, TreeNode)} this method will make sure it takes all {@link TreeNode}s currently
     * hidden by tree filtering into account meaning result might not be consistent with what you actually see in the tree at the time.
     *
     * @param tree        {@link JTree}  containing specified {@link TreeNode}s
     * @param node        tested {@code node}
     * @param anotherNode node to test as an ancestor of {@code node}
     * @return {@code true} if {@code anotherNode} is an ancestor of {@code node}, {@code false} otherwise
     */
    public static boolean isNodeAncestor ( @NotNull final JTree tree, @NotNull final TreeNode node, @Nullable final TreeNode anotherNode )
    {
        final boolean ancestor;
        final TreeModel model = tree.getModel ();
        if ( model instanceof ExTreeModel )
        {
            boolean isParent = false;
            final ExTreeModel exTreeModel = ( ExTreeModel ) model;
            if ( anotherNode != null )
            {
                TreeNode parent = node;
                do
                {
                    if ( parent == anotherNode )
                    {
                        isParent = true;
                        break;
                    }
                }
                while ( ( parent = exTreeModel.getRawParent ( ( UniqueNode ) parent ) ) != null );
            }
            ancestor = isParent;
        }
        /* todo This should be implemented along with WebAsyncCheckBoxTree
        else if ( model instanceof AsyncTreeModel )
        {
            boolean isParent = false;
            final AsyncTreeModel asyncTreeModel = ( AsyncTreeModel ) model;
            if ( other != null )
            {
                TreeNode parent = node;
                do
                {
                    if ( parent == other )
                    {
                        isParent = true;
                        break;
                    }
                }
                while ( ( parent = asyncTreeModel.getRawParent ( ( UniqueNode ) parent ) ) != null );
            }
            ancestor = isParent;
        }*/
        else
        {
            ancestor = isNodeAncestor ( node, anotherNode );
        }
        return ancestor;
    }

    /**
     * Returns number of levels above the specified {@code node}.
     * It is basically the distance from the root to the specified {@code node}.
     * Returns {@code 0} if {@code node} is the root.
     *
     * @param node {@code node}
     * @return number of levels above the specified {@code node}
     */
    public static int getLevel ( @NotNull final TreeNode node )
    {
        int levels = 0;
        TreeNode ancestor = node;
        while ( ( ancestor = ancestor.getParent () ) != null )
        {
            levels++;
        }
        return levels;
    }

    /**
     * Expands all {@link JTree} nodes in a single call.
     *
     * @param tree {@link JTree} to expand all nodes for
     */
    public static void expandAll ( @NotNull final JTree tree )
    {
        if ( tree instanceof WebTree )
        {
            final WebTree webTree = ( WebTree ) tree;
            webTree.expandAll ();
        }
        else
        {
            int row = 0;
            while ( row < tree.getRowCount () )
            {
                tree.expandRow ( row );
                row++;
            }
        }
    }

    /**
     * Expands all {@link TreeNode}s loaded within {@link JTree} in a single call.
     *
     * @param tree {@link JTree} to expand {@link TreeNode}s for
     */
    public static void expandLoaded ( @NotNull final JTree tree )
    {
        final Object root = tree.getModel ().getRoot ();
        if ( root instanceof TreeNode )
        {
            expandLoaded ( tree, ( TreeNode ) root );
        }
        else
        {
            throw new RuntimeException ( "Specified tree doesn't use TreeNodes: " + tree );
        }
    }

    /**
     * Expands all {@link TreeNode}s loaded within {@link JTree} in a single call.
     *
     * @param tree {@link JTree} to expand {@link TreeNode}s for
     * @param node {@link TreeNode} under which all other {@link TreeNode}s should be expanded
     */
    public static void expandLoaded ( @NotNull final JTree tree, @NotNull final TreeNode node )
    {
        // Only expand parent for non-root nodes
        if ( node.getParent () != null )
        {
            // Make sure this node's parent is expanded
            // We do not need to expand the node itself, we only need to make sure it is visible in the tree
            final TreePath path = getTreePath ( node.getParent () );
            if ( !tree.isExpanded ( path ) )
            {
                tree.expandPath ( path );
            }
        }

        // Expanding all child nodes
        // We are asking node instead of tree model to avoid any additional data loading to occur
        for ( int index = 0; index < node.getChildCount (); index++ )
        {
            expandLoaded ( tree, node.getChildAt ( index ) );
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy