Someone posted about how they could selectively hide tree nodes, and I already had this AbstractTreeModel class (which does some things DefaultTreeModel does and some it doesn't) and a concrete subclass for TreeNode objects, so I was thinking how one could do hideable nodes. So I came up with this solution.
There's 4 classes here:
- AbstractTreeModel is the base for the concrete TreeNodeTreeModel
- TreeNodeTreeModel extends AbstractTreeModel to support TreeNodes (DefautlMutableTreeNode, etc.)
- HideableMutableTreeNode which is a DefautlMutableTreeNode subclass which has a visible field (with is/set methods, of course).
- HideableTreeModel is the hideable model which is a subclass of TreeNodeTreeModel.
A HideableMutableTreeNode can be set invisible directly, but the tree still needs to be notified to update. So it's best to use the methods in HideableTreeModel which set a node's visibility which notify the tree of changes accordingly. Methods are also provided to check a full path's visibility or ensure a node including all parent nodes are visible.
A HideableTreeModel can take any TreeNode class, it doesn't have to be all HideableMutableTreeNodes, but only HideableMutableTreeNodes can be made invisible, of course. Any other TreeNode type would just be considered visible.
Hiding nodes works basically by making the tree think there's less nodes then there are. And to do this, the node counts and child index search just works by looping thru the parent's children. This has potential perfomance drawbacks of course, since one has to loop thru the node's children to get nodes every time. This could be alleviated by not supporting non-hideable nodes changing the internal maintenance of HideableMutableTreeNode contents. But I'll leave that to whoever really needs it. It shouldn't be a problem if there are are a relatively small set of child nodes in any given parent.
Also, note that the root node in the model cannot be made invisible, cuz it'd be redundant since JTree can be set to hide the root node.
// *** HideableTreeModel ***
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.tree.*;
/**
* <code>HideableTreeModel</code> is an <code>TreeNodeTreeModel</code>
* implementation for <code>HideableMutableTreeNode</code> objects. The
* model can also take any other <code>javax.swing.tree.TreeNode</code>
* objects.
*/
public class HideableTreeModel extends TreeNodeTreeModel {
/**
* Creates a new <code>HideableTreeModel</code> object.
*
* @param root the root node
*/
public HideableTreeModel(TreeNode root) {
super(root);
}
/**
* Checks if the specified node is visible. A node can only be
* hidden if the node is an instance of <code>HideableMutableTreeNode</code>. <br />
* <br />
* Note that this only test the visibility of the specified node, not
* whether a parent node is visible. Use <code>isPathToNodeVisible(Object)</code>
* to check if the full path is visible.
*
* @param node the node
* @param true if the node is visible, else false
*/
public boolean isNodeVisible(Object node) {
if(node != getRoot()) {
if(node instanceof HideableMutableTreeNode) {
return ((HideableMutableTreeNode)node).isVisible();
}
}
return true;
}
/**
* Sets the specified node to be hidden. A node can only be made hidden
* if the node is an instance of <code>HideableMutableTreeNode</code>. <br />
* <br />
* Note that this method will notify the tree to reflect any changes to
* node visibility. <br />
* <br />
* Note that this will not alter the visibility of any nodes in the
* specified node's path to the root node. Use
* <code>ensurePathToNodeVisible(Object)</code> instead to make sure the
* full path down to that node is visible. <br />
* <br />
* Note that this method will notify the tree to reflect any changes to
* node visibility.
*
* @param node the node
* @param v true for visible, false for hidden
* @param true if the node's visibility could actually change, else false
*/
public boolean setNodeVisible(Object node, boolean v) {
// can't hide root
if(node != getRoot()) {
if(node instanceof HideableMutableTreeNode) {
HideableMutableTreeNode n = (HideableMutableTreeNode)node;
// don't fix what ain't broke...
if(v != n.isVisible()) {
TreeNode parent = n.getParent();
if(v) {
// need to get index after showing...
n.setVisible(v);
int index = getIndexOfChild(parent, n);
super.nodeInserted(parent, n, index);
} else {
// need to get index before hiding...
int index = getIndexOfChild(parent, n);
n.setVisible(v);
super.nodeRemoved(parent, n, index);
}
}
return true;
}
}
return false;
}
/**
* Checks if the specified node is visible and all nodes above it are
* visible.
*
* @param node the node
* @param true if the path is visible, else false
*/
public boolean isPathToNodeVisible(Object node) {
Object[] path = getPathToRoot(node);
for(int i = 0; i < path.length; i++) {
if(!isNodeVisible(path)) {
return false;
}
}
return true;
}
/**
* Sets the specified node and all nodes above it to be visible.
*
* Note that this method will notify the tree to reflect any changes to
* node visibility.
*
* @param node the node
*/
public void ensurePathToNodeVisible(Object node) {
Object[] path = getPathToRoot(node);
for(int i = 0; i < path.length; i++) {
setNodeVisible(path[i], true);
}
}
/**
* Returns the child of parent at index index in the parent's child array.
*
* @param parent the parent node
* @param index the index
* @return the child or null if no children
*/
public Object getChild(Object parent, int index) {
if(parent instanceof TreeNode) {
TreeNode p = (TreeNode)parent;
for(int i = 0, j = -1; i < p.getChildCount(); i++) {
TreeNode pc = (TreeNode)p.getChildAt(i);
if(isNodeVisible(pc)) {
j++;
}
if(j == index) {
return pc;
}
}
}
return null;
}
/**
* Returns the number of children of parent.
*
* @param parent the parent node
* @return the child count
*/
public int getChildCount(Object parent) {
int count = 0;
if(parent instanceof TreeNode) {
TreeNode p = (TreeNode)parent;
for(int i = 0; i < p.getChildCount(); i++) {
TreeNode pc = (TreeNode)p.getChildAt(i);
if(isNodeVisible(pc)) {
count++;
}
}
}
return count;
}
/**
* Returns the index of child in parent.
*
* @param parent the parent node
* @param child the child node
* @return the index of the child node in the parent
*/
public int getIndexOfChild(Object parent, Object child) {
int index = -1;
if(parent instanceof TreeNode && child instanceof TreeNode) {
TreeNode p = (TreeNode)parent;
TreeNode c = (TreeNode)child;
if(isNodeVisible(c)) {
index = 0;
for(int i = 0; i < p.getChildCount(); i++) {
TreeNode pc = (TreeNode)p.getChildAt(i);
if(pc.equals(c)) {
return index;
}
if(isNodeVisible(pc)) {
index++;
}
}
}
}
return index;
}
/**
* Main method for testing.
*
* @param args the command-line arguments
*/
public static void main(String[] args) {
JFrame f = new JFrame();
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
HideableMutableTreeNode root = new HideableMutableTreeNode("root");
root.add(new HideableMutableTreeNode("child_1"));
final HideableMutableTreeNode c2 = new HideableMutableTreeNode("child_2");
c2.setVisible(false);
final HideableMutableTreeNode c2a = new HideableMutableTreeNode("child_2_A");
c2.add(c2a);
c2.add(new HideableMutableTreeNode("child_2_B"));
root.add(c2);
HideableMutableTreeNode c3 = new HideableMutableTreeNode("child_3");
HideableMutableTreeNode cC = new HideableMutableTreeNode("child_3_C");
cC.setVisible(false);
c3.add(cC);
c3.add(new HideableMutableTreeNode("child_3_D"));
root.add(c3);
root.add(new HideableMutableTreeNode("child_4"));
root.add(new HideableMutableTreeNode("child_5"));
DefaultMutableTreeNode c6 = new DefaultMutableTreeNode("child_6");
c6.add(new DefaultMutableTreeNode("child_6_A"));
c6.add(new DefaultMutableTreeNode("child_6_B"));
root.add(c6);
final HideableTreeModel model = new HideableTreeModel(root);
JTree tree = new JTree(model);
f.getContentPane().add(new JScrollPane(tree), BorderLayout.CENTER);
JButton b = new JButton("toggle");
b.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
model.setNodeVisible(c2, !model.isNodeVisible(c2));
//model.ensurePathToNodeVisible(c2a);
}
});
f.getContentPane().add(b, BorderLayout.SOUTH);
f.pack();
f.setSize(300, 500);
f.show();
}
}
// *** HideableMutableTreeNode ***
import javax.swing.*;
import javax.swing.tree.*;
/**
* <code>HideableMutableTreeNode</code> is a <code>DefaultMutableTreeNode</code>
* implementation that works with <code>HideableTreeModel</code>.
*/
public class HideableMutableTreeNode extends DefaultMutableTreeNode {
/**
* The node is visible flag.
*/
public boolean visible = true;
/**
* Creates a tree node that has no parent and no children, but which
* allows children.
*/
public HideableMutableTreeNode() {
super();
}
/**
* Creates a tree node with no parent, no children, but which allows
* children, and initializes it with the specified user object.
*
* @param userObject - an Object provided by the user that
* constitutes the node's data
*/
public HideableMutableTreeNode(Object userObject) {
super(userObject);
}
/**
* Creates a tree node with no parent, no children, initialized with the
* specified user object, and that allows children only if specified.
*
* @param userObject - an Object provided by the user that
* constitutes the node's data
* @param allowsChildren - if true, the node is allowed to have child
* nodes -- otherwise, it is always a leaf node
*/
public HideableMutableTreeNode(Object userObject, boolean allowsChildren) {
super(userObject, allowsChildren);
}
/**
* Checks if the node is visible.
*
* @return true if the node is visible, else false
*/
public boolean isVisible() {
return this.visible;
}
/**
* Sets if the node is visible.
*
* @param v true if the node is visible, else false
*/
public void setVisible(boolean v) {
this.visible = v;
}
}
// *** TreeNodeTreeModel ***
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.tree.*;
/**
* <code>TreeNodeTreeModel</code> is an <code>AbstractTreeModel</code>
* implementation for <code>javax.swing.tree.TreeNode</code> objects.
*/
public class TreeNodeTreeModel extends AbstractTreeModel {
/**
* Creates a new <code>TreeNodeTreeModel</code> object.
*
* @param root the root node
*/
public TreeNodeTreeModel(TreeNode root) {
super();
setRoot(root);
}
/**
* Returns the parent of the child node.
*
* @param node the child node
* @return the parent or null if root
*/
public Object getParent(Object node) {
if(node != getRoot() && (node instanceof TreeNode)) {
return ((TreeNode)node).getParent();
}
return null;
}
/**
* Returns the child of parent at index index in the parent's child array.
*
* @param parent the parent node
* @param index the index
* @return the child or null if no children
*/
public Object getChild(Object parent, int index) {
if(parent instanceof TreeNode) {
return ((TreeNode)parent).getChildAt(index);
}
return null;
}
/**
* Returns the number of children of parent.
*
* @param parent the parent node
* @return the child count
*/
public int getChildCount(Object parent) {
if(parent instanceof TreeNode) {
return ((TreeNode)parent).getChildCount();
}
return 0;
}
/**
* Returns the index of child in parent.
*
* @param parent the parent node
* @param child the child node
* @return the index of the child node in the parent
*/
public int getIndexOfChild(Object parent, Object child) {
if(parent instanceof TreeNode && child instanceof TreeNode) {
return ((TreeNode)parent).getIndex((TreeNode)child);
}
return -1;
}
/**
* Returns true if node is a leaf.
*
* @param node the node
* @return true if the node is a leaf
*/
public boolean isLeaf(Object node) {
if(node instanceof TreeNode) {
return ((TreeNode)node).isLeaf();
}
return true;
}
/**
* Main method for testing.
*
* @param args the command-line arguments
*/
public static void main(String[] args) {
JFrame f = new JFrame();
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
DefaultMutableTreeNode root = new DefaultMutableTreeNode("root");
root.add(new DefaultMutableTreeNode("child_1"));
DefaultMutableTreeNode c2 = new DefaultMutableTreeNode("child_2");
c2.add(new DefaultMutableTreeNode("child_2_A"));
c2.add(new DefaultMutableTreeNode("child_2_B"));
root.add(c2);
root.add(new DefaultMutableTreeNode("child_3"));
root.add(new DefaultMutableTreeNode("child_4"));
JTree tree = new JTree(new TreeNodeTreeModel(root));
f.getContentPane().add(new JScrollPane(tree));
f.pack();
f.setSize(300, 500);
f.show();
}
}
// *** AbstractTreeModel ***
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.net.*;
import java.text.*;
import java.util.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.tree.*;
public abstract class AbstractTreeModel implements TreeModel {
/**
* The list of tree model listeners.
*/
private Vector modelListeners = new Vector();
/**
* The root object of the tree.
*/
private Object root = null;
/**
* Basic no-op constructor.
*/
public AbstractTreeModel() {
}
/**
* Gets the root object of the tree.
*
* @return the root object
*/
public Object getRoot() {
return this.root;
}
/**
* Sets the root object of the tree.
*
* @param r the root object
*/
protected void setRoot(Object r) {
this.root = r;
}
/**
* Gets the path to the root node for the specified object.
*
* @param node the root node
* @return the path to the object or <CODE>null</CODE>
*/
public Object[] getPathToRoot(Object node) {
return getPathToRoot(node, 0);
}
/**
* Gets the path to the root node for the specified object.
*
* @param node the root node
* @param i the current index
* @return the path to the object or <CODE>null</CODE>
*/
private Object[] getPathToRoot(Object node, int i) {
Object anode[];
if(node == null) {
if(i == 0) {
return null;
}
anode = new Object[i];
} else {
i++;
if(node == getRoot()) {
anode = new Object[i];
} else {
anode = getPathToRoot(getParent(node), i);
}
anode[anode.length - i] = node;
}
return anode;
}
/**
* Gets the parent object of the specified object. This method is not
* part of the <code>javax.swing.tree.TreeModel</code> interface, but is
* required to support the <code>getPathToRoot(Object)</code> method,
* which is widely used in this class. Therefore, it is important to
* correctly implement this method.
*
* @param obj the object
* @parma the parent object or null if no parent or invalid object
*/
protected abstract Object getParent(Object obj);
/**
* Adds a listener for the <CODE>TreeModelEvent</CODE> posted after the
* tree changes.
*
* @param l the tree model listener
*/
public void addTreeModelListener(TreeModelListener l) {
modelListeners.addElement(l);
}
/**
* Removes a listener previously added with addTreeModelListener().
*
* @param l the tree model listener
*/
public void removeTreeModelListener(TreeModelListener l) {
modelListeners.removeElement(l);
}
/**
* Forces the tree to reload. This is useful when many changes occur
* under the root node in the tree structure.
* <b>NOTE:</b> This will cause the tree to be collapsed. To maintain
* the expanded nodes, see the <code>getExpandedPaths(JTree)</code>
* and <code>expandPaths(JTree, ArrayList)</code> methods.
*
* @see #getExpandedPaths(JTree)
* @see #expandPaths(JTree, ArrayList)
*/
public void reload() {
reload(getRoot());
}
/**
* Forces the tree to repaint. This is useful when many changes occur
* under a specific node in the tree structure.
* <b>NOTE:</b> This will cause the tree to be collapsed below the
* updated node.
*
* @param node the node that changed
*/
public void reload(Object node) {
if(node != null) {
TreePath tp = new TreePath(getPathToRoot(node));
fireTreeStructureChanged(new TreeModelEvent(this, tp));
}
}
/**
* Messaged when the user has altered the value for the item identified
* by <CODE>path</CODE> to <CODE>newValue</CODE>.
*
* @param path the path to the changed object
* @param newValue the new value
*/
public void valueForPathChanged(TreePath path, Object newValue) {
nodeChanged(path.getLastPathComponent());
}
/**
* Notifies the tree that nodes were inserted. The index is looked up
* automatically.
*
* @param node the parent node
* @param child the inserted child node
*/
public void nodeInserted(Object node, Object child) {
nodeInserted(node, child, -1);
}
/**
* Notifies the tree that nodes were inserted.
*
* @param node the parent node
* @param child the inserted child node
* @param index the index of the child
*/
public void nodeInserted(Object node, Object child, int index) {
if(index < 0) {
index = getIndexOfChild(node, child);
}
if(node != null && child != null && index >= 0) {
TreePath tp = new TreePath(getPathToRoot(node));
int[] ai = { index };
Object[] ac = { child };
fireTreeNodesInserted(new TreeModelEvent(this, tp, ai, ac));
}
}
/**
* Notifies the tree that nodes were removed. The index is required
* since by this point, the object will no longer be in the tree.
*
* @param node the parent node
* @param child the removed child node
* @param index the index of the child
*/
public void nodeRemoved(Object node, Object child, int index) {
if(node != null && child != null && index >= 0) {
TreePath tp = new TreePath(getPathToRoot(node));
int[] ai = { index };
Object[] ac = { child };
fireTreeNodesRemoved(new TreeModelEvent(this, tp, ai, ac));
}
}
/**
* Notifies the tree that a node was changed.
*
* @param node the changed node
*/
public void nodeChanged(Object node) {
if(node != null) {
TreePath tp = new TreePath(getPathToRoot(node));
fireTreeNodesChanged(new TreeModelEvent(this, tp, null, null));
}
}
/**
* Fires "tree nodes changed" events to all listeners.
*
* @param event the tree model event
*/
protected void fireTreeNodesChanged(TreeModelEvent event) {
for(int i = 0; i < modelListeners.size(); i++) {
((TreeModelListener)modelListeners.elementAt(i)).treeNodesChanged(event);
}
}
/**
* Fires "tree nodes inserted" events to all listeners.
*
* @param event the tree model event
*/
protected void fireTreeNodesInserted(TreeModelEvent event) {
for(int i = 0; i < modelListeners.size(); i++) {
((TreeModelListener)modelListeners.elementAt(i)).treeNodesInserted(event);
}
}
/**
* Fires "tree nodes removed" events to all listeners.
*
* @param event the tree model event
*/
protected void fireTreeNodesRemoved(TreeModelEvent event) {
for(int i = 0; i < modelListeners.size(); i++) {
((TreeModelListener)modelListeners.elementAt(i)).treeNodesRemoved(event);
}
}
/**
* Fires "tree structure changed" events to all listeners.
*
* @param event the tree model event
*/
protected void fireTreeStructureChanged(TreeModelEvent event) {
for(int i = 0; i < modelListeners.size(); i++) {
((TreeModelListener)modelListeners.elementAt(i)).treeStructureChanged(event);
}
}
/**
* Records the list of currently expanded paths in the specified tree.
* This method is meant to be called before calling the
* <code>reload()</code> methods to allow the tree to store the paths.
*
* @param tree the tree
* @param pathlist the list of expanded paths
*/
public ArrayList getExpandedPaths(JTree tree) {
ArrayList expandedPaths = new ArrayList();
addExpandedPaths(tree, tree.getPathForRow(0), expandedPaths);
return expandedPaths;
}
/**
* Adds the expanded descendants of the specifed path in the specified
* tree to the internal expanded list.
*
* @param tree the tree
* @param path the path
* @param pathlist the list of expanded paths
*/
private void addExpandedPaths(JTree tree, TreePath path, ArrayList pathlist) {
Enumeration enum = tree.getExpandedDescendants(path);
while(enum.hasMoreElements()) {
TreePath tp = (TreePath)enum.nextElement();
pathlist.add(tp);
addExpandedPaths(tree, tp, pathlist);
}
}
/**
* Re-expands the expanded paths in the specified tree. This method is
* meant to be called before calling the <code>reload()</code> methods
* to allow the tree to store the paths.
*
* @param tree the tree
* @param pathlist the list of expanded paths
*/
public void expandPaths(JTree tree, ArrayList pathlist) {
for(int i = 0; i < pathlist.size(); i++) {
tree.expandPath((TreePath)pathlist.get(i));
}
}
}