Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Restoring expanded/collapsed tree node states

I was working with the tree state (expanded/selected nodes) saving and made an utility class that can save and restore node states. It works fine.

But still there is one problem with JTree itself - while user is working with some JTree instance (expanding/collapsing nodes) there might be a situation where some node (hidden under another collapsed node) is expanded. Nothing special about it - that is just fine.

JTree keeps records about expanded/collapsed nodes in a separate expandedState Hashtable using node path as key and boolean as expanded state value. So when that expanded node under collapsed parent node will become visible it will still be expanded since there is a record for it in expandedState Hashtable with true value.

Situation explained on screenshots...
1. Expand root and expand some node ("glassfish4" folder) under root:
enter image description here
2. Collapse root:
enter image description here
3. Expand root again and we still see the child node ("glassfish4" folder) expanded:
enter image description here

Imagine that i have saved tree state at the screenshot #2 moment, when root is collapsed - the problem is that if i want to restore all tree node states (even for the hidden ones) i cannot expand a node under another collapsed node because this will force all parent nodes to expand. Also i cannot access expandedState Hashtable to change expanded states directly inside of it since it is declared private in JTree and there are no good ways to access it. So i cannot fully reproduce initial tree state.

So what i can do is:

  1. Forcefully access that Hashtable through reflection - really bad idea
  2. Rewrite JTree nodes expand logic - this is also a bad idea
  3. Restore all expanded states first then restore all collapsed states - that will force tree to do additional pointless repaints and a lot of additional rendering, so that is a really bad workaround i don't want to use

Maybe i am missing something else?

So basically the question is:
Is there any other way to expand the child nodes without causing parent nodes to expand?

You can find a few classes that i use to save/restore tree state below.

Simply call TreeUtils.getTreeState(tree) to retrieve JTree state and TreeUtils.setTreeState(tree,treeState) to restore JTree state. Note that tree must be using UniqueNode, otherwise those methods will throw ClassCastException - you can simply replace DefaultMutableTreeNode with UniqueNode if you have your own nodes extending DefaultMutableTreeNode.

UniqueNode.java - simple node with its own unique ID

public class UniqueNode extends DefaultMutableTreeNode implements Serializable
{
    /**
     * Prefix for node ID.
     */
    private static final String ID_PREFIX = "UN";

    /**
     * Unique node ID.
     */
    protected String id;

    /**
     * Costructs a simple node.
     */
    public UniqueNode ()
    {
        super ();
        setId ();
    }

    /**
     * Costructs a node with a specified user object.
     *
     * @param userObject custom user object
     */
    public UniqueNode ( Object userObject )
    {
        super ( userObject );
        setId ();
    }

    /**
     * Returns node ID and creates it if it doesn't exist.
     *
     * @return node ID
     */
    public String getId ()
    {
        if ( id == null )
        {
            setId ();
        }
        return id;
    }

    /**
     * Changes node ID.
     *
     * @param id new node ID
     */
    public void setId ( String id )
    {
        this.id = id;
    }

    /**
     * Changes node ID to new random ID.
     */
    private void setId ()
    {
        this.id = TextUtils.generateId ( ID_PREFIX );
    }

    /**
     * {@inheritDoc}
     */
    public UniqueNode getParent ()
    {
        return ( UniqueNode ) super.getParent ();
    }

    /**
     * Returns TreePath for this node.
     *
     * @return TreePath for this node
     */
    public TreePath getTreePath ()
    {
        return new TreePath ( getPath () );
    }
}

TreeUtils.java - utility class that saves/loads TreeState from/into JTree

public class TreeUtils
{
    /**
     * 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
     */
    public static TreeState getTreeState ( 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
     */
    public static TreeState getTreeState ( JTree tree, boolean saveSelection )
    {
        TreeState treeState = new TreeState ();

        List<UniqueNode> elements = new ArrayList<UniqueNode> ();
        elements.add ( ( UniqueNode ) tree.getModel ().getRoot () );
        while ( elements.size () > 0 )
        {
            UniqueNode element = elements.get ( 0 );

            TreePath path = new TreePath ( element.getPath () );
            treeState.addState ( element.getId (), tree.isExpanded ( path ), saveSelection && tree.isPathSelected ( path ) );

            for ( int i = 0; i < element.getChildCount (); i++ )
            {
                elements.add ( ( UniqueNode ) element.getChildAt ( i ) );
            }

            elements.remove ( element );
        }

        return treeState;
    }

    /**
     * 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 ( JTree tree, TreeState treeState )
    {
        setTreeState ( tree, treeState, 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 ( JTree tree, TreeState treeState, boolean restoreSelection )
    {
        if ( treeState == null )
        {
            return;
        }

        tree.clearSelection ();

        List<UniqueNode> elements = new ArrayList<UniqueNode> ();
        elements.add ( ( UniqueNode ) tree.getModel ().getRoot () );
        while ( elements.size () > 0 )
        {
            UniqueNode element = elements.get ( 0 );
            TreePath path = new TreePath ( element.getPath () );

            // Restoring expansion states
            if ( treeState.isExpanded ( element.getId () ) )
            {
                tree.expandPath ( path );

                // We are going futher only into expanded nodes, otherwise this will expand even collapsed ones
                for ( int i = 0; i < element.getChildCount (); i++ )
                {
                    elements.add ( ( UniqueNode ) tree.getModel ().getChild ( element, i ) );
                }
            }
            else
            {
                tree.collapsePath ( path );
            }

            // Restoring selection states
            if ( restoreSelection )
            {
                if ( treeState.isSelected ( element.getId () ) )
                {
                    tree.addSelectionPath ( path );
                }
                else
                {
                    tree.removeSelectionPath ( path );
                }
            }

            elements.remove ( element );
        }
    }
}

TreeState.java - container class for the map that holds node states

public class TreeState implements Serializable
{
    /**
     * Tree node states.
     */
    protected Map<String, NodeState> states = new LinkedHashMap<String, NodeState> ();

    /**
     * Constructs new object instance with empty states.
     */
    public TreeState ()
    {
        super ();
    }

    /**
     * Constructs new object instance with specified states.
     *
     * @param states node states
     */
    public TreeState ( Map<String, NodeState> states )
    {
        super ();
        if ( states != null )
        {
            setStates ( states );
        }
    }

    /**
     * Returns all node states.
     *
     * @return all node states
     */
    public Map<String, NodeState> getStates ()
    {
        return states;
    }

    /**
     * Sets all node states.
     *
     * @param states all node states
     */
    public void setStates ( Map<String, NodeState> states )
    {
        this.states = states;
    }

    /**
     * Adds node state.
     *
     * @param nodeId   node ID
     * @param expanded expansion state
     * @param selected selection state
     */
    public void addState ( String nodeId, boolean expanded, boolean selected )
    {
        states.put ( nodeId, new NodeState ( expanded, selected ) );
    }

    /**
     * Returns whether node with the specified ID is expanded or not.
     *
     * @param nodeId node ID
     * @return true if node with the specified ID is expanded, false otherwise
     */
    public boolean isExpanded ( String nodeId )
    {
        final NodeState state = states.get ( nodeId );
        return state != null && state.isExpanded ();
    }

    /**
     * Returns whether node with the specified ID is selected or not.
     *
     * @param nodeId node ID
     * @return true if node with the specified ID is expanded, false otherwise
     */
    public boolean isSelected ( String nodeId )
    {
        final NodeState state = states.get ( nodeId );
        return state != null && state.isSelected ();
    }
}

NodeState.java - single node expansion/selection state

public class NodeState implements Serializable
{
    /**
     * Whether node is expanded or not.
     */
    protected boolean expanded;

    /**
     * Whether node is selected or not.
     */
    protected boolean selected;

    /**
     * Constructs empty node state.
     */
    public NodeState ()
    {
        super ();
        this.expanded = false;
        this.selected = false;
    }

    /**
     * Constructs node state with the specified expansion and selection states.
     *
     * @param expanded expansion state
     * @param selected selection state
     */
    public NodeState ( boolean expanded, boolean selected )
    {
        super ();
        this.expanded = expanded;
        this.selected = selected;
    }

    /**
     * Returns whether node is expanded or not.
     *
     * @return true if node is expanded, false otherwise
     */
    public boolean isExpanded ()
    {
        return expanded;
    }

    /**
     * Sets whether node is expanded or not.
     *
     * @param expanded whether node is expanded or not
     */
    public void setExpanded ( boolean expanded )
    {
        this.expanded = expanded;
    }

    /**
     * Returns whether node is selected or not.
     *
     * @return true if node is selected, false otherwise
     */
    public boolean isSelected ()
    {
        return selected;
    }

    /**
     * Sets whether node is selected or not.
     *
     * @param selected whether node is selected or not
     */
    public void setSelected ( boolean selected )
    {
        this.selected = selected;
    }
}

By the way, setTreeState method avoids restoring expanded states under collapsed nodes at the moment:

        // Restoring expansion states
        if ( treeState.isExpanded ( element.getId () ) )
        {
            tree.expandPath ( path );

            // We are going futher only into expanded nodes, otherwise this will expand even collapsed ones
            for ( int i = 0; i < element.getChildCount (); i++ )
            {
                elements.add ( ( UniqueNode ) tree.getModel ().getChild ( element, i ) );
            }
        }
        else
        {
            tree.collapsePath ( path );
        }

Method that gathers child nodes called only if the parent node is expanded. So all child nodes under collapsed nodes are ignored. If you change that behavior you will see the problem i described in the beginning of this question - parent nodes will get expanded.

like image 919
Mikle Garin Avatar asked Nov 12 '22 23:11

Mikle Garin


1 Answers

Why not restore the state by performing the same actions as described, first set the sub nodes to expanded then set their parent node to collapsed as necessary?

The only difference to your current code is to use two iterations instead of one. First iterate and expand where desired, then iterate and collapse where desired.

The tree should paint a single time anyway due to the repaint logic.

like image 178
Holger Avatar answered Nov 14 '22 23:11

Holger