Chapter 17. Trees

In this chapter:

17.1 JTree

JTree is a perfect tool for the display, navigation, and editing of hierarchical data. Because of its complex nature, JTree has a whole package devoted to it: javax.swing.tree. This package consists of a set of classes and interfaces which we will briefly review before moving on to several examples. But first, what is a tree?

17.1.1 Tree concepts and terminology

The tree is a very important and heavily used data structure throughout computer science (e.g. compiler design, graphics, artificial intelligence, etc.). This data structure consists of a logically arranged set of nodes, which are containers for data. Each tree contains one root node, which serves as that tree’s top-most node. Any node can have an arbitrarty number of child (descendant) nodes. In this way, each descendant node is the root of a subtree.

Each node is connected by an edge. An edge signifies the relationship between two nodes. A node’s direct predecessor is called its parent, and all predecesors (above and including the parent) are called its ancestors. A node that has no descendants is called a leaf node. All direct child nodes of a given node are siblings.

A path from one node to another is a sequence of nodes with edges from one node to the next. The level of a node is the number of nodes visited in the path between the root and that node. The height of a tree is its largest level--the length of its longest path.

17.1.2 Tree traversal

It is essential that we be able to systematically visit each and every node of a tree. (The term ‘visit’ here refers to performing some task before moving on.) There are three common traversal orders used for performing such an operation: preoder, inorder, and postorder. Each is recursive and can be summarized as follows:

Preorder

Recursively do the following:

If the tree is not empty, visit the root and then traverse all subtrees in ascending order.

Inorder (often referred to as breadth first):

Start the traversal by visiting the main tree root. Then, in ascending order, visit the root of each subtree. Continue visiting the roots of all subtrees in this mannar, in effect visiting the nodes at each level of the tree in ascending order.

Postorder (often referred to as depth first):

Recursively do the following:

If the tree is not empty, traverse all subtrees in ascending order, and then visit the root.

17.1.3 JTree

class javax.swing.JTree

So how does Swing’s JTree component deal with all this structure? Implementations of the TreeModel interface encapsulate all tree nodes, which are implementations of the TreeNode interface. The DefaultMutabeTreeNode class (an implementation of TreeNode) provides us with the ability to perform preorder, inorder, and postorder tree traversals.

 

Note: There is nothing stopping us from using TreeModel as a data structure class without actually displaying it in a GUI. However, since this book, and the Swing library, is devoted to GUI, we will not discuss these possibilities further.)

JTree graphically displays each node similar to how JList displays its elements: in a vertical column of cells. Similarly, each cell can be rendered with a custom renderer (an implementation of TreeCellRenderer) and can be edited with a custom TreeCellEditor. Each tree cell shows a non-leaf node as being expanded or collapsed, and can represent node relationships (i.e. edges) in various ways. Expanded nodes show their subtree nodes, and collapsed nodes hide this information.

The selection of tree cells is similar to JList’s selection mechanism, and is controlled by a TreeSelectionModel. Selection also involves keeping track of paths between nodes as instances of TreeNode. Two kinds of events are used specifically with trees and tree selections: TreeModelEvent and TreeExpansionEvent. Other AWT and Swing events also apply to JTree. For instance, we can use MouseListeners to intercept mouse presses and clicks. Also note that JTree implements the Scrollable interface (see chapter 7) and is intended to be placed in a JScrollPane.

A JTree can be constructed using either the default constructor, by providing a TreeNode to use for the root node, providing a TreeModel containing all constituent nodes, or by providing a one-dimensional array, Vector, or Hashtable of objects. In the latter case, if any element in the given structure is a multi-element structure itself, it is recursively used to build a subtree (this functionality is handled by an inner class called DynamicUtilTreeNode).

We will see how to construct and work with all aspects of a JTree soon enough. But first we need to develop a more solid understanding of its underlying constituents and how they interact.

17.1.4 The TreeModel interface

abstract interface javax.swing.tree.TreeModel

This model handles the data to be used in a JTree, assuming that each node maintains an array of child nodes. Nodes are represented as Objects, and a separate root node accessor is defined. A set of methods is intended to retrieve a node based on a given parent node and index, return the number of children of a given node, return the index of a given node based on a given parent, check if a given node is a leaf node (has no children), and a method to notify JTree that a node which is the destination of a given TreePath has been modified. It also provides method declarations for adding and removing T! reeModelListeners which should be notified when any nodes are added, removed, or changed. A JTree’s TreeModel can be retrieved and assigned with its getModel() and setModel() methods respectively.

17.1.5 DefaultTreeModel

class javax.swing.tree.DefaultTreeModel

DefaultTreeModel is the default concrete implementation of the TreeModel interface. It defines the root and each node of the tree as TreeNode instances. It maintains an EventListenerList of TreeModelListeners and provides several methods for firing TreeModelEvents when anything in the tree changes. It defines the asksAllowedChildren flag which is used to confirm whether or not a node allows children to be added before actually attempting to add them. ! DefaultTreeModel also defines methods for returning an array of nodes from a given node to the root node, inserting and removing nodes, and a method to reload/refresh a tree from a specified node. We normally build off of this class when implementing a JTree component.

17.1.6 The TreeNode interface

abstract interface javax.swing.tree.TreeNode

TreeNode describes the base interface which all tree nodes must conform to in a DefaultTreeModel. Implementations of this class represent the basic building block of JTree’s model. It declares properties for specifying whether a node is a leaf, a parent, allows addition of child nodes, determining the number of children, obtaining a TreeNode child at a given index or the parent node, and obtaining an Enumeration of all child nodes.

17.1.7 The MutableTreeNode interface

abstract interface javax.swing.tree.MutableTreeNode

Thic interface extends TreeNode to describe a more sophisticated tree node which can carry a user object. This is the object that represents the data of a given tree node. The setUserObject() method declares how the user object should be assigned (it is assumed that implementations of this interface will provide the equivalent of a getUserObject() method, even though none is included here). This interface also provides method declarations for inserting and removing nodes from a given node, and changing its parent node.

17.1.8 DefaultMutableTreeNode

class javax.swing.tree.DefaultMutableTreeNode

DefaultMutableTreeNode is a concrete implementation of the MutableTreeNode interface. Method getUserObject() returns a the data object encapsulated by this node. It stores all child nodes in a Vector called children, accessible with the children() method which returns an Enumeration of all child nodes. We can also use the getChildAt() method to retreive the node corresponding to the given index. There are many methods for, among other things, retrieving and ! assigning tree nodes, and they are all self-explanitory (or can be understood through simple reference of the API docs). The only methods that deserve special mention here are the overriden toString() method, which returns the String given by the user object’s toString() method, and the tree traversal methods which return an Enumeration of nodes in the order they were visited. As discussed above, there are three types of traversal supported: preorder, inorder, and postorder. The corresponding methods are preorderEnumeration(), breadthFirstEnumeration(), depthFirstEnumeration()! and postorderEnumeration() (note that the last two methods do the same thing).

17.1.9 TreePath

class javax.swing.tree.TreePath

A TreePath represents the path to a node as a set of nodes starting from the root. (Recall that nodes are Objects, not necessarily TreeNodes.) TreePaths are read-only objects and provide functionality for comparison between other TreePaths. The getLastPathComponent() gives us the final node in the path, equals() compares two paths, getPathCount() ! gives the number of nodes in a path, isDescendant() checks whether a given path is a descendant of (i.e. is completely contained in) a given path, and pathByAddingChild() returns a new TreePath instance resulting from adding the given node to the path. See the example of section 17.2 for more about working with TreePaths.

17.1.10 The TreeCellRenderer interface

abstract interface javax.swing.tree.TreeCellRenderer

This interface describes the component used to render a cell of the tree. The getTreeCellRendererComponent() method is called to return the component to render corresponding to a given cell and that cell’s selection, focus, and tree state (i.e. whether it is a leaf or a parent, and whether it is expanded or collapsed). This works similar to custom cell rendering in JList and JComboBox (see chapters 9 and 10). To assign a renderer to JTree we use its setCellRenderer() method. Recall that renderer components are not at all interactive and simply act as "rubber! stamps" for display purposes only.

17.1.11 DefaultTreeCellRenderer

class javax.swing.tree.DefaultTreeCellRenderer

DefaultTreeCellRenderer is the default concrete implementation of the TreeCellRenderer interface. It extends JLabel and maintains several properties used to render a tree cell based on its current state, as described above. These properties include Icons used to represent the node in any of its possible states (leaf, parent collapsed, parent expanded) and background and foreground colors to use based on whether the node is selected or unselected. Each of these properties is self-explanitory and typical get/set accessors are provided.

17.1.12 CellRenderPane

class javax.swing.CellRenderPane

In chapter 2 we discussed the painting and validation process in detail, but we purposely avoided the discussion of how renderers actually work behind the scenes because they are only used be a few specific components. The component returned by a renderer’s getXXRendererComponent() method is placed in an instance of CellRenderPane. The CellRenderPane is used to act as the component’s parent so that any validation and repaint requests that occur do not propogate up the ancestry tree of the container it resides in. It does this by overriding the paint() and invalidate() with empty implementations.

Several paintComponent() methods are provided to render a given component onto a given graphical context. These are used by the JList, JTree, and JTable UI delegates to actually paint each cell, which results in the "rubber stamp" behavior we have referred to.

17.1.13 The CellEditor interface

abstract javax.swing.CellEditor

Unlike renderers, cell editors for JTree and JTable are defined from a generic interface. This interface is CellEditor and it declares methods for controlling when editing will start and stop, retrieving a new value resulting from an edit, and whether or not an edit request changes the component’s current selection.

Object getCellEditorValue(): used by JTree and JTable after an accepted edit to retrieve the new value.

boolean isCellEditable(EventObject anEvent): used to test whether the given event should trigger a cell edit. For instance, to accept a single mouse click as an edit invocation we would override this method to test for an instance of MouseEvent and check its click count. If the click count is 1 return true, otherwise return false.

boolean shouldSelectCell(EventObject anEvent): used to specify whether the given event causes a cell that is about to be edited to also be selected. This will cancel all previous selection, and for components that want to allow editing during an ongoing selection we would return false here. It is most common to return true, as we normally think of the cell being edited as the currently selected cell.

boolean stopCellEditing(): used to stop a current cell edit. This method can be overriden to perform input validation. If a value is found to be unacceptable we can return false indicating to the component that editing should not be stopped.

void cancelCellEditing(): used to stop a current cell edit and ignore any new input.

This interface also declares methods for adding and removing CellEditorListeners which should recieve ChangeEvents whenever an edit is stopped or canceled. So stopCellEditing() and cancelCellEditing() are responsible for firing ChangeEvents to any registered listeners.

Normally cell editing starts with the user clicking on a cell a specified number of times which can be defined in the isCellEditable() method. The component containing the cell then replaces the current renderer pane with its editor component (JTree’s editor component is that returned by TreeCellEditor’s getTreeCellEditorComponent() method). If shouldSelectCell() returns true then the component’s selection state changes to only contain the cell being edited. A new value is entered using the editor and an appropriate action takes place which invokes either stopCellEditi! ng() or cancelCellEditing(). Finally, if the edit was stopped and not canceled, the component retrieves the new value from the editor, using getCellEditorValue(), and overwrites the old value. The editor is then replaced by the renderer pane which is updated to reflect the new data value.

17.1.14 The TreeCellEditor interface

abstract interface javax.swing.tree.TreeCellEditor

This interface extends CellEditor and describes the behavior of a component to be used in editing the cells of a tree. Method getTreeCellEditorComponent() is called prior to the editing of a new cell to set the initial data for the component it returns as the editor, based on a given cell and that cell’s selection, focus, and its expanded/collapsed states. We can use any component we like as an editor. To assign a TreeCellEditor to JTree we use its setCellEditor() method.

17.1.15 DefaultCellEditor

class javax.swing.DefaultCellEditor

This is a concrete implementation of the TreeCellEditor interface as well as the TableCellEditor interface (see 18.1.11). This editor allows the use of JTextField, JComboBox, or JCheckBox components to edit data. It defines a protected inner class called EditorDelegate which is responsible for returning the current value of the editor component in use when the getCellEditorValue() method is invoked. DefaultCellEditor is limited to three constructors for creating a JTextField, JComboBox, or a JCheckBox editor.

 

Note: The fact that the only constructors provided are component-specific makes DefaultCellEditor a bad candidate for extensibility.

DefaultCellEditor maintains an int property called clickCountToStart which specifies how many mouse click events should trigger an edit. By default this is 2 for JTextFields and 1 for JComboBox and JCheckBox editors. As expected ChangeEvents are fired when stopCellEditing() and cancelCellEditing() are invoked.

17.1.16 DefaultTreeCellEditor

class javax.swing.tree.DefaultTreeCellEditor

DefaultTreeCellEditor extends DefaultCellEditor, and is the default concrete implementation of the TreeCellEditor interface. It uses a JTextField for editing a node's data (an instance of DefaultTreeCellEditor.DefaultTextField). stopCellEditing() is called when ENTER is pressed in this text field.

An instance of DefaultTreeCellRenderer is needed to construct this editor, allowing renderer icons to remain visible while editing (accomplished by embedding the editor in an instance of DefaultTreeCellEditor.EditorContainer), and fires ChangeEvents when editing begins and ends. As expected, we can add CellEditorListeners to intercept and process these events.

By default, editing starts (if it is enabled) when a cell is triple-clicked or a pause of 1200ms occurs between two single mouse clicks (the latter is accomplished using an internal Timer). We can set the click count requirement using the setClickCountToStart() method, or check for it directly by overriding isCellEditable().

17.1.17 The RowMapper interface

abstract interface javax.swing.text.RowMapper

RowMapper describes a single method, getRowsForPaths(), which maps an array of tree paths to array of tree rows. A tree row corrsponds to a tree cell, and as we discussed, these are organized similar to JList cells. JTree selections are based on rows and tree paths, and we can choose which to deal with depending on the needs of our application. We aren’t expected to implement this interface unless we decide to build our own JTree UI delegate.

17.1.18 The TreeSelectionModel interface

abstract interface javax.swing.tree.TreeSelectionModel

The TreeSelectionModel interface describes a base interface for a tree's selection model. Three modes of selection are supported, similar to JList (see chapter 10), and implementations allow setting this mode through the setSelectionMode() method: SINGLE_TREE_SELECTION, DISCONTIGUOUS_TREE_SELECTION, and CONTIGUOUS_TREE_SELECTION. Implementations are expected to maintain a RowMapper instance. The getSelectionPath() and getSelectionPaths() methods are intended to return a TreePath and an array of TreePaths respectively, allowing access to the currently selected paths. The getSelectionRows() method should return an int array representing the indices of all rows currently selected. The lead selection refers to the most recently added path to the current selection. Whenever the selection changes, implementations of this interface should fire TreeSelectionEvents. Appropriately, add/remove TreeSelectionListener methods are also declared. All other metho! ds! are, for the most part, self explanitory (see API docs). The tree selection model can be retrieved using JTree‘s getSelectionModel() method.

 

Note: JTree defines the inner class EmptySelectionModel which does not allow any selection at all.

 

17.1.19 DefaultTreeSelectionModel

class javax.swing.tree.DefaultTreeSelectionModel

DefaultTreeSelectionModel is the default concrete implementation of the TreeSelectionModel interface. This model supports TreeSelectionListener notification when changes are made to a tree's path selection. Several methods are defined for, among other things, modifying a selection, testing if it can be modified, and firing TreeSelectionEvents when a modification occurs.

17.1.20 The TreeModelListener interface

abstract interface javax.swing.event.TreeModelListener

The TreeModelListener interface describes a listener which receives notifications about changes in a tree's model. TreeModelEvents are normally fired from a TreeModel when nodes are modified, added, or removed. We can register/unregsiter a TreeModelListener with a JTree’s model using TreeModel’s addTreeModelListener() and removeTreeModelListener()! methods respectively.

17.1.21 The TreeSelectionListener interface

abstract interface javax.swing.event.TreeSelectionListener

The TreeSelectionListener interface describes a listener which receives notifications about changes in a tree's selection. It declares only one method, valueChanged(), accepting a TreeSelectionEvent. These events are normally fired whenever a tree’s selection changes. We can register/unregister a TreeSelectionListener with a tree’s selection model using JTree’s addTreeSelectionListener() and removeTreeSelectionListener() methods respectively.

17.1.22 The TreeExpansionListener interface

abstract interface javax.swing.event.TreeExpansionListener

The TreeExpansionListener interface describes a listener which receives notifications about tree expansions and collapses. Implementations must define treeExpanded() and treeCollapsed() events, which take a TreeExpansionEvent as parameter. We can register/unregister a TreeExpansionListener with a tree using JTree’s addTreeExpansionListener() and removeTreeExpansionListener() methods respectively.

17.1.23 The TreeWillExpandListener interface

abstract interface javax.swing.event.TreeWillExpandListener

The TreeWillExpandListener interface describes a listener which receives notifications when a tree is about to expand or collapse. Unlike TreeExpansionListener this listener will be notified before the actual change occurs. Implementations are expected to throw an ExpandVetoException if it is determined that a pending expansion or collapse should not be carried out. Its two methods, treeWillExpand() and treeWillCollapse(), take a TreeExpansionEvent as parameter. We can register/unr! egister a TreeWillExpandListener with a tree using JTree’s addTreeWillExpandListener() and removeTreeWillExpandListener() methods respectively.

17.1.24 TreeModelEvent

class javax.swing.event.TreeModelEvent

TreeModelEvent is used to notify TreeModelListeners that all or part of a JTree’s data has changed. This event encapsulates a reference to the source component, and a single TreePath or an array of path Objects leading to the top-most affected node. We can extract the source as usual, using getSource(), and we can extract the path(s) using either of the getPath() or getTreePath() methods (the former returns an array of Objects, the latter returns a TreePath). Optionally, this event can also carry an int array of node indices and an array of child nodes. These can be extracted using the getChildIndices() and getChildren() methods respectively.

17.1.25 TreeSelectionEvent

class javax.swing.event.TreeSelectionEvent

TreeSelectionEvent is used to notify TreeSelectionListeners that the selection of a JTree has changed. One variant of this event encapsulates a reference to the source component, the selected TreePath, a flag specifying whether the tree path is a new addition to the selection (true if so), and the new and old lead selection paths (remember that the lead selection path is the newest path added to a selection). The second variant of this event encapsulates a reference to the source component, an array of selected TreePaths, an array of flags specifying whether each path is a new addition or not, and t! he new and old lead selection paths. Typical getXX() accessor methods allow extraction of this data.

 

Note: An interesting and unusual method defined in this class is cloneWithSource(). By passing it a component, this method returns a clone of the event, but with a reference to the given component as the event source.

 

17.1.26 TreeExpansionEvent

class javax.swing.event.TreeExpansionEvent

TreeExpansionEvent is used to encapsulate a TreePath corresponding to a recently, or possibly pending, expanded or collapsed tree path. This path can be extracted with the getPath() method.

17.1.27 ExpandVetoException

class javax.swing.tree.ExpandVetoException

ExpandVetoException may be thrown by TreeWillExpandListener methods to indicate that a tree path expansion or collapse is prohibited, and should be vetoed.

17.1.28 JTree client properties and UI defaults

When using the Metal L&F, JTree uses a specific line style to represent the edges between nodes. The default is no edges, but we can set JTree’s lineStyle client property so that each parent node appears connected to each of its child nodes by an angled line:

myJTree.putClientProperty("JTree.lineStyle", "Angled");

We can also set this property such that each tree cell is separated by a horizontal line:

myJTree.putClientProperty("JTree.lineStyle", "Horizontal");

To disable the line style:

myJTree.putClientProperty("JTree.lineStyle", "None");

As with any Swing component, we can also change the UI resource defaults used for all instances of the JTree class. For instance, to change the color of the lines used for rendering the edges between nodes as described above, we can modify the entry in the UI defaults table for this resource as follows:

UIManager.put("Tree.hash",

new ColorUIResource(Color.lightGray));

To modify the open node icons used by all trees when a node’s children are shown:

UIManager.put("Tree.openIcon", new IconUIResource(

new ImageIcon("myOpenIcon.gif")));

We can do a similar thing for the closed, leaf, expanded, and collapsed icons using Tree.closedIcon, Tree.leafIcon, Tree.expandedIcon, and Tree.collapsedIcon respectively. (See the BasicLookAndFeel source code for a complete list of UI resource defaults.)

 

Note: We used the ColorUIResource and IconUIResource wrapper classes found in the javax.swing.plaf package to wrap our resources before placing them in the UI defaults table. If we do not wrap our resources in UIResource objects they will persist through L&F changes (which may or may not be desirable). See chapter 21 for more about L&F and resource wrappers.

 

17.1.29 Controlling JTree appearance

Though we haven’t concentrated heavily on UI delegate customization for each component throughout this book, Swing certainly provides us with a high degree of flexibility in this area. It is particularly useful with JTree because no methods are provided in the component itself to control the indentation spacing of tree cells (note that the row height can be specified with JTree’s setRowHeight() method). The JTree UI delegate also provides methods for setting expanded and collapsed icons, allowing us to assign these on a per-component basis rather than a global basis (which is done using UIManager -- see 17.1.28). The following BasicTreeUI methods provide this control, and figure 17.1 illustrates:

void setCollapsedIcon(Icon newG): icon used to specify that a child icon is in the collapsed state.

void setExpandedicon(Icon newG): icon used to specify that a child icon is in the expanded state.

void setLeftChildIndent(int newAmount): used to assign a distance between the left side of a parent node and the center of an expand/collapse box of a child node.

void setRightChildIndent(int newAmount): used to assign a distance between the center of the expand/collapse box of a child node to the left side of that child node’s cell renderer.

Figure 17.1 JTree UI delegate icon and indentation properties.

<<file figure17-1.gif>>

To actually use these methods we first have to obtain the target tree’s UI delegate. For example, to assign a left indent of 8 and a right indent of 10:

BasicTreeUI basicTreeUI = (BasicTreeUI) myJTree.getUI();

basicTreeUI.setRightChildIndent(10);

basicTreeUI.setLeftChildIndent(8);

 

UI Guideline : When to use a tree As a selection device

The tree component was invented as a mechanism for selection from large hierarchical data sets without having to resort to a "Search" mechanism. As such, JTree falls between listing and search data as a component which can improve usability by easing the process of finding something, providing that the item to be found (or selected) is hidden within a hierarchical data set.

For example, finding an employee by name. For a small data set, a simple list may be sufficient. As the data set grows, it may be easier for the user if you sort the names alphabetically, or by department in which they work. By doing so, you have introduced a hierarchy and may now use a tree component. Use of the tree component may help and speed random selection from the data set, providing that the hierarchical structure used exists within the domain. i.e. don't introduce artificial hierarchies and expect users to understand them. More explicitly, if you can put the hierarchy into your analysis model and users accept it then its fine. If you can't then don't and consequently, don't select a tree component as the solution.

As a data set rises to become very large, tree component may again be of little value and you will need to introduce a full search facility.

As a general rule, when using a tree as a selection device, you would start with the tree collapsed and allow the user to expand it as they "search" for the item they are looking for. If there is a default selection or a current selection then you may expand that part of the tree to show that selection.

As a Visual Layering Device

Even with a small data set, you may find it advantagous to display a visual hierarchy to aid visual comprehension and visual searching. With the employee example, even for a small data set, you may prefer to layer by department or by alphabetical order. Even if selection is being used in the UI, it is important to understand when you chose to use the tree component for improved visual communication i.e. selection would still have been perfectly possible using a list but for visual communication reasons a tree was chosen.
When a tree is selected for display only i.e. no selection is taking place, then you are definitely using the tree as a visual layering device.

As a general rule, when you use a tree as a visual layering device, you will be default expand the tree in full, revealing the full hierarchy.

How you use a tree and which options to select amongst the many selection and display variants, can be affected by the usage as we will demonstrate later.

 

17.2 Basic JTree example - network object IDs

As we know very well by now, JTree is suitable for the display and editing of a hierarchical set of objects. To demonstrate this in an introductory-level example, we will consider a set of Object Identifiers (OIDs) used in the Simple Network Management Protocol (SNMP). In the following example, we show how to build a simple JTree displaying the initial portion of the OID tree.

SNMP is used extensively to manage network components, and is particularly important in managing internet routers and hosts. Every object managed by SNMP must have a unique OID. An OID is built from a sequence of numbers separated by periods. Objects are organized heirarchicaly and have an OID with a sequence of numbers equal in length to their level (see 17.1.1) in the OID tree. The International Organization of Standards (ISO) establishes rules for building OIDs.

Note that understanding SNMP is certainly not necessary to understand this example. The purpose is to show how to construct a tree using:

A DefaultTreeModel with DefaultMutableTreeNodes containing custom user objects.

A customized DefaultTreeCellRenerer.

A TreeSelectionListener which displays information in a status bar based on the TreePath encapsulated in the TreeSelectionEvents it receives.

Figure 17.2 JTree with custom cell renderer icons, selection listener, and visible root handles.

<<file figure17-2.gif>>

The Code: Tree1.java

see \Chapter17\1

import java.awt.*;
import java.awt.event.*;
import java.util.*;

import javax.swing.*;
import javax.swing.tree.*;
import javax.swing.event.*;

public class Tree1 extends JFrame 
{
	protected JTree  m_tree = null;
	protected DefaultTreeModel m_model = null;
	protected JTextField m_display;

	public Tree1()
	{
		super("Sample Tree [OID]");
		setSize(400, 300);

        Object[] nodes = new Object[5];
		DefaultMutableTreeNode top = new DefaultMutableTreeNode(
			new OidNode(1, "ISO"));
		DefaultMutableTreeNode parent = top;
		nodes[0] = top;

		DefaultMutableTreeNode node = new DefaultMutableTreeNode(
			new OidNode(0, "standard"));
		parent.add(node);
		node = new DefaultMutableTreeNode(new OidNode(2, 
			"member-body"));
		parent.add(node);
		node = new DefaultMutableTreeNode(new OidNode(3, "org"));
		parent.add(node);
		parent = node;
		nodes[1] = parent;

		node = new DefaultMutableTreeNode(new OidNode(6, "dod"));
		parent.add(node);
		parent = node;
		nodes[2] = parent;

		node = new DefaultMutableTreeNode(new OidNode(1, 
			"internet"));
		parent.add(node);
		parent = node;
		nodes[3] = parent;

		node = new DefaultMutableTreeNode(new OidNode(1, 
			"directory"));
		parent.add(node);
		node = new DefaultMutableTreeNode(new OidNode(2, 
			"mgmt"));
		parent.add(node);
		nodes[4] = node;
		node.add(new DefaultMutableTreeNode(new OidNode(1, 
			"mib-2")));
		node = new DefaultMutableTreeNode(new OidNode(3, 
			"experimental"));
		parent.add(node);
		node = new DefaultMutableTreeNode(new OidNode(4, 
			"private"));
		node.add(new DefaultMutableTreeNode(new OidNode(1, 
			"enterprises")));
		parent.add(node);
		node = new DefaultMutableTreeNode(new OidNode(5, 
			"security"));
		parent.add(node);
		node = new DefaultMutableTreeNode(new OidNode(6, 
			"snmpV2"));
		parent.add(node);
		node = new DefaultMutableTreeNode(new OidNode(7, 
			"mail"));
		parent.add(node);

		m_model = new DefaultTreeModel(top);
		m_tree = new JTree(m_model);

		DefaultTreeCellRenderer renderer = new 
			DefaultTreeCellRenderer();
		renderer.setOpenIcon(new ImageIcon("opened.gif"));
		renderer.setClosedIcon(new ImageIcon("closed.gif"));
		renderer.setLeafIcon(new ImageIcon("leaf.gif"));
		m_tree.setCellRenderer(renderer);

		m_tree.setShowsRootHandles(true); 
		m_tree.setEditable(false);
		TreePath path = new TreePath(nodes);
		m_tree.setSelectionPath(path);

		m_tree.addTreeSelectionListener(new 
			OidSelectionListener());

		JScrollPane s = new JScrollPane();
		s.getViewport().add(m_tree);
		getContentPane().add(s, BorderLayout.CENTER);

		m_display = new JTextField();
		m_display.setEditable(false);
		getContentPane().add(m_display, BorderLayout.SOUTH);

		WindowListener wndCloser = new WindowAdapter()
		{
			public void windowClosing(WindowEvent e) 
			{
				System.exit(0);
			}
		};
		addWindowListener(wndCloser);
		
		setVisible(true);
	}

	public static void main(String argv[]) 
	{
		new Tree1();
	}

	class OidSelectionListener implements TreeSelectionListener 
	{
		public void valueChanged(TreeSelectionEvent e)
		{
			TreePath path = e.getPath();
			Object[] nodes = path.getPath();
			String oid = "";
			for (int k=0; k < nodes.length; k++)
			{
				DefaultMutableTreeNode node = 
					(DefaultMutableTreeNode)nodes[k];
				OidNode nd = (OidNode)node.getUserObject();
				oid += "."+nd.getId();
			}
			m_display.setText(oid);
		}
	}
}

class OidNode
{
	protected int    m_id;
	protected String m_name;

	public OidNode(int id, String name)
	{
		m_id = id;
		m_name = name;
	}

	public int getId() 
	{ 
		return m_id;
	}

	public String getName() 
	{ 
		return m_name;
	}

	public String toString() 
	{ 
		return m_name;
	}
}

Understanding the Code

Class Tree1

This class extends JFrame to implement the frame container for our JTree. Three instance variables are declared:

JTree m_tree: our OID tree.

DefaultTreeModel m_model: tree model to manage data.

JTextField m_display: used as a status bar to display the selected object’s OID.

The constructor first initializes the parent frame object. Then a number of DefaultMutableTreeNodes encapsulating OidNodes (see below) are created. These objects form a hierarchical structure with DefaultMutableTreeNode top at the root. Note that during the construction of these nodes, the Object[] nodes array is populated with a path of nodes leading to the "mgmt" node.

DefaultTreeModel m_model is created with the top node as the root, and JTree m_tree is created to manage this model. Then some specific options are set for this tree component. First, we replace the default icons for opened, closed and leaf icons with our custom icons, using a DefaultTreeCellRenderer as our tree’s cell renderer:

        DefaultTreeCellRenderer renderer = new DefaultTreeCellRenderer();
        renderer.setOpenIcon(new ImageIcon("opened.gif"));
        renderer.setClosedIcon(new ImageIcon("closed.gif"));
        renderer.setLeafIcon(new ImageIcon("leaf.gif"));
        m_tree.setCellRenderer(renderer);

Then we set the showsRootHandles property to true, the Editable property to false, and select the path determined by the nodes array formed above:

        m_tree.setShowsRootHandles(true);
        m_tree.setEditable(false);
        TreePath path = new TreePath(nodes);
        m_tree.setSelectionPath(path);

Our custom OidSelectionListener (see below) TreeSelectionListener is added to the tree to receive notification when our tree’s selection changes.

A JScrollPane is created to provide scrolling capabilities, and our tree is added to its JViewport. This JScrollPane is then added to the center of our frame. A non-editable JTextField m_display is created and added to the south region of our frame’s content pane to display the currently selected OID.

Class Tree1.OidSelectionListener

This inner class implements the TreeSelectionListener interface to receive notifications about when our tree’s selection changes. Our valueChanged() implementation extracts TreePath for the current selection and visits each node, starting from the root, accumulating the OID in .N.N.N form as it goes (where N is a digit). This method ends by displaying the resulting OID in our text field status bar.

Class OidNode

This class encapsulates a single object identifier as a number and a String name describing the associated object. Both values are passed to the OidNode constructor. Instances of this class are passed directly to the DefaultMutableTreeNode constructor to act as a node’s user object. The overridden toString() method is used to return the name String so that our tree’s cell renderer will display each node correctly. Recall that, by default, DefaultTreeCellRenderer will call a node’s user object! toString() method for rendering.

Running the Code

Figure 17.1 shows our OID tree in action. Try selecting various tree nodes and note how the selected OID is displayed at the bottom of the frame.

 

UI Guideline : Icons & Root Handles

In this example, we are visually re-inforcing the data hierarchy with icons. The icons are overloading on the root handles to communicate whether an element is a document or a container and whether that container is open or closed. The book icon has two variants to communicate "open book" and "closed book". The icons are communicating the same information as the root handles.
Therefore, it is technically possible to remove the root handles. In some problem domains, hidden root handles may be more appropriate, providing that the Users are comfortable with interpreting the book icons and realise that a "closed book" icon means that the node can be expanded.

 

17.3 Directories tree: part I - dynamic node retrieval

The example in this section uses the JTree component to display and navigate through a tree of directories located on drives accessible from the user’s machine. We will show how to build a custom tree cell renderer as well as create and insert tree nodes dynamically.

The main problem encountered in building this application is the fact that it is not practical to read all directories for all accessible drives before displaying our tree component. This would take an extremely long time. To deal with this issue we initially display only the roots (i.e. disk partitions or network drives), and then dynamically expand the tree as the user navigates through it. This requires the use of threads and SwingUtilities.invokeLater() for thread-safe updating of our tree.

Figure 17.3 Dynamic, threaded directories tree with custom cell renderer and angled line style.

<<file figure17-3.gif>>

The Code: FileTree1.java

see \Chapter17\2

import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.*;

import javax.swing.*;
import javax.swing.tree.*;
import javax.swing.event.*;

public class FileTree1 extends JFrame 
{
	public static final ImageIcon ICON_COMPUTER = 
		new ImageIcon("computer.gif");
	public static final ImageIcon ICON_DISK = 
		new ImageIcon("disk.gif");
	public static final ImageIcon ICON_FOLDER = 
		new ImageIcon("folder.gif");
	public static final ImageIcon ICON_EXPANDEDFOLDER = 
		new ImageIcon("expandedfolder.gif");

	protected JTree  m_tree;
	protected DefaultTreeModel m_model;
	protected JTextField m_display;

	public FileTree1()
	{
		super("Directories Tree");
		setSize(400, 300);

		DefaultMutableTreeNode top = new DefaultMutableTreeNode(
			new IconData(ICON_COMPUTER, null, "Computer"));

		DefaultMutableTreeNode node;
		File[] roots = File.listRoots();
		for (int k=0; k < roots.length; k++)
		{
			node = new DefaultMutableTreeNode(new IconData(ICON_DISK, 
				null, new FileNode(roots[k])));
			top.add(node);
                        node.add( new DefaultMutableTreeNode(new Boolean(true)));
		}

		m_model = new DefaultTreeModel(top);
		m_tree = new JTree(m_model);

                m_tree.putClientProperty("JTree.lineStyle", "Angled");

		TreeCellRenderer renderer = new	IconCellRenderer();
		m_tree.setCellRenderer(renderer);

		m_tree.addTreeExpansionListener(new DirExpansionListener());

		m_tree.addTreeSelectionListener(new DirSelectionListener());

		m_tree.getSelectionModel().setSelectionMode(
			TreeSelectionModel.SINGLE_TREE_SELECTION); 
		m_tree.setShowsRootHandles(true); 
		m_tree.setEditable(false);

		JScrollPane s = new JScrollPane();
		s.getViewport().add(m_tree);
		getContentPane().add(s, BorderLayout.CENTER);

		m_display = new JTextField();
		m_display.setEditable(false);
		getContentPane().add(m_display, BorderLayout.NORTH);

		WindowListener wndCloser = new WindowAdapter()
		{
			public void windowClosing(WindowEvent e) 
			{
				System.exit(0);
			}
		};
		addWindowListener(wndCloser);
		
		setVisible(true);
	}

	DefaultMutableTreeNode getTreeNode(TreePath path)
	{
		return (DefaultMutableTreeNode)(path.getLastPathComponent());
	}

	FileNode getFileNode(DefaultMutableTreeNode node)
	{
		if (node == null)
			return null;
		Object obj = node.getUserObject();
		if (obj instanceof IconData)
		  obj = ((IconData)obj).getObject();
		if (obj instanceof FileNode)
		  return (FileNode)obj;
		else
		  return null;
	}

    // Make sure expansion is threaded and updating the tree model
    // only occurs within the event dispatching thread.
    class DirExpansionListener implements TreeExpansionListener
    {
        public void treeExpanded(TreeExpansionEvent event)
        {
            final DefaultMutableTreeNode node = getTreeNode(
                event.getPath());
            final FileNode fnode = getFileNode(node);

            Thread runner = new Thread() 
            {
              public void run() 
              {
                if (fnode != null && fnode.expand(node)) 
                {
                  Runnable runnable = new Runnable() 
                  {
                    public void run() 
                    {
                       m_model.reload(node);
                    }
                  };
                  SwingUtilities.invokeLater(runnable);
                }
              }
            };
            runner.start();
        }

        public void treeCollapsed(TreeExpansionEvent event) {}
    }


	class DirSelectionListener implements TreeSelectionListener 
	{
		public void valueChanged(TreeSelectionEvent event)
		{
			DefaultMutableTreeNode node = getTreeNode(
				event.getPath());
			FileNode fnode = getFileNode(node);
			if (fnode != null)
				m_display.setText(fnode.getFile().
					getAbsolutePath());
			else
				m_display.setText("");
		}
	}

	public static void main(String argv[]) 
	{
		new FileTree1();
	}
}

class IconCellRenderer 	extends    JLabel implements TreeCellRenderer
{
	protected Color m_textSelectionColor;
	protected Color m_textNonSelectionColor;
	protected Color m_bkSelectionColor;
	protected Color m_bkNonSelectionColor;
	protected Color m_borderSelectionColor;

	protected boolean m_selected;

	public IconCellRenderer()
	{
		super();
		m_textSelectionColor = UIManager.getColor(
			"Tree.selectionForeground");
		m_textNonSelectionColor = UIManager.getColor(
			"Tree.textForeground");
		m_bkSelectionColor = UIManager.getColor(
			"Tree.selectionBackground");
		m_bkNonSelectionColor = UIManager.getColor(
			"Tree.textBackground");
		m_borderSelectionColor = UIManager.getColor(
			"Tree.selectionBorderColor");
		setOpaque(false);
	}

	public Component getTreeCellRendererComponent(JTree tree, 
		Object value, boolean sel, boolean expanded, boolean leaf, 
		int row, boolean hasFocus) 
		
	{
		DefaultMutableTreeNode node = 
			(DefaultMutableTreeNode)value;
		Object obj = node.getUserObject();
		setText(obj.toString());

                if (obj instanceof Boolean)
                  setText("Retrieving data...");

		if (obj instanceof IconData)
		{
			IconData idata = (IconData)obj;
			if (expanded)
			  setIcon(idata.getExpandedIcon());
			else
			  setIcon(idata.getIcon());
		}
		else
			setIcon(null);

		setFont(tree.getFont());
		setForeground(sel ? m_textSelectionColor : m_textNonSelectionColor);
		setBackground(sel ? m_bkSelectionColor : m_bkNonSelectionColor);
		m_selected = sel;
		return this;
	}
    
	public void paintComponent(Graphics g) 
	{
		Color bColor = getBackground();
		Icon icon = getIcon();

		g.setColor(bColor);
		int offset = 0;
		if(icon != null && getText() != null) 
			offset = (icon.getIconWidth() + getIconTextGap());
		g.fillRect(offset, 0, getWidth() - 1 - offset,
			getHeight() - 1);
		
		if (m_selected) 
		{
			g.setColor(m_borderSelectionColor);
			g.drawRect(offset, 0, getWidth()-1-offset, getHeight()-1);
		}
		super.paintComponent(g);
    }
}

class IconData
{
	protected Icon   m_icon;
	protected Icon   m_expandedIcon;
	protected Object m_data;

	public IconData(Icon icon, Object data)
	{
		m_icon = icon;
		m_expandedIcon = null;
		m_data = data;
	}

	public IconData(Icon icon, Icon expandedIcon, Object data)
	{
		m_icon = icon;
		m_expandedIcon = expandedIcon;
		m_data = data;
	}

	public Icon getIcon() 
	{ 
		return m_icon;
	}

	public Icon getExpandedIcon() 
	{ 
		return m_expandedIcon!=null ? m_expandedIcon : m_icon;
	}

	public Object getObject() 
	{ 
		return m_data;
	}

	public String toString() 
	{ 
		return m_data.toString();
	}
}

class FileNode
{
	protected File m_file;

	public FileNode(File file)
	{
		m_file = file;
	}

	public File getFile() 
	{ 
		return m_file;
	}

	public String toString() 
	{ 
		return m_file.getName().length() > 0 ? m_file.getName() : 
			m_file.getPath();
	}

	public boolean expand(DefaultMutableTreeNode parent)
	{
		DefaultMutableTreeNode flag = 
			(DefaultMutableTreeNode)parent.getFirstChild();
		if (flag==null)	  // No flag
			return false;
		Object obj = flag.getUserObject();
		if (!(obj instanceof Boolean))
			return false;      // Already expanded

		parent.removeAllChildren();  // Remove Flag

		File[] files = listFiles();
		if (files == null)
			return true;

		Vector v = new Vector();

		for (int k = 0; k < files.length; k++)
		{
			File f = files[k];
			if (!(f.isDirectory()))
				continue;

			FileNode newNode = new FileNode(f);
			
			boolean isAdded = false;
			for (int i = 0; i < v.size(); i++)
			{
				FileNode nd = (FileNode)v.elementAt(i);
				if (newNode.compareTo(nd) < 0)
				{
					v.insertElementAt(newNode, i);
					isAdded = true;
					break;
				}
			}
			if (!isAdded)
				v.addElement(newNode);
		}

		for (int i = 0; i < v.size(); i++)
		{
			FileNode nd = (FileNode)v.elementAt(i);
			IconData idata = new IconData(FileTree1.ICON_FOLDER, 
				FileTree1.ICON_EXPANDEDFOLDER, nd);
			DefaultMutableTreeNode node = new 
				DefaultMutableTreeNode(idata);
			parent.add(node);
				
			if (nd.hasSubDirs())
				node.add(new DefaultMutableTreeNode( 
					new Boolean(true) ));
		}

		return true;
	}

	public boolean hasSubDirs()
	{
		File[] files = listFiles();
		if (files == null)
			return false;
		for (int k = 0; k < files.length; k++)
		{
			if (files[k].isDirectory())
				return true;
		}
		return false;
	}
	
	public int compareTo(FileNode toCompare)
	{ 
		return  m_file.getName().compareToIgnoreCase(
			toCompare.m_file.getName() ); 
	}

	protected File[] listFiles()
	{
		if (!m_file.isDirectory())
			return null;
		try
		{
			return m_file.listFiles();
		}
		catch (Exception ex)
		{
			JOptionPane.showMessageDialog(null, 
				"Error reading directory "+m_file.getAbsolutePath(),
				"Warning", JOptionPane.WARNING_MESSAGE);
			return null;
		}
	}
}

Understanding the Code

Class FileTree1

Four custom icons are loaded as static ImageIcon variables: ICON_COMPUTER, ICON_DISK, ICON_FOLDER, and ICON_EXPANDEDFOLDER, and three instance variables are declared:

JTree m_tree: tree component to display directory nodes.

DefaultTreeModel m_model: tree model to manage nodes.

JTextField m_display: component to display selected directory (acts as a status bar).

The FileTree1 constructor creates and initializes all GUI components. A root node "Computer" hosts child nodes for all disk partitions and network drives in the system. These nodes encapsulate Files retrieved with the static File.listRoots() method (a valuable addition to the File class in Java 2). Note that IconData objects (see below) encapsulate Files in the tree. Also note that each newly created child node immediately receives a child node containing a Boolean user object. This Boolean object allows us to display an expanding message for nodes when they are in the process of being expanded. Exactly how we expand them will be explained soon enough.

We then create a DefaultTreeModel and pass our "Computer" node as the root. This model is used to instantiate our JTree object:

m_model = new DefaultTreeModel(top);

m_tree = new JTree(m_model);

We then set the lineStyle client property so that angled lines will represent the edges between parent and child nodes:

m_tree.putClientProperty("JTree.lineStyle", "Angled");

We also use a custom tree cell renderer, as well as a tree exapansion listener and a tree selection listener, (each of which in detail below): IconCellRenderer, DirExpansionListener, and DirSelectionListener respectively.

The actual contents of our tree nodes represent directories. Each node is a DefaultMutableTreeNode with an IconData user object. Each user object is an instance of IconData, and each IconData contains an instance of FileNode. Each FileNode contain a java.io.File object. Thus we have a four layered nested structure:

DefaultMutableTreeNode is used for each node to represent a directory or disk (as well as the "Computer" root node). When we retrieve a node at the end of a given TreePath, using the getLastPathComponent() method, we are provided with an instance of this class.

IconData (see below) sits inside DefaultMutableTreeNode and provides custom icons for our tree cell renderer, and encapsulation of a FileNode object. IconData can be retrieved using DefaultMutableTreeNode‘s getUserObject() method. Note that we need to cast the returned Object to an IconData instance.

FileNode (see below) sits inside IconData and encapsulates a File objects. A FileNode can be retrieved using IconData’s getObject() method, also requireing a subsequent cast.

A File object sits inside a FileNode and can be retrieved using FileNode’s getFile() method.

Figure 17.2 illustrates this structure.

Figure 17.2 Nested structure of our tree nodes.

<<file figure17-2.gif>>

To keep things simple, two helper methods are provided to work with these encapsulated nodes: getTreeNode() retrieves a DefaultMutableTreeNode from a given TreePath, and getFileNode() retrieves the FileNode (or null) from a DefaultMutableTreeNode. We will see where these methods are needed soon enough.

Class FileTree1.DirExpansionListener

This inner class implements TreeExpansionListener to listen for tree expansion events. When a node is expanded, method treeExpanded() retrieves the FileNode instance for that node, and, if it's not null, calls the expand() method on it (see below). This is wrapped in a separate thread because it can often be a very time consuming process and we do not want the application to freeze. Inside this thread, once expand() has completed, we need to update the tree model with any new nodes retrieved. As we learned in chapter 2, updating the state of a component should on! ly occur within the event dispatching thread. For this reason we wrap the call to reload() in a Runnable and send it the event dispatching queue using SwingUtilities.invokeLater():

    Runnable runnable = new Runnable() {
      public void run() {
        m_model.reload(node);
      }
    };
    SwingUtilities.invokeLater(runnable);

As we will see below in our discussion of IconCellRenderer, by placing a Boolean user object in a dummy child node of each non-expanded node, we allow a certain String to be displayed while a node is in the process of being expanded. In our case, "Retreiving data..." is shown below a node until finished expanding.

Class FileTree1.DirSelectionListener

This inner class implements TreeSelectionListener to listen for tree selection events. When a node is selected, the valueChanged() method extracts the FileNode instance contained in that node, and, if it is not null, displays the absolute path to that directory in the m_display text field.

Class IconCellRenderer

This class implements the TreeCellRenderer interface and extends JLabel. The purpose of this renderer is to display custom icons and access FileNodes contained in IconData instances.

First we declare five Colors and retrieve them from the current look-and-feel in use through UIManager’s getColor() method. The getTreeCellRendererComponent() method is then implemented to set the proper text and icon (retrieved from the underlying IconData object). If the user object happens to be a Boolean, this signifies that a node is in the process of being expanded:

        if (obj instanceof Boolean) 
          setText("Retrieving data...");

The reason we do this is slightly confusing. In the FileNode expand() method (see below), when each new node is added to our tree it receives a node containing a Boolean user object only if the corresponding directory has sub-directories. When we click on this node, the Boolean child will be immediately shown, and we also generate an expansion event that is received by our DirExpansionListener. As we discussed above, this listener extracts the encapsulated FileNode and calls the FileNode expand() method on it. The child node containing the Boolean object is removed before all new nodes are added. Until this update occurs, the JTree will display the Boolean child node, in effect telling us that the expansion is not complete yet. So, if our cell renderer detects a Boolean user object, we simply display "Recieving data..." for its text.

The paintComponent() method is overridden to fill the text background with the appropriate color set in the getTreeCellRendererComponent() method. Fortunately we don't need to explicitly draw the text and icon because we have extended JLabel, which can do this for us.

Class IconData

Instances of this class are used as our DefaultMutableTreeNode user data objects, and they encapsulate a generic Object m_data and two Icons for use by IconCellRenderer. These icons can be retrieved with our getIcon() and getExpandedIcon() methods. The icon retrieved with getExpandedIcon() represents an expanded folder, and the icon retrieved wi! th getIcon() represents a collapsed/non-expanded folder. Note that the toString() method invokes toString() on the m_data object. In our example this object is either a FileNode, in the case of an expanded folder, or a Boolean, in the case of a non-expanded folder.

Class FileNode

This class encapsulates a File object, which is in turn encapsulated in an IconData object in a DefaultMutableTreeNode.

As we discussed above, the toString() method determines the text to be displayed in each tree cell containing a FileNode. It returns File.getName() for regular directories and File.getPath() for partitions.

The most interesting and complex method of this class is expand(), which attempts to expand a node by dynamically inserting new DefaultMutableTreeNode’s corresponding to each sub-directory. This method returns true if nodes are added, and false otherwise. First we need to discuss the mechanism of dynamically reading information (of any kind) into a tree:

Before we add any new node to the tree, we must determine somehow whether or not it has children (we don't need a list of children yet, just a yes or no answer).

If a newly created node has children, a fake child to be used as a flag will be added to it. This will signify that the parent node has not been expanded.

When a node is expanded, its list of children is examined. Three situations are possible:

No children. This node is a leaf and cannot be expanded (remember, we've checked previously whether or not any newly created node has children).

One flag child is present. That node has children which have not been added yet. So we create these children (depending on the nature of the tree) and add new nodes to the parent node.

One or more non-flag children are present. This node has already been processed, so expand it as usual.

Method FileNode.expand() implements this dynamic tree expansion strategy, and takes a parent node as parameter. In the process of expansion it also alphabetically sorts each node for a more organized display structure. Initially this method checks the first child of the given parent node:

    DefaultMutableTreeNode flag =  (DefaultMutableTreeNode)parent.getFirstChild();
    if (flag==null)      // No flag
      return false;
    Object obj = flag.getUserObject();
    if (!(obj instanceof Boolean))
      return false;    // Already expanded

    parent.removeAllChildren();  // Remove Flag

If no child is found, it can only mean that this node was already checked and was found to be a true leaf (a directory with no sub-directories). If this isn’t the case then we extract the associated data object and check whether it is an instance of Boolean, i.e. it is a flag child. If yes, the flag child is removed and our method proceeds to add nodes corresponding to each sub-directory. Otherwise we conclude that this node has already been processed and return, allowing it to be expanded as usual.

We process a newly expanded node by retrieving an array of File objects representing files contained in the corresponding directory:

File[] files = listFiles(); if (files == null) return true;

If the contents have been successfully read, we check for sub-directories and create new FileNodes for each:

    Vector v = new Vector();

    for (int k=0; k<files.length; k++) {
      File f = files[k];
      if (!(f.isDirectory()))
        continue;

      FileNode newNode = new FileNode(f);
           

To perform alphabetical sorting of child nodes we store them in a temporary collection Vector v, and iterate through our array of Files, inserting them accordingly

      boolean isAdded = false;
      for (int i=0; i<v.size(); i++) {
        FileNode nd = (FileNode)v.elementAt(i);
        if (newNode.compareTo(nd) < 0) {
          v.insertElementAt(newNode, i);
          isAdded = true;
           break;
        }
      }
      if (!isAdded)
      v.addElement(newNode);
    }

We then wrap each newly created FileNode object in an IconData to bind them with folder icons, and add the sorted nodes to the given parent node. At the same time, flags are added to new nodes if they contain any sub-directories themselves (this is checked by the hasSubDirs() method):

    for (int i=0; i<v.size(); i++) {
      FileNode nd = (FileNode)v.elementAt(i);
      IconData idata = new IconData(FileTree1.ICON_FOLDER, 
      FileTree1.ICON_EXPANDEDFOLDER, nd);
      DefaultMutableTreeNode node = new 
        DefaultMutableTreeNode(idata);
      parent.add(node);
      if (nd.hasSubDirs())
        node.add(new DefaultMutableTreeNode( 
          new Boolean(true) ));
    }
    return true;

The rest of FileNode class implements three methods which do not require much explanation at this point:

boolean hasSubDirs() returns true if this directory has sub-directories and false otherwise.

int compareTo(FileNode toCompare) returns the result of the alphabetical comparison of this directory with another given as parameter.

File[] listFiles() reads a list of contained flies in this directory. If an exception occurs (possible while reading from a floppy disk or network drive) this method displays a warning message and returns null.

Running the Code

Figure 17.3 shows our directory tree at work. Note the use of custom icons for partition roots. Try selecting various directories and note how the selected path is reflected at the top of the frame in our status bar. Also note that when expanding large directories "Retrieving data.." will be displayed underneath the corresponding node. Because we have properly implemented multithreading, we can go off and expand other directories while this one is being processed. Also not that the tree is always updated correctly when the expanding procedure completes because we have made sure to only change its state in the event dispatching thread using invokeLater().

 

UI Guideline : When to use connecting lines

Angled connecting lines (or edges ) add visual noise and clutter to a tree display. Reduced visual clutter aiding recognition and comprehension is a clear advantage to leaving them out of the design. So when is it appropriate to have them included?

Include the line edges when it is likely that:

(a) several nodes may be expanded at one time and/or

(b) the data set is very large and a node may expand off the bottom of the screen and perhaps several screens deep. In this case the introduction of the lines, helps to give the viewer a clear picture of how many layers deep in the hierarchy they are viewing and makes it easier to trace back to the original root node.

 

17.4 Directories tree: part II - popup menus and programmatic navigation

The example in the previous section can be extended in numerous ways to serve as a framework for a much more flexible application. In this section we'll add simple popup menus to our tree, displayed in response to a right mouse click, with content dependant on the click location. (We discussed popup menus in chapter 12.)

Our popup menu either contains an "Expand" or "Collapse" item depending on the status of the corresponding node nearest to the mouse click. These items will programatically invoke an expand or collapse of the given node. Our popup menu also contains "Delete" and "Rename" dummy items that are not completely implemented, but explicitly illustrate how we might continue to build upon this example to create a more complete directory explorer application.

Figure 17.4 Node dependant popup menus allowing programmatic expand and collapse.

<<file figure17-4.gif>>

The Code: FileTree2.java

see \Chapter17\3

import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.*;
import java.text.SimpleDateFormat;

import javax.swing.*;
import javax.swing.tree.*;
import javax.swing.event.*;

public class FileTree2 	extends JFrame 
{
	public static final ImageIcon ICON_COMPUTER = 
		new ImageIcon("computer.gif");
	public static final ImageIcon ICON_DISK = 
		new ImageIcon("disk.gif");
	public static final ImageIcon ICON_FOLDER = 
		new ImageIcon("folder.gif");
	public static final ImageIcon ICON_EXPANDEDFOLDER = 
		new ImageIcon("expandedfolder.gif");

	protected JTree  m_tree;
	protected DefaultTreeModel m_model;
	protected JTextField m_display;

	// NEW
	protected JPopupMenu m_popup;
	protected Action m_action;
	protected TreePath m_clickedPath;

	public FileTree2()
	{
		super("Directories Tree [Popup Menus]");
		setSize(400, 300);

		DefaultMutableTreeNode top = new DefaultMutableTreeNode(
			new IconData(ICON_COMPUTER, null, "Computer"));

		DefaultMutableTreeNode node;
		File[] roots = File.listRoots();
		for (int k=0; k < roots.length; k++)
		{
			node = new DefaultMutableTreeNode(new IconData(ICON_DISK, 
				null, new FileNode(roots[k])));
			top.add(node);
			node.add(new DefaultMutableTreeNode( new Boolean(true) ));
		}

		m_model = new DefaultTreeModel(top);
		m_tree = new JTree(m_model);

                m_tree.putClientProperty("JTree.lineStyle", "Angled");

		TreeCellRenderer renderer = new 
			IconCellRenderer();
		m_tree.setCellRenderer(renderer);

		m_tree.addTreeExpansionListener(new 
			DirExpansionListener());

		m_tree.addTreeSelectionListener(new 
			DirSelectionListener());

		m_tree.getSelectionModel().setSelectionMode(
			TreeSelectionModel.SINGLE_TREE_SELECTION); 
		m_tree.setShowsRootHandles(true); 
		m_tree.setEditable(false);

		JScrollPane s = new JScrollPane();
		s.getViewport().add(m_tree);
		getContentPane().add(s, BorderLayout.CENTER);

		m_display = new JTextField();
		m_display.setEditable(false);
		getContentPane().add(m_display, BorderLayout.NORTH);

		// NEW
		m_popup = new JPopupMenu();
		m_action = new AbstractAction() 
		{ 
			public void actionPerformed(ActionEvent e)
			{
				if (m_clickedPath==null)
					return;
				if (m_tree.isExpanded(m_clickedPath))
					m_tree.collapsePath(m_clickedPath);
				else
					m_tree.expandPath(m_clickedPath);
			}
		};
		m_popup.add(m_action);
		m_popup.addSeparator();

		Action a1 = new AbstractAction("Delete") 
		{ 
			public void actionPerformed(ActionEvent e)
			{
                                m_tree.repaint();
				JOptionPane.showMessageDialog(FileTree2.this, 
					"Delete option is not implemented",
					"Info", JOptionPane.INFORMATION_MESSAGE);
			}
		};
		m_popup.add(a1);

		Action a2 = new AbstractAction("Rename") 
		{ 
			public void actionPerformed(ActionEvent e)
			{
                                m_tree.repaint();
				JOptionPane.showMessageDialog(FileTree2.this, 
					"Rename option is not implemented",
					"Info", JOptionPane.INFORMATION_MESSAGE);
			}
		};
		m_popup.add(a2);
		m_tree.add(m_popup);
		m_tree.addMouseListener(new PopupTrigger());

		WindowListener wndCloser = new WindowAdapter()
		{
			public void windowClosing(WindowEvent e) 
			{
				System.exit(0);
			}
		};
		addWindowListener(wndCloser);
		
		setVisible(true);
	}

	DefaultMutableTreeNode getTreeNode(TreePath path)
	{
		return (DefaultMutableTreeNode)(path.getLastPathComponent());
	}

	FileNode getFileNode(DefaultMutableTreeNode node)
	{
		if (node == null)
			return null;
		Object obj = node.getUserObject();
		if (obj instanceof IconData)
			obj = ((IconData)obj).getObject();
		if (obj instanceof FileNode)
			return (FileNode)obj;
		else
			return null;
	}

	// NEW
	class PopupTrigger extends MouseAdapter
	{
		public void mouseReleased(MouseEvent e)
		{
			if (e.isPopupTrigger())
			{
				int x = e.getX();
				int y = e.getY();
				TreePath path = m_tree.getPathForLocation(x, y);
				if (path != null)
				{
					if (m_tree.isExpanded(path))
						m_action.putValue(Action.NAME, "Collapse");
					else
						m_action.putValue(Action.NAME, "Expand");
					m_popup.show(m_tree, x, y);
					m_clickedPath = path;
				}
			}
		}
	}

    // Make sure expansion is threaded and updating the tree model
    // only occurs within the event dispatching thread.
    class DirExpansionListener implements TreeExpansionListener
    {
        public void treeExpanded(TreeExpansionEvent event)
        {
            final DefaultMutableTreeNode node = getTreeNode(
                event.getPath());
            final FileNode fnode = getFileNode(node);

            Thread runner = new Thread() 
            {
              public void run() 
              {
                if (fnode != null && fnode.expand(node)) 
                {
                  Runnable runnable = new Runnable() 
                  {
                    public void run() 
                    {
                       m_model.reload(node);
                    }
                  };
                  SwingUtilities.invokeLater(runnable);
                }
              }
            };
            runner.start();
        }

        public void treeCollapsed(TreeExpansionEvent event) {}
    }

	class DirSelectionListener implements TreeSelectionListener 
	{
		public void valueChanged(TreeSelectionEvent event)
		{
			DefaultMutableTreeNode node = getTreeNode(
				event.getPath());
			FileNode fnode = getFileNode(node);
			if (fnode != null)
				m_display.setText(fnode.getFile().
					getAbsolutePath());
			else
				m_display.setText("");
		}
	}

	public static void main(String argv[]) 
	{
		new FileTree2();
	}
}

// Unchanged code from section 17.3

Understanding the Code

Class FileTree2

This example adds three new instance variables:

JPopupMenu m_popup: popup menu component

Action m_action: context sensitive menu action.

TreePath m_clickedPath: most recent tree path corresponding to a mouse click.

New code in the base class constructor creates a popup menu component and populates it with three menu items: "Expand", " Delete", and "Rename". The last two items intentionally just display an "option is not implemented" message. Their true implementations would take us too far into file manipulation techniques for this chapter. The first one, on the contrary, is quite meaningful here. The corresponding actionPerformed() method uses the recently clicked path (not necessary the currently selected path) which has been set by the PopupTrigger instance (see below). This path is collapsed if it currently expanded, or expanded it if this path is currently collapsed.

Finally this newly created popup menu is added to our tree component. An instance of our PopupTrigger class is also attatched to our tree as a mouse listener.

Class FileTree2.PopupTrigger

This class extends MouseAdapter to trigger the display of our popup menu. This menu should be displayed when the right mouse button is released. So we override the mouseReleased() method and check whether isPopupTrigger() is true (see MouseEvent API docs). In this case we determine the coordinates of the click and retrieve the TreePath corresponding to that coordinate with the getPathForLocation() method. If a path is not found (i.e. the click does not occur on a tree node or leaf) we do noth! ing. Otherwise we adjust the title of the first menu item accordingly, display our popup menu with the show() method, and store our recently clicked path in the m_clickedPath instance variable (for use by the expand/collapse Action as discussed above).

Running the Code

Figure 17.4 shows our directories tree application displaying a context sensitive popup menu. Note how the first menu item is changed depending on whether the selected tree node is collapsed or expanded. Also note that the tree can be manipulated (expanded or collapsed) programmatically by choosing the "Collapse" or "Expand" popup menu item.

 

UI Guideline : Visually re-inforcing variations in behaviour

If you intend to introduce context dependant pop-up menus on tree cells, then this is an ideal time to consider the use of a tree cell renderer which incorporates an icon. The differing icons help to re-inforce the idea that the data on the cell is of different types and consequently when the behaviour is slightly different across nodes, it is less surprising. The icon visually re-inforces the difference in behaviour.

 

17.5 Directories tree: part III - tooltips

As we discussed in chapter 5, tooltips are commonly used to display helpful information. In this example we will show how to use tooltips specific to each tree cell. The key point (which is mentioned in the JTree documentation, but can be easily overlooked) is to register the tree component with the ToolTipManager instance:

ToolTipManager.sharedInstance().registerComponent(myTree);

Without doing this, no tooltips will appear over our tree (refer back to chapter 2, section 2.5, for more about shared instances and service classes).

The JTree component overrides the getToolTipText(MouseEvent ev) method inherited from JComponent, and delegates this call to the tree's cell renderer component. By implementing the getToolTipText(MouseEvent ev) method in our renderer we can allow cell-specific tooltips. Specifically, we can can return the tooltip text as a String depending on the last node passed to the getTreeCellRendererComponent() method. Alternatively we can subclass our JTree component and ! provide our own getToolTipText() implementation. We use the latter method here.

Figure 17.5 JTree with node-specific tooltips.

<<file figure17-5.gif>>

The Code: FileTree2.java

see \Chapter17\4

import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.*;

import javax.swing.*;
import javax.swing.tree.*;
import javax.swing.event.*;

public class FileTree3 extends JFrame 
{
  // Unchanged code from section 17.4

public FileTree3() {
    super("Directories Tree [Tool Tips]");
    setSize(400, 300);
    getContentPane().setLayout(new BorderLayout());

    // Unchanged code

    m_model = new DefaultTreeModel(top);
    m_tree = new JTree(m_model) {
     public String getToolTipText(MouseEvent ev) {
         if (ev == null)
           return null;
         TreePath path = m_tree.getPathForLocation(ev.getX(), ev.getY());
         if (path != null) {
           FileNode fnode = getFileNode(getTreeNode(path));
           if (fnode==null)
             return null;
           File f = fnode.getFile();
           return (f==null ? null : f.getPath());
         }
         return null;
    }
};

ToolTipManager.sharedInstance().registerComponent(m_tree);

  // The rest of the code is unchanged

Understanding the Code

Class FileTree3

This example anonymously subclasses the JTree component to override getToolTipText(MouseEvent ev), which finds the path closest to the current mouse location, determines the FileNode at the end of that path, and returns the full file path to that node as a String for use as tooltip text. Also note that our JTree component is manually registered with the shared instance of ToolTipManager, as discussed above.

Running the Code

Figure 17.5 shows our directories tree application displaying a tooltip with text specifying the full path of the directory corresponding to the node nearest to the current mouse location.

 

UI Guideline : Tooltips as an aid to selection

Tooltips have two really useful advantages for tree cells. Trees have a habit of wandering off to the right hand side of a display, particularly in deep hierarchies. This may result in cell labels being clipped. Using the tooltip to display the full length cell label, will speed selection and prevent the need for scrolling.

The second use is shown clearly in this example. The tooltip is used to unravel the hierarchy. This would be particularly useful when the original root node is off screen. The User can see quickly, the full hierarchical path to the selected cell. This is a very powerful aid to correct selection. Another example of additional coding effort providing improved usability.

 

17.6 JTree and XML documents

Many see the future of the web in XML™ (Extensible Markup Language). This standard will most likely replace HTML. Unlike HTML, XML allows the definition of custom document tags, allowing the transmition of virtually any type of information over the web. Many sources of information about XML are available, including the standard definition at http://www.w3.org/TR/WD-xml-lang-970331.html.

XML documents have a tree-like structure, and JTree can be very useful for constructing an XML structure browser. In this section we'll show how to build a simple implementation of such a browser. We do not intend to give an introduction to XML, and we will not discuss Sun's API for XML in detail (which contains a fair number of classes and is likely to change significantly in the near future). However, a brief introduction to the XML classes used in this example is appropriate.

 

Note: Sun’s early access XML library is required to run this example. See http://www.javasoft.com/.

 

17.6.1 XmlDocument

class com.sun.xml.tree.XmlDocument

This class represents a top level XML 1.0 document created with the XML parser built into XmlDocumentBuilder. The getDocumentElement() method retrieves the top-most node in the document (i.e. the root node). All other nodes can be extracted through this node.

17.6.2 XmlDocumentBuilder

class com.sun.xml.tree.XmlDocumentBuilder

This class builds an XML document using an internal parser. Two overloaded methods, createXmlDocument(InputStream in) and createXmlDocument(String documentURL), create new XmlDocument instances. (We will use the latter method in this example.)

17.6.3 DataNode

class com.sun.xml.tree.DataNode

This class represents a node in an XML document tree that encapsulates data and has no child nodes (i.e. a leaf node). Method getData() retrieves the contained data as a String.

17.6.4 ElementNode

class com.sun.xml.tree.ElementNode

This class encapsulates an element in the XML document tree, which is a node with children (i.e. a non-leaf node). The getLength() method retrieves the number of child nodes, and item(int index) returns the node with the given index.

Figure 17.6 An XML document structure tree.

<<file figure17-6.gif>>

The Code: XmlTree.java

see \Chapter17\5

import java.awt.*;
import java.awt.event.*;
import java.util.*;
import java.io.*;
import java.net.*;

import javax.swing.*;
import javax.swing.tree.*;
import javax.swing.event.*;

import com.sun.xml.tree.*;
import com.sun.xml.parser.*;
import org.w3c.dom.*;

public class XmlTree extends JFrame 
{

	protected JTree  m_tree;
	protected DefaultTreeModel m_model;
	protected JTextField m_location;

	public XmlTree()
	{
		super("XML Tree");
		setSize(400, 300);
		getContentPane().setLayout(new BorderLayout());

		m_location = new JTextField();
		m_location.setText("samples\\book-order.xml");
		ActionListener lst = new ActionListener() 
		{ 
			public void actionPerformed(ActionEvent e)
			{
				readXml(m_location.getText());
			}
		};
		m_location.addActionListener(lst);
		getContentPane().add(m_location, BorderLayout.NORTH);

		DefaultMutableTreeNode top = new DefaultMutableTreeNode(
			"Empty");
		m_model = new DefaultTreeModel(top);
		m_tree = new JTree(m_model);

		m_tree.getSelectionModel().setSelectionMode(
			TreeSelectionModel.SINGLE_TREE_SELECTION);            
		m_tree.setShowsRootHandles(true); 
		m_tree.setEditable(false);

		JScrollPane s = new JScrollPane();
		s.getViewport().add(m_tree);
		getContentPane().add(s, BorderLayout.CENTER);

		WindowListener wndCloser = new WindowAdapter()
		{
			public void windowClosing(WindowEvent e) 
			{
				System.exit(0);
			}
		};
		addWindowListener(wndCloser);
		
		setVisible(true);
	}

	public void readXml(String sUrl)
	{
		setCursor( Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR) );
		try
		{
			URL source;
			try
			{
				File f = new File(sUrl);
				source = f.toURL();
			}
			catch (Exception ex)
			{
				source = new URL(sUrl);
			}

			XmlDocument doc = XmlDocumentBuilder.createXmlDocument(
				source.toString());

			ElementNode root = (ElementNode)doc.getDocumentElement();
			root.normalize();

			DefaultMutableTreeNode top = createTreeNode(root);

			m_model.setRoot(top);
			m_tree.treeDidChange();
		}
		catch (Exception ex)
		{
			ex.printStackTrace();
			JOptionPane.showMessageDialog(this, 
				ex.toString(), "Warning", 
				JOptionPane.WARNING_MESSAGE);
		}
		setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
	}

	protected DefaultMutableTreeNode createTreeNode(ElementNode root)
	{
		DefaultMutableTreeNode node = new DefaultMutableTreeNode(
			root.getNodeName());
		for (int k = 0; k < root.getLength(); k++)
		{
			Node nd = root.item(k);

			if (nd instanceof DataNode)
			{
				DataNode dn = (DataNode)nd;

				String data = dn.getData().trim();
				if (data.equals("\n") || data.equals("\r\n"))
					data = "";
				if (data.length() > 0)
					node.add(new DefaultMutableTreeNode(data));
			}
			else if (nd instanceof ElementNode)
			{
				ElementNode en = (ElementNode)nd;
				node.add(createTreeNode(en));
			}
		}
		return node;
	}

	public static void main(String argv[]) 
	{
		new XmlTree();
	}
}

Understanding the Code

Class XmlTree

Instance variables:

JTree m_tree: used to display an XML document.

DefaultTreeModel m_model: used to store the content of an XML document.

JTextField m_location: used for entry of a file name or URL location of an XML document.

Initially the JTree component receives a single node, "Empty". An ActionListener is added to the m_location text field which calls our readXml() method passing it the current text.

The readXml() method loads an XML document, corresponding to the String passed as parameter, into our tree model. The body of this method is placed in a separate thread because it can be a very expensive procedure, and we want to make sure not to clog up the event dispatching thread (to retain GUI responsiveness). First the given string is treated as a file name. A File instance is created and converted to a URL. If this does not succeed, the string is treated as a URL address:

          URL source;
          try {
            File f = new File(sUrl);
            source = f.toURL();
          }
          catch (Exception ex) {
            source = new URL(sUrl);
          }

The static method XmlDocumentBuilder.createXmlDocument() creates an XmlDocument corresponding to the resulting URL. As soon as this finishes (which may take a while for large documents), the root ElementNode is retrieved from that document:

          XmlDocument doc = XmlDocumentBuilder.createXmlDocument(source.toString());

          ElementNode root = (ElementNode)doc.getDocumentElement();
          root.normalize();

We then transform our XML document into a structure suitable for addition to our Swing tree model. Our createTreeNode() method does this job, returning the top-most node (the root node) as a DefaultMutableTreeNode. Finally that node is set as a root of our tree model, and our tree component is notified that it's content has changed:

          DefaultMutableTreeNode top = createTreeNode(root);
          m_model.setRoot(top);
          m_tree.treeDidChange();

Note: Normally we are expected to avoid calling the treeDidChange() method directly, as it should be called by our UI as needed. However, in this case our JTree will not update correctly without it.

The createTreeNode() method creates a DefaultMutableTreeNode from an ElementNode provided. First it creates a root node corresponding to the given ElementNode, and then all lower level nodes are retrieved and processed in turn to populate the whole tree:

    DefaultMutableTreeNode node = new DefaultMutableTreeNode(root.getNodeName());
    for (int k=0; k<root.getLength(); k++) {
      Node nd = root.item(k);

Two possibilities are reconciled during this procedure. If a newly processed node is an instance of DataNode, its text is retrieved and is used as the user data object for a new DefaultMutableTreeNode which is then added to the parent node (in this case care should be taken to avoid empty nodes containing only end-of-line symbols). Otherwise, if a newly processed node is an instance of ElementNode, method createTreeNode() is called recursively.

Running the Code

Figure 17.6 shows our XML Tree example displaying the contents of "The Two Gentlemen of Verona" XML document which can be found at ftp://sunsite.unc.edu/pub/sun-info/standards/xml/eg/.

 

17.7 Custom editors and renderers

In this section we'll construct a simple family tree application. We will show how to use a custom cell editor for name entry, as well as a custom cell renderer which displays an icon corresponding to a node’s data rather than its state. This example allows dynamic node insertion, and each node can have no more than two children.

Our representation of an ancestor tree is structured differently than how we normally think of structuring trees, even though, technically speaking, both methods are equivalent. Our root tree node represents a child, and child tree nodes represent parents, grandparents, etc., of that child. So a parent node in this JTree actually corresponds to a child in the family ancestry. This illustrates that JTree is flexible enough to allow adaptation to any type of heirarchical data set, including a dynamically changing one (as we also saw in our file directories tree examples above).

Figure 17.7 JTree with custom editor and cell renderer enforcing nodes with two children.

<<file figure17-7.gif>>

The Code: AncestorTree.java

see \Chapter17\6

import java.awt.*;
import java.awt.event.*;
import java.util.*;

import javax.swing.*;
import javax.swing.border.*;
import javax.swing.event.*;
import javax.swing.tree.*;

public class AncestorTree extends JFrame
{
  public static ImageIcon ICON_SELF = 
    new ImageIcon("myself.gif");
  public static ImageIcon ICON_MALE = 
    new ImageIcon("male.gif");
  public static ImageIcon ICON_FEMALE = 
    new ImageIcon("female.gif");

  protected JTree  m_tree;
  protected DefaultTreeModel m_model;
  protected IconCellRenderer m_renderer;
  protected IconCellEditor m_editor;

  public AncestorTree() {
    super("Ancestor Tree");
    setSize(500, 400);

    DefaultMutableTreeNode top = new DefaultMutableTreeNode(
      new IconData(ICON_SELF, "Myself"));
    addAncestors(top);
    m_model = new DefaultTreeModel(top);
    m_tree = new JTree(m_model);
    m_tree.getSelectionModel().setSelectionMode(
      TreeSelectionModel.SINGLE_TREE_SELECTION);
    m_tree.setShowsRootHandles(true); 
    m_tree.setEditable(true);

    m_renderer = new IconCellRenderer();
    m_tree.setCellRenderer(m_renderer); 
    m_editor = new IconCellEditor(m_tree);
    m_tree.setCellEditor(m_editor);
    m_tree.setInvokesStopCellEditing(true);

    m_tree.addMouseListener(new TreeExpander()); 

    JScrollPane s = new JScrollPane();
    s.getViewport().add(m_tree);
    getContentPane().add(s, BorderLayout.CENTER);

    WindowListener wndCloser = new WindowAdapter() {
      public void windowClosing(WindowEvent e) {
        System.exit(0);
      }
    };
    addWindowListener(wndCloser);

    setVisible(true);
  }

  public boolean addAncestors(DefaultMutableTreeNode node) {
    if (node.getChildCount() > 0)
      return false;

    Object obj = node.getUserObject();
    if (obj == null)
      return false;
    node.add(new DefaultMutableTreeNode( new IconData(
      ICON_MALE, "Father of: "+obj.toString()) ));
    node.add(new DefaultMutableTreeNode( new IconData(
      ICON_FEMALE, "Mother of: "+obj.toString()) ));
    return true;
  }

  public static void main(String argv[]) { new AncestorTree(); }

  class TreeExpander extends MouseAdapter 
  {
    public void mouseClicked(MouseEvent e) {
      if (e.getClickCount() == 2) {
        TreePath selPath = m_tree.getPathForLocation(
          e.getX(), e.getY());
        if (selPath == null)
          return;
        DefaultMutableTreeNode node = 
          (DefaultMutableTreeNode)(selPath.
             getLastPathComponent());
        if (node!=null && addAncestors(node)) {
          m_tree.expandPath(selPath);
          m_tree.repaint();
        }
      }
    }
  }
}

// Classes IconCellRenderer and IconData are
// unchanged from previous examples, and are 
// not listed here to conserve space.

class IconCellEditor extends JLabel 
 implements TreeCellEditor, ActionListener
{
  protected JTree m_tree = null;
  protected JTextField m_editor = null;
  protected IconData m_item = null;
  protected int m_lastRow = -1;
  protected long m_lastClick = 0;
  protected Vector m_listeners = null;

  public IconCellEditor(JTree tree) {
    super();
    m_tree = tree;
    m_listeners = new Vector();
  }

  public Component getTreeCellEditorComponent(JTree tree, 
   Object value, boolean isSelected, boolean expanded, 
   boolean leaf, int row)
  {
    if (value instanceof DefaultMutableTreeNode) {
      DefaultMutableTreeNode node = 
        (DefaultMutableTreeNode)value;
      Object obj = node.getUserObject();
      if (obj instanceof IconData) {
        IconData idata = (IconData)obj;
        m_item = idata;
        // Reserve some more space...
        setText(idata.toString()+"     "); 
        setIcon(idata.m_icon);
        setFont(tree.getFont());
        return this;
      }
    }
    // We don't support other objects...
    return null;
  }

  public Object getCellEditorValue() { 
    if (m_item != null && m_editor != null)
      m_item.m_data = m_editor.getText();
    return m_item; 
  }

  public boolean isCellEditable(EventObject evt) { 
    if (evt instanceof MouseEvent) {
      MouseEvent mEvt = (MouseEvent)evt;
      if (mEvt.getClickCount() == 1) {
        int row = m_tree.getRowForLocation(mEvt.getX(), mEvt.getY());
        if (row != m_lastRow) {
          m_lastRow = row;
          m_lastClick = System.currentTimeMillis();
          return false;
        }
        else if (System.currentTimeMillis()-m_lastClick > 1000)
        {
          m_lastRow = -1;
          m_lastClick = 0;
          prepareEditor();
          mEvt.consume();
          return true;
        }
        else
          return false; 
      }
    }
    return false; 
  }

  protected void prepareEditor() {
    if (m_item == null)
      return;
    String str = m_item.toString();

    m_editor = new JTextField(str);
    m_editor.addActionListener(this);
    m_editor.selectAll();
    m_editor.setFont(m_tree.getFont());
        
    add(m_editor);
    revalidate();

    TreePath path = m_tree.getPathForRow(m_lastRow);
    m_tree.startEditingAtPath(path);
  }

  protected void removeEditor() {
    if (m_editor != null) {
      remove(m_editor);
      m_editor.setVisible(false);
      m_editor = null;
      m_item = null;
    }
  }

  public void doLayout() {
    super.doLayout();
    if (m_editor != null) {
      int offset = getIconTextGap();
      if (getIcon() != null)
        offset += getIcon().getIconWidth();
      Dimension cSize = getSize();
      m_editor.setBounds(offset, 0, cSize.width - offset,
        cSize.height);
    }
  }

  public boolean shouldSelectCell(EventObject evt) { return true; }

  public boolean stopCellEditing() {
    if (m_item != null)
      m_item.m_data = m_editor.getText();

    ChangeEvent e = new ChangeEvent(this);
    for (int k=0; k<m_listeners.size(); k++) {
      CellEditorListener l = (CellEditorListener)m_listeners.
        elementAt(k);
      l.editingStopped(e);
    }
    removeEditor();
    return true; 
  }

  public void cancelCellEditing() {
    ChangeEvent e = new ChangeEvent(this);
    for (int k=0; k<m_listeners.size(); k++) {
      CellEditorListener l = (CellEditorListener)m_listeners.
        elementAt(k);
      l.editingCanceled(e);
    }
    removeEditor();
  }

  public void addCellEditorListener(CellEditorListener l) {
    m_listeners.addElement(l);
  }

  public void removeCellEditorListener(CellEditorListener l) {
    m_listeners.removeElement(l);
  }

  public void actionPerformed(ActionEvent e) {
    stopCellEditing();
    m_tree.stopEditing();
  }
}

Understanding the Code

Class AncestorTree

Class AncestorTree declares and creates three static images representing male and female ancestors, and a root (representing a child whose ancestry is being represented).

Instance variables:

JTree m_tree: ancestors tree component.

DefaultTreeModel m_model: ancestor tree data model.

IconCellRenderer m_renderer: ancestor tree cell renderer.

IconCellEditor m_editor: ancestor tree cell editor.

The AncestorTree constructor is similar to the Tree1 constructor from section 17.2. However, there are some important differences:

addAncestors() method (see below) is called to create initial child nodes.

The editable property is set to true.

Instances of our custom IconCellRenderer and IconCellEditor classes are set as the renderer and editor for m_tree, respectively.

An instance of our custom TreeExpander class is attached as a MouseListener to our m_tree.

The addAncestors() method adds male and female ancestors to a given node as child nodes (representing parents in an ancestry), if this hasn't been done already. Instances of IconData (which hold a combination of icons and text, see previous examples) are added as user objects for a newly created node. The initial text of each IconData object is assigned as "Father of: <node text>" and " Mother of: <node text>", and appropriate icons are used to distinguish between women and men.

Class AncestorTree.TreeExpander

The TreeExpander inner class extends MouseAdapter and is used to insert ancestor nodes when a double click occurs on a node. The most significant aspect of this listener is the call to the JTree.getPathForLocation() method, which retrieves the currently selected tree path and allows us to determine the selected node. The addAncestors() method is then called on that node, and our tree is repainted to show the newly added nodes (if any).

Class IconCellRenderer and class IconData

Class IconCellRenderer, as well as class IconData, have recieved no changes from section 17.3. Refer back to this section for more information about their inner workings.

Class IconCellEditor

This class implements the TreeCellEditor and ActionListener interfaces, and creates a JTextField for editing a node's text in-place. This editor is designed in such a way that a cell icon remains in unchanged and in the same location whether in editing mode or not. This explains why IconCellEditor extends JLabel, and not JTextField as we might expect. The underlying JLabel component is used to render the icon and reserve ! any ecessary space. The JTextField component is created dynamically and placed above the JLabel's text portion to perform the actual editing. Note that the JLabel serves as a Container for the JTextField component (recall that all Swing components extend the java.awt.Container class).

 

Note: Swing's default editor DefaultTreeCellEditor is used similarly, but in a more complex manner. It takes a DefaultTreeCellRenderer as parameter and uses it to determine the size of a node's icon. Then it uses a custom inner class as a container which renders the icon and positions the text field. For more details see DefaultTreeCellEditor.java.

Instance variables:

JTree m_tree: reference to the parent tree component (must be passed to constructor).

JTextField m_editor: editing component.

IconData m_item: data object to be edited (we limit ourselves to IconData instances).

int m_lastRow: tree row where the most recent mouse click occured.

long m_lastClick: time (in ms) that the last mouse click occurred.

Vector m_listeners: collection of CellEditorListeners, which we must manage according to the TreeCellEditor interface.

The getTreeCellEditorComponent() method will be called to initialize a new data object in the editor before we begin editing a node. (Note that the user object from the selected node must be an instance of IconData, or editing is not allowed.) The icon and text from the IconData object are assigned using inherited JLabel functionality. A few spaces are intentionally appended to the end of the text to provide some white space between the icon and the editing area (a very simply way to accomplish this).

The getCellEditorValue() method returns the current value of the editor as an Object. In our case we return m_item, adjusting its m_data to m_editor's text:

      if (m_item != null && m_editor != null)
        m_item.m_data = m_editor.getText();
      return m_item; 

Method isCellEditable() is called to determine whether an editor should enter editing mode. It takes an EventObject as parameter. In this way, we can use any user activity resulting in an event as a signal for editing, e.g. double mouse click, specific key press combination, etc. We’ve implemented this method to start editing mode when two single mouse clicks occur on a cell separated by no less than 1 second (1000 ms). To do this we first filter only single mouse click events:

      if (evt instanceof MouseEvent) {
        MouseEvent mEvt = (MouseEvent)evt;
        if (mEvt.getClickCount() == 1) {

For these events we determine the tree's row that was clicked and store it in our m_lastRow instance variable. The current system time is also saved in the m_lastClick instance variable. If another click occurs on the same cell after 1000 ms, our custom prepareEditor() method is called to prepare the editor for editing, and returns true. Otherwise false is returned.

Method shouldSelectCell()always returns true to indicate that a cell's content should be selected at the beginning of editing.

Method stopCellEditing() is called to stop editing and store the result of the edit. We simply change m_data in the m_item object to m_editor's text.

      if (m_item != null)
        m_item.m_data = m_editor.getText();

The m_item object also has a reference to our tree's model, so it will affect our tree directly. Thus all registered CellEditorListeners are notified in turn by calling their editingStopped() method. Finally the editing process is terminated by calling our custom removeEditor() method.

The cancelCellEditing() method is called to stop editing without storing the result of the edit. Similar to stopCellEditing(), all registered CellEditorListeners are notified in turn by calling their editingCanceled(), and the editing process is terminated by calling removeEditor().

Two methods, addCellEditorListener() and removeCellEditorListener(), add and remove listeners to/from our m_listeners vector.

The prepareEditor() method actually starts the editing process. It creates a JTextField component and sets its initial text to that of the m_item object:

      m_editor = new JTextField(str);
      m_editor.addActionListener(this);
      m_editor.selectAll();
      m_editor.setFont(m_tree.getFont());
        
      add(m_editor);
      revalidate();

An ActionListener is added to the text field to enable the ability to stop editing when the user presses the "Enter" key (recall that this class implements ActionListener, so we provide a this reference for the listener). The most important aspect of this method is the fact that a JTextField is added to our base component, and the overridden doLayout() method is invoked indirectly (through revalidate()) to assign the correct ! size and position to our editor component. Finally, the base tree is notified by calling our startEditingAtPath() method, to allow editing.

The removeEditor() method is called to quit the editing process. It removes the editing component from the container, hides and destroys it (dereferences it to allow garbage collection):

          remove(m_editor);
          m_editor.setVisible(false);
          m_editor = null;
          m_item = null;

Method doLayout() overrides Component.doLayout() to set the correct size and location for the editing component.

Method actionPerformed() will be called when "Enter" is pressed during editing. It directly calls our stopCellEditing() implementation, and notifies our tree by calling stopEditing().

Running the Code

Perform a single click on a tree cell, wait about a second, then click it again to enter editing mode. Note that there is no limit to how far back we can go with this ancestor tree, and all nodes can have either no children, or two children (representing the parents of the individual represented by that node). Try creating your own ancestor tree as far back as you can go (if you end with monkey or gorilla please contact us). Figure 17.7 shows the ancestor tree of Prince William of Wales, the oldest son of the heir of the British monarchy.

 

UI Guideline : Family Trees and Organisation Charts

The family tree given here is used as an example only. There is still considerable debate in the UI Design field as to whether Tree component is appropriate for displaying and manipulating such data. Generally, Family Trees or Organisation Charts are displayed using a top-down (horizontal orientation), evenly distributed graph. Therefore, the Tree component view with its Left-Right (vertical orientation) is alien for this type of data.

If your User community is particularly technical then you should have no difficulties, however, consider carefully before selecting tree component for a wider user group, in this instance.

You may also like to consider that such a tree component could be used a prototype or "proof of concept". You could later replace the Tree component with an OrganisationChart component (for example) which re-uses the same TableModel interface. Thus the actual domain data and model classes would not need to be changed. The ability to do this, demonstrates the power of the Swing MVC architecture.