Candamir
Candamir

Reputation: 596

Highlighting a JTree node works under the hood, but not visually

In my application, I show a JTree on the left side and if the user double clicks on a leaf, the corresponding data is loaded on the right side. At the time of loading this data, I (i) save the existing document (not relevant here), (ii) update the tree to account for any changes that may have happened, (iii) make sure the right node is selected in the tree after updating (i.e. the node the user double-clicked on) and (iv) load the selected node. The application logic works fine, i.e. the correct file is loaded and we therefore know that the correct node is selected in the tree, but visually no node is selected at all after the aforementioned steps.

I am aware of this question but the problem there seems to have been that the tree was not in focus. I have already tried the different remedies suggested in that post but have not been able to solve my problem. (There is also this related forum thread, although the site seems to be down right now. Furthermore, this question seems to be similar on the surface but the problem there stems from OP building a proprietary renderer.)

Please see below my code; I tried to reduce it to a SSCCE but I'm still stuck. My best guess at this time is that the problem has to do with the fact that a completely new TreeModel is created and loaded into the tree every time updateTree is called and that this somehow makes it impossible to visually select the right node. If this were indeed to be the case, then one potential solution would be to alter the TreeModel instead of re-creating it from scratch but (i) that's less convenient for me and (ii) I believe this presents an interesting problem in its own right.

public class JTree_Problem_SSCCE
extends JFrame
{
    private final JTree tree;

    public JTree_Problem_SSCCE()
    {
        super("XYZ");

        // Tree to select data
        DefaultTreeModel treeModel = getTreeModel();

        this.tree = new JTree(treeModel);
        // I don't allow the user to select more than one node at a time but I can reproduce the problem with or without this
        //TreeSelectionModel tsm = new DefaultTreeSelectionModel();
        //tsm.setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
        //tree.setSelectionModel(tsm);
        tree.addMouseListener(new TreeMouseAdapter());
        expandAllTreeNodes();
        getContentPane().add(tree,BorderLayout.WEST);

        setLocation(25,25);
        setSize(1700,1000);
        setVisible(true);
    }

    private DefaultTreeModel getTreeModel()
    {
        DefaultMutableTreeNode n1 = new DefaultMutableTreeNode("Root");
        DefaultMutableTreeNode n2 = new DefaultMutableTreeNode("Child 1");
        DefaultMutableTreeNode n3 = new DefaultMutableTreeNode("Child 2");
        n1.add(n2);
        n1.add(n3);
        return new DefaultTreeModel(n1);
    }

    private void updateTree(DefaultMutableTreeNode treeNode)
    {
        DefaultTreeModel dtm = getTreeModel();
        tree.setModel(dtm);
        expandAllTreeNodes();

        // No idea why the below doesn't work visually (the application logic works just fine but no tree node is actually selected)
        System.err.println(tree.getExpandsSelectedPaths()); // always returns true (I've seen this to be the problem in other questions)
        System.err.println(new TreePath(((DefaultTreeModel)tree.getModel()).getPathToRoot(treeNode)));
        if (treeNode != null)
        {
            tree.setSelectionPath(new TreePath(((DefaultTreeModel)tree.getModel()).getPathToRoot(treeNode)));
        }

        // As recommended in the answers here (https://stackoverflow.com/q/8896678/8031521),
        // I have tried the below but to no avail
//        tree.requestFocus(); // I have also tried requestFocusInWindow
//        SwingUtilities.invokeLater(new Runnable() {
//            @Override
//            public void run()
//            {
//                tree.setSelectionPath(new TreePath(((DefaultTreeModel)tree.getModel()).getPathToRoot(treeNode)));
//            }
//        });
    }

    private void expandAllTreeNodes()
    {
        // Expand all the nodes in the tree
        // See https://stackoverflow.com/a/15211697/8031521
        for (int i = 0; i < tree.getRowCount(); i++)
        {
            tree.expandRow(i);
        }
    }

    class TreeMouseAdapter
    extends MouseAdapter
    {
        @Override
        public void mouseClicked(MouseEvent e)
        {
            if (e.getClickCount() == 2 &&
                    ((DefaultMutableTreeNode)tree.getLastSelectedPathComponent()).isLeaf())
            {
                // [Before opening the new file, save the old one]
                // After saving the old file, make sure the tree is up to date
                updateTree((DefaultMutableTreeNode)tree.getLastSelectedPathComponent());
                // [Now we open the new file]
            }
        }
    }

}

Upvotes: 1

Views: 336

Answers (1)

DragonAssassin
DragonAssassin

Reputation: 979

The problem is related to creating a brand new TreeModel on every call to updateTree. The problem is that the treeNode variable refers to a node in the old tree. Therefore, there is no path from the root of the new tree, to the node in old tree (i.e., Calling getParent() multiple times starting from treeNode will lead to the root of the old tree and not the new one). I see two potential options to solve your problem.

Option 1: Search for new tree node

You can write a function like the one below to search for the tree node starting from the new root with the path of the old treeNode.

private static DefaultMutableTreeNode searchTree(DefaultMutableTreeNode root, Object[] path) {
    if (!root.getUserObject().equals(path[0])) {
        // completely different root
        // potentially problematic
        return null;
    }

    DefaultMutableTreeNode node = root;
    for (int i = 1; i < path.length; ++i) {
        Object searchItem = path[i];
        Enumeration<TreeNode> children = node.children();
        boolean found = false;
        while (children.hasMoreElements()) {
            DefaultMutableTreeNode child = (DefaultMutableTreeNode) children.nextElement();
            if (searchItem.equals(child.getUserObject())) {
                found = true;
                node = child;
                break;
            }
        }

        if (!found) {
            // path does not exist any more
            // potentially problematic
            return null;
        }
    }

    return node;
}

You would then add the following to the updateTree method before setting the tree selection path.

treeNode = searchTree((DefaultMutableTreeNode) tree.getModel().getRoot(), treeNode.getUserObjectPath());

Option 2: Modify existing tree

Instead of creating a new tree model every time modify the existing one. For each directory start by creating a set of files in the directory and a set of tree nodes for the directory in tree. Remove all tree nodes for files that are not in the directory. Then, create nodes for all files in the directory that are not already in the tree. This option may be more complex that option 1, but it does not create potential problems with duplicate tree nodes through out your code.

Upvotes: 1

Related Questions