简体   繁体   中英

How to not collapse the node in JTree after the node is edited

I want to know how to implement this feature:

I have editable JTree where I can edit name of the nodes. If I have some node which is branch node (it has some leaf nodes in it) and this branch node is expanded while editing, after editing, this node will be collapsed.

I want to leave that branch node open if it is open and collapsed if it is collapsed, after editing is done.

I tried to look at TreeWillExpandListener but it seems it does not solve my problem because I need to recognize if actual node is in edit mode BEFORE I call these methods ...

How to do this trick? It is so obvious it is necessary thing but I can't find the answer at all :/

Ok so this is the code, I'll try to explain it. First of all, I have the class ContactTreeModel which implements TreeModel. The constructor just loads addressbook and group manager from the main app frame, I create new root and I load data from the database in the second method.

public ContactTreeModel() {
    addressBookManager = ContactManagerFrame.getAddressBookManager();
    groupManager = ContactManagerFrame.getGroupManager();
    root = new DefaultMutableTreeNode();
    processTreeHierarchy();
}

private void processTreeHierarchy() {
    DefaultMutableTreeNode group, contact;
    for (Group g : addressBookManager.getGroups()) {
        group = new DefaultMutableTreeNode(g);
        root.add(group);
        for (Contact c : addressBookManager.getContactsFromGroup(g)) {
            contact = new DefaultMutableTreeNode(c);
            group.add(contact);
        }
    }
}

I read that the method valueForPathChanged in the TreeModel is fired up if

Messaged when the user has altered the value for the item identified by path to newValue. If newValue signifies a truly new value the model should post a treeNodesChanged event.

So I wrote that method like this:

@Override
public void valueForPathChanged(TreePath path, Object newValue) {
    // backup of the original group
    Group oldGroup = (Group) path.getLastPathComponent();
    try {
        Group testGroup = (Group) path.getLastPathComponent();
        testGroup.setName((String) newValue);
        // validation of the group to be updated
        groupManager.validateGroup(testGroup, true);
        oldGroup.setName((String) newValue);
        // updating of the group in db
        groupManager.updateGroup(oldGroup);
    } catch (ServiceFailureException | ValidationException ex) {
        // if database error occured or validation exception is raised, 
        // update label in gui 
        ContactManagerFrame.getStatusPanelLabel().setText(ex.getMessage());
    } finally {
        fireTreeStructureChanged();
    }
}

Notice the method in the finally block, that method is always fired up. It looks like

protected void fireTreeStructureChanged() {
    Object[] o = {root};
    TreeModelEvent e = new TreeModelEvent(this, o);
    for (TreeModelListener l : treeModelListeners) {
        l.treeNodesChanged(e);
    }
}

This is little bit tricky and the reason why it does not work (I guess). I just iterate through all treeModelListeners and fire up the treeNodesChanged method.

I have the array specified for tree model listeners as a private attribute in the ContactTreeModel and related methods too:

private Vector<TreeModelListener> treeModelListeners = new Vector<>();

@Override
public void addTreeModelListener(TreeModelListener l) {
    treeModelListeners.addElement(l);
}

@Override
public void removeTreeModelListener(TreeModelListener l) {
    treeModelListeners.removeElement(l);
}

The last thing, how does the model listener I added to the model look like? Here it comes:

public class ContactTreeModelListener implements TreeModelListener {
    @Override
    public void treeNodesChanged(TreeModelEvent e) {
        System.out.println("nodes changed");
    }

    @Override
        public void treeNodesInserted(TreeModelEvent e) {
        System.out.println("nodes inserted");
    }

    @Override
    public void treeNodesRemoved(TreeModelEvent e) {
        System.out.println("nodes removed");
    }

    @Override
    public void treeStructureChanged(TreeModelEvent e) {
        System.out.println("structure changed");
    }
}

So it does basically nothing. I registered listener in the other place, it does not matter right now.

So, when I execute it, in this state, the tree does NOT collapse and the behavior is as wanted. But even it really rewrite the name of that node (label), if the original string was about 5 characters and I changed it eg to 4 characters, there will be only 4 characters in the label but also the empty space for the fifth. So the size of the original label has not changed after I finished editing that label. Similarly, when I expand the name of the group, eg I rename it from 4 to 5 characters, the label of that node will contain dots indicating that the whole text is too large to display. That's weird ... How do I make the udpdate of that label?

The very last thing ... since I have custom icons in the JTree + I do the recognition between empty and non-empty group, I need to check the actual icon in db every time I do some action in the tree (I checked it is executed even I just open and close the nodes). This is very inefficient so I extended DefaultTreeCellRenderer where I do the actual caching:

@Override
public Component getTreeCellRendererComponent(
        JTree tree,
        Object value,
        boolean sel,
        boolean expanded,
        boolean leaf,
        int row,
        boolean hasFocus) {

    if (iconCache == null) {
        throw new NullPointerException("iconCache in " 
             + this.getClass().getName() + " is null");
    }

    super.getTreeCellRendererComponent(
            tree, value, sel,
            expanded, leaf, row,
            hasFocus);

    if (value instanceof Group) {
        Group g = (Group) value;
        Icon groupIcon = iconCache.get(g);
        if (groupIcon == null) {
            if (groupHasContacts(g)) {
                groupIcon = groupNonEmpty;
            } else {
                groupIcon = groupEmptyIcon;
            }
            iconCache.put(g, groupIcon);
        }
        JLabel result = (JLabel) super.getTreeCellRendererComponent(tree,
                g.getName(), sel, expanded, leaf, row, hasFocus);

        result.setIcon(groupIcon);
        return result;
    }
    else if (value instanceof Contact) {
        Contact c = (Contact) value;
        Icon icon = iconCache.get(c);
        if (icon == null) {
            icon = this.contactIcon;
            iconCache.put(c, icon);
        }
        JLabel result = (JLabel) super.getTreeCellRendererComponent(
                tree, c.getName() + c.getSurname(), 
                sel, expanded, leaf, row, hasFocus);
        result.setIcon(icon);
        return result;
    }

    JLabel defaultNode = (JLabel) super.getTreeCellRendererComponent(
            tree, "?", sel, expanded, leaf, row, hasFocus);

    defaultNode.setIcon(unknownNode);
    return defaultNode;
}

The problem with update of the label that is not repainted properly is probably related to the fact that you fire treeNodesChanged event with only root in the array that identifies the path.

Try to call the following from valueForPathChanged :

public void fireTreeNodesChanged(TreePath path) {
    TreeModelEvent e = new TreeModelEvent(this, path.getPath());
    for (TreeModelListener l : treeModelListeners) {
        l.treeNodesChanged(e);
    }
}

By the way, your name for fireTreeStructureChanged that actually fires treeNodesChanged is very misleading.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM