Restore Expansion/Selection State in JTree with Lazy Loading
Asked Answered
D

1

6

I have an application which contains a JTree backed with a DefaultTreeModel that is used to display a hierarchy of files which reflects a section of the file system on my server (I will refer to this as my client app). I also have a server application which provides the data that my client app needs to display (I will refer to this as my server app). I am using a "lazily load children" approach so that I only have to load files into my tree if the user is interested in them. Lazy load approach:

  1. I override treeWillExpand(TreeExpansionEvent evt)
  2. I set the selection path to be that of the expanding node.
  3. Then I send a message to the server asking for the children of that node.
  4. When the server responds I get the last selected path component.
  5. Then I use DefaultTreeModel.insertNodeInto() for each the returned data files.
  6. Finally I call DefaultTreeModel.nodeStructureChanged().

The above works fine and I am not having any issues with lazily loading the children. My problem comes when new data is uploaded to the server and I want to update the tree to not only include the new data, but also to set the expansion state and selected node to what it was prior to updating the tree (so that the user is not jerked around on the tree just because there is new data to view). The flow is as follows:

  1. New data is uploaded to the server
  2. Server app archives this data and populates a database with information about the uploaded files.
  3. Server app notifies client app that new data was uploaded.
  4. Client app saves the expansion state of the tree using JTree.getExpandedDescendants()
  5. Client app saves the selection path of the tree using JTree.getSelectionPath()
  6. Client app removes all nodes from the DefaultTreeModel.
  7. Client app requests data from the server starting with the root node.
  8. Client app traverses the tree path enumeration returned from JTree.getExpandedDescendants() calling JTree.expandPath() on each TreePath in the enumeration.
  9. Client app sets the selected tree path.

My problem is that no matter what I try the tree's GUI is not updated to reflect the expansion state. I know that my call to expandPath is working because I can see the request for data sent from the client and the response with data from the server for each call to expandPath. I also display information about the currently selected node in another window and it is showing the correctly selected node. But, alas, to my disappointment, the GUI only displays the root node (expanded) and it's children (not expanded) instead of the previous expanded state.

So my question is: How can I restore the expansion state of my JTree so that the GUI remains the same before and after a data model update?

These are a few of the things that I have tried:

  • I found a thread with a similar setup to mine and his problem was solved by overriding equals() and hashCode() but that did not work for me.
  • Various methods to invoke expansion such as: setExpandsSelectedPaths(true), nodeStructureChanged(), JTree.invalidate()
  • Many different variations on saving the expansion state, however, I don't believe the expansion state is incorrect as I can see the correct data being passed back and forth between the client app and the server app.

Here is my SSCCE:

package tree.sscce;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import java.awt.BorderLayout;
import javax.swing.JScrollPane;
import javax.swing.JTree;
import javax.swing.JButton;
import java.util.Enumeration;
import javax.swing.BoxLayout;
import javax.swing.event.TreeExpansionEvent;
import javax.swing.event.TreeWillExpandListener;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.ExpandVetoException;
import javax.swing.tree.MutableTreeNode;
import javax.swing.tree.TreePath;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
import javax.swing.JTextPane;

public class TreeSSCCE extends JFrame implements TreeWillExpandListener {

private static final long serialVersionUID = -1930472429779070045L;

public static void main(String[] args) 
{
    SwingUtilities.invokeLater(new Runnable() {
        public void run() {
            TreeSSCCE inst = new TreeSSCCE();
            inst.setLocationRelativeTo(null);
            inst.setVisible(true);
            inst.setDefaultCloseOperation(EXIT_ON_CLOSE);
        }           
    });
}

private DefaultMutableTreeNode rootNode;
private JTree tree;
private DefaultTreeModel treeModel;
private TreePath selectionPathPriorToNewData;
private Enumeration<TreePath> expandedPathsPriorToNewData;
private int treeSize = 5;

public TreeSSCCE() {

    this.setBounds(0, 0, 500, 400);
    JPanel mainPanel = new JPanel();
    getContentPane().add(mainPanel, BorderLayout.CENTER);
    mainPanel.setBounds(0, 0, 500, 400);
    mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));

    JPanel descriptionPanel = new JPanel();
    descriptionPanel.setBounds(0, 0, 500, 200);
    mainPanel.add(descriptionPanel);

    JTextPane textPane = new JTextPane();
    String newLine = System.getProperty("line.separator");
    descriptionPanel.setLayout(new BorderLayout(0, 0));
    textPane.setText("Start by expanding some nodes then click 'Add New Data' and you will notice that the tree state is not retained.");
    descriptionPanel.add(textPane);

    // Initialize The Tree
    tree = new JTree();
    rootNode = new DefaultMutableTreeNode("Root");
    treeModel = new DefaultTreeModel(rootNode);
    tree.addTreeWillExpandListener(this);
    tree.setModel(treeModel);
    tree.setShowsRootHandles(true);
    populateTree(false);

    JScrollPane scrollPane = new JScrollPane(tree);
    mainPanel.add(scrollPane);

    JPanel buttonPanel = new JPanel();
    mainPanel.add(buttonPanel);

    JButton btnAddNewData = new JButton("Add New Data");
    btnAddNewData.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent arg0) {
            addNewDataToTree();
        }
    });
    buttonPanel.add(btnAddNewData);


}

private void removeAllTreeNodes()
{

    while(!treeModel.isLeaf(treeModel.getRoot()))
    {
        treeModel.removeNodeFromParent((MutableTreeNode)treeModel.getChild(treeModel.getRoot(),0));
    }
    treeModel = null;
    treeModel = new DefaultTreeModel(rootNode);
    tree.setModel(treeModel);
}

public void restoreExpansionState(Enumeration enumeration)
{
    if (enumeration != null) 
    {
        while (enumeration.hasMoreElements()) 
        {
            TreePath treePath = (TreePath) enumeration.nextElement();
            tree.expandPath(treePath);
            tree.setSelectionPath(treePath);
        }
        tree.setSelectionPath(selectionPathPriorToNewData);
    }
}

protected void addNewDataToTree() 
{
    // save the tree state
    selectionPathPriorToNewData = tree.getSelectionPath();
    expandedPathsPriorToNewData = tree.getExpandedDescendants(new TreePath(tree.getModel().getRoot()));
    removeAllTreeNodes();
    populateTree(true);
    restoreExpansionState(expandedPathsPriorToNewData);
}

private void populateTree(boolean newData)
{
    if(newData)
        treeSize++;
    MyParentNode[] parents = new MyParentNode[treeSize];
    for(int i = 0; i < treeSize; i++)
    {
        parents[i] = new MyParentNode("Parent [" + i + "]");
        treeModel.insertNodeInto(parents[i], rootNode, i);
    }
}

@Override
public void treeWillCollapse(TreeExpansionEvent evt) throws ExpandVetoException {
    // Not used.
}

@Override
public void treeWillExpand(TreeExpansionEvent evt) throws ExpandVetoException 
{
    System.out.println("Tree expanding: " + evt.getPath());
    tree.setExpandsSelectedPaths(true);
    tree.setSelectionPath(evt.getPath());
    // we have already loaded the top-level items below root when we
    // connected so lets just return...
    if(evt.getPath().getLastPathComponent().equals(treeModel.getRoot()))
        return;

    // if this is not root lets figure out what we need to do.
    DefaultMutableTreeNode expandingNode = (DefaultMutableTreeNode) evt.getPath().getLastPathComponent();

    // if this node already has children then we don't want to reload so lets return;
    if(expandingNode.getChildCount() > 0)
        return;     
    // if this node has no children then lets add some
    MyParentNode mpn = new MyParentNode("Parent Under " + expandingNode.toString());
    treeModel.insertNodeInto(mpn, expandingNode, expandingNode.getChildCount());
    for(int i = 0; i < 3; i++)
    {
        treeModel.insertNodeInto(new DefaultMutableTreeNode("Node [" + i + "]"), mpn, i);
    }
}

private class MyParentNode extends DefaultMutableTreeNode
{
    private static final long serialVersionUID = 433317389888990065L;
    private String name = "";

    public MyParentNode(String _name)
    {
        super(_name);
        name = _name;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + getOuterType().hashCode();
        result = prime * result + ((name == null) ? 0 : name.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        MyParentNode other = (MyParentNode) obj;
        if (!getOuterType().equals(other.getOuterType()))
            return false;
        if (name == null) {
            if (other.name != null)
                return false;
        } else if (!name.equals(other.name))
            return false;
        return true;
    }

    @Override
    public boolean isLeaf()
    {
        return false;
    }

    private TreeSSCCE getOuterType() {
        return TreeSSCCE.this;
    }       
}

}

Thanks in advance for any help you can provide.

P.S. This is my first question so please let me know if I am asking properly (and take it easy on me ;) ).

Disingenuous answered 22/2, 2013 at 3:48 Comment(7)
Wow, +1 for lots of useful info upfront! I suspect that you're going to have to reconstruct a tree path using the new nodes, based off the old nodes. I assume you have some way to identifing each node? Generate a simple String (for example), that represents all the expanded paths. When you come to update the tree, using these String paths, build new tree paths from the available nodes...Catarina
The FileTreeModel cited here may simplify your code, if not eliminate the problem.Valvular
@Valvular The FileTreeModel won't work for me as my nodes are custom objects that have information about the file as well as application specific information. Also, my client app doesn't have direct access to these file as the reside on the server.Disingenuous
@Catarina My problem isn't building the paths that seems to be working. The real crux of the issue is displaying the tree in the expanded state. Swing doesn't seem to play nice with my tree state for some reason.Disingenuous
is there real reason to recreating whole DefaultTreeModel, not, if yes then all notifiers expired, are gone aways, this code doesn't inserted a new XxxNode on proper coordinated, have to override TreeModelListener and to store last path in variable (after recreating model) and iterating into new model then to expand corresponding path, notice don't to confuse users, lets that for users action, notify them about changes,Saluki
Jeremy did you fix this?Bourg
Check this: algosome.com/articles/save-jtree-expand-state.htmlShut
G
2

I am using a custom tree model (extends DefaultTreeModel) and reacting in the DBTreeEvent.STRUCTURE_CHANGED event to handle this. This is what I do to preserve the old state. Not sure if it will help you or not..

//NOTE: node is the tree node that caused the tree event
TreePath nodesPath = new TreePath(node.getPath());
TreePath currentSel = myTree.getLeadSelectionPath();
List<TreePath> currOpen  = getCurrExpandedPaths(nodesPath);
super.nodeStructureChanged(node);
reExpandPaths(currOpen);
myTree.setSelectionPath(currentSel);

private List<TreePath> getCurrExpandedPaths(TreePath currPath)
{
    List<TreePath> paths = new ArrayList<TreePath>();
    Enumeration<TreePath> expandEnum = myTree.getExpandedDescendants(currPath);
    if (expandEnum == null)
        return null;

    while (expandEnum.hasMoreElements())
        paths.add(expandEnum.nextElement());

    return paths;
}

private void reExpandPaths(List<TreePath> expPaths)
{
    if(expPaths == null)
        return;
    for(TreePath tp : expPaths)
        myTree.expandPath(tp);
}
Geronimo answered 5/8, 2013 at 18:22 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.