JavaSwingJTree and TreeModel: TreeModel - dynamic changes


TreeModel - dynamic changes

Firing events

As any Swing model, TreeModel must fire events to registered TreeModelListeners if its structure is modified. Listeners are registered by the methods addTreeModelListener() and removed by removeTreeModelListener().

In TreeModel, changes always happen at a location and not at a node - each TreeModelEvent carries a TreePath the location. Nodes that are contained at multiple locations may not dynamically change because the event is only fired for a specific location although in fact all locations have changed. This decreases the usefulness of having the same node at multiple locations.

TreeModelEvents don't carry all information about the event that happened. It lacks the information what type of change actually occurred, i.e. which method of the TreeModelListener was called.

TreeModelEvents contain a TreePath to identify the change location, a sorted integer array of child indices, and in parallel an array of the nodes.

The query methods may not fire events (the handling of inspecting code and TreeModelListener would be impossibly complicated if it were allowed). If a query method detects an change, it must suppress it (i.e. return the old state) and only perform the change and deliver events with SwingUtilities.invokeLater().

Property changes of a node (delivered by treeNodesChanged)

This event can only be used to notify of changes of a property of the path's child node(s) that may be interesting to TreeModelListeners (like the userObject of a TreeNode). In most cases these will be properties that may reflect the GUI representation of the locations in the JTree.

A special case is the change of the root's properties. In this case there exists no parent path, so the event's path is the path consisting of the root itself, and the indices and children are null.

Adding of child nodes to a location (delivered by treeNodesInserted)

If the set of a node's children gets new elements, listeners must be notified (see exceptions below). A TreeModelEvent identifying the parent location, the new indices and (though redundant) the new nodes is sent. These new nodes may already be contained at another location.

A node may have been a leaf before and stil have (suddenly) children added it it. In such cases, you may as well fire a complete structural subtree change beneath the location (see below). For use with JTree, this shouldn't be done when the node wasn't a leaf before, since JTree will collapse the path in such a case.

You may omit firing the event if no model inspector may possible know that the child nodes were not there from the beginning. This is true if getChildCount() has never been called and no other event has delivered information about the child count of the node. But if isLeaf(node) was true before, you again need to fire, since visitors assumed that the node couldn't contain any children.

Removal of children (delivered by treeNodesRemoved)

If nodes are removed from the set of a node's children, events must be delivered by treeNodesRemoved. Again a TreeModelEvent with the same information as above is sent. In this case, the information about the removed nodes is not redundant, since they cannot be accessed by calling getChild() anymore. The event's indices are those of the children before the removal.

If a node has lost all its children, it may have become a leaf. Again then a completely subtree change event may sent (though in this case it is inconvenient for listeners, as they get no information about the removed children).

You may omit the firing in the same cases as with adding. If the node turned into a leaf, you must fire it, if isLeaf(node) has been called before.

Complete subtree changes (delivered by treeStructureChanged)

If a lot of changes have happened in a certain subtree, this event can be used to either avoid firing a lot of events or if the TreeModel doesn't really know what exactly changed. The TreeModelEvent's indices and children fields are not used, only the path to identify the location of change. The event also implies that the node's leaf state may have changed.

This type of change must invalidate all information that the JTree had about the subtree, for example selection and expansion states. Use finer-grained events to keep these.

Change of root (delivered by treeStructureChanged)

A new root object is also signified by firing treeStructureChanged, giving either a TreePath consisting only of the new root if there is one or null as path if there is no root (the latter case is unspecified, but the only possible way, and is done like that in 1.4).

Leaf changes

Explicit leaf changes (delivered by treeStructureChanged)

If the only thing that changes if the leaf state of a node (and there are no children added or removed), the current implementation wants to receive a treeStructureChanged event.

This is reasonable. It is not possible to fire no event at all because for example the actual image of the node depends on the leaf state (It gets passed to the TreeCellRenderer, DefaultTreeCellRenderer uses different icons for leafs/non-leafs). Firing treeNodesChanged would be strange because it only means a change of a node's properties, not a structural change.

JTree only updates its list of expanded nodes (only non-leaf nodes can be expanded) if you fire treeStructureChanged, otherwise nodes that have become leafs are still stored as expanded (which invalidates the general assumption that you can only expand paths that are not leaves).

DefaultTreeModel has a major bug because it does not fire any event if you change asksAllowsChildren, although many nodes may then change their leaf state.

Implicit leaf changes

These occur when a node that was a leaf gets children added to it, or when a node that had children lost all of them and became a leaf. See discussion above.

Replacing a node object with another (unequal) one (at one or all of its locations)

This is not possible with TreeModel and one of its major problems. The only way to signify it is either to fire a structure change for the parent(s) of the node or to first remove the old node from its parent (one by one if there multiple ones), then fire treeNodesRemoved, then add the new node and fire treeNodesAdded.

Both ways are unsatisfying. The first invalidates all paths beneath the parent of the node, the latter is complicated to implement and invalidates all paths beneath the node. (This means JTree will deselect/collapse any paths of descendant nodes).

The reason why treeNodesChanged cannot be used in this case is that there may (and will) exist TreePath objects which contain the old node. The TreeModelEvent does not contain information about it (only about the new node), so it is impossible to figure out which TreePaths are still valid.

If one wants to replace nodes, one can make the TreeModel contain wrapper objects as nodes instead of the "real "object themselves. Then exchanging the "nodes" is actually changing properties of the wrapper objects, which can be easily done. But then finding the wrapper object if you only know the real object gets inconvient and slow, unless the TreeModel offers some lookup method.

DefaultTreeModel uses the (Mutable)TreeNodes both for the actual tree structure and as wrappers around the "user objects". When you only have a user object, you must loop (possibly by a DefaultMutableTreeNode's iteration methods) until you find it again. Since the JTree doesn't "see" the user objects at all, this also allows duplicates, so a user object doesn't represent a real location (and not even a "node") in the TreeModel.

Moving a subtree/subforest from one parent to another

That is also not possible. One has to remove it from the old parent and then add it to the new in two steps. By doing that, all paths will become collapsed and deselected.

With a lot of effort and if one knows about the JTree, one can extract the old expansion and selection state, update it, and reset it after the change. But that is somewhat contrary to the idea that the model should work independent from its observers/views.

In both latter cases one could extend TreeModelEvent to give the additional information to listeners that know about the extension and then will know how to handle only both events together, and one could even subclass JTree to update the states.

User interaction

TreeModel contains one method that makes it possible for the user to interactively edit a tree node's properties. This method may also be used for programmatical changes, but the semantics are specific to each TreeModel, so it isn't that useful.

To make only certain locations editable, one has to subclass JTree and override isPathEditable(TreePath). This ask the TreeModel whether it is possible at all.

void valueForPathChanged(TreePath path, Object newValue)

This method gets called when the user has edited a node. newValue is the value returned by the TreeCellEditor. It is usually used to change a property of the location, leading to a treeNodesChanged event. It is not (directly) possible to replace the actual node object with this call, see above, so what it actually does depends on the implementation. This is quite unlike JTable's setValueAt() for example, where the new value is expected to replace the old one.

DefaultTreeCellEditor lets the user edit the node's String representation.So newValue is the String result of the user's editing.

DefaultTreeModel sets value as the userObject of the MutableTreeNode identified by path.


AbstractTreeModel.java contains all necessary methods to write any dynamically changing TreeModel.


(C) 2001-2009 Christian Kaufhold (swing@chka.de)