ORIGINAL DRAFT

Selecting elements from a JTree is one of those exercises that seems like it should be much simpler to deal with than it actually is. Swing provides incredible power in it’s architecture but the last mile is often the most difficult to travel. In this month’s installment we’ll develop a reusable set of classes that facilitate the development of a JTree with check box selections. To maximize flexibility, the node, renderer and editor objects will support containment of other node elements and renderers, so that existing behavior can easily be delegated.

Figure 1: JCheckTree Test in action. By default 
high-level select and deselect operations are automatically propagated down child nodes.

Figure 1: JCheckTree Test in action. By default high-level select and deselect operations are automatically propagated down child nodes.

Our implementation allows you to plug in your own renderers and data elements by wrapping a CheckTreeRenderer and CheckTreeEditor class around the renderer you want to use. By default this is the DefaultTreeCellRenderer to make it simple. I’ll provide a BasicTreeNode and associated BasicTreeCellRenderer to support arbitrary text nodes with alternate icons for demonstration purposes. As always, limited space precludes the listing of all the classes, so you’ll find them all online at www.java-pro.com.

A couple of comments about Swing are important at this point. First, you need to know that Swing doesn’t like to reference the same instance of a renderer in both the CheckTreeRenderer and CheckTreeEditor classes we’ll be using, so you’ll have to create a second instance to avoid problems. I wasn’t able to determine why this is such a big issue but it is and you’ll have no end of heartache if you do this, since it won’t be clear what you might have done wrong.

TreeCellEditor implementations also suffers from an interesting problem when used in a JTree component.. The getCellEditorValue method has to return a modified version of a mutable node to make this work properly. A clone or new instance of the modified node appears to cause problems when it comes to redrawing the tree.

These two problems alone cause a fair amount of debugging to take place when I implemented this and you may want to reference this article in the future if you find yourself implementing a check box or similar JTree variant. Fortunately, the rest of this implementation was relatively problem-free and I hope this walkthough will help you understand what it takes to do this and leave you with a set of reusable classes that make this whole process considerably easier.

The only significant shortcoming with the delegation approach we’ll be using is that alternate editors are not supported. The assumption is that you can use alternate renderers if you need them, but you’ll need to use the editor to handle the check box selection. A more complicated and comprehensive solution is certainly possible but is well beyond the scope of this article. In most cases, you will may to render custom nodes but the need for custom editors is most likely to be rare.

Figure 2: JCheckTree classes.

Figure 2: JCheckTree classes.

We’ll be throwing a class into the mix I think you’ll find of interest if you develop a lot of Swing components that need to implement a listener interface. SwingEventMulticaster inherits from the Java AWTEventMulticaster and implements the same list-based multicaster for all the Swing events and listeners. If you’ve never used the AWTEventMulticaster, it’s well worth reading the JavaDoc description which tells you how to apply the various methods you’ll need to us.

The principle behind the AWTEventMulticaster is simple enough and uses a linked list model to manage a thread-safe listener list. You can use the add and remove methods to add and remove listeners to and from the list, calling respective events methods directly or through delegation as you need them. This is a very useful approach that is easier to apply that the listenerList solution offered in Swing directly. We don’t have room to take a closer look but you can find the code online along with the rest of this project.

Take a look at Figure 2, which shows the classes used in the CheckTree component. The JCheckTree test makes use of the BasicTreeNode and BasicCellRenderer to demonstrate the use of alternate renderers. You can use the DefaultTreeNode and DefaultTreeCellRenderer to accomplish the same results but I wanted to use a variant to make it clear that you can use any suitable node/renderer combination, so it was important to include these in the JCheckTreeTest environment. We won’t say much about these classes, since they are not critical in this implementation.

The four classes that deserve attention are CheckTreeNode, CheckTreeRenderer, CheckTreeEditor and JCheckTree. Lets take a look at each of these in turn.

Listing 1 is the CheckTreeNode class. We extend DefaultMutableTreeNode and add the ability to set or get the current boolean selection state, while supporting any user-defined data element with setUserObject and getUserObject. The three constructors provide settings for the user object, the selection state and a flag indicating whether we should propagate selections to the children of this node. This is useful when dealing with check boxes, since the selection of a parent node often implies selecting it’s children. Naturally, we want this behavior to be based on circumstances, so we provide access to the flag directly in the constructor.

CheckTreeNode exposes a pair of accessor methods to deal with the selection state in the form of isSelected and setSelected. The propagateSelected method, called only when the propagate flag is set, uses the children method from the TreeNode interface to enumerate through the node’s children to reset the selection state on each child. You’ll notice we override the setUserObject method. For some reason, if you don’t filter out the condition where the current node is being set, you can end up in a recursive look that causes a stack overflow. This is easily avoided by ignoring that condition and returning immediately.

CheckTreeRenderer is shows in Listing 2. This class allows us to show the current selection state in a JCheckBox, rendering the user object with a specified renderer. Both constructors expect a reference to the parent JTree and an optional renderer. By default, we use the DefaultTreeCellRenderer if one is not specified. The display component is a JPanel with the JCheckBox in the WEST position. Notice that we use the JDK 1.3 method setBorderPaintedFlat to render the check box flatly against the background, rather than sunken as it normally would be. We also set a zero margin and make JCheckBox transparent with setOpaque set to false.

The render component is initially created in the constructor to avoid having to add the rendering component repeatedly to the panel. Each subsequent call to getTreeCellRendererComponent sets the component values for rendering but we expect the reference to be the same so we can avoid the overhead of adding the component to the container, removing the previous reference, and recalculating the layout positions. This is not only more efficient but avoids inconsistencies in later rendering.

You’ll find the code for CheckTreeEditor in Listing 3. The editor extends our CheckTreeCellRenderer and implements the TreeCellEditor and ActionListener interfaces. We use the ActionListener to listen to changes in the JCheckBox from the parent class. Whenever a change occurs, we call fireEditingStopped to make sure any cell editor listeners get the message. In the case of JTree, this allows the view to be updated whenever the user makes a selection. The ActionListener is registered in the constructor, which supports the same arguments as the parent class.

The TreeCellEditor interface requires the implementation of seven methods from the CellEditor interface and the getTreeCellEditorComponent, which closely mimics the getTreeCellRendererComponent. We use the SwingEventMulticaster class I mentioned earlier to implement the addCellEditorListener and removeCellEditorListener methods, as well as the fireEditingStopped and fireEditingCanceled methods. Notice how simple and straight forward this is with the SwingEventMulticaster.

The CellEditor methods isCellEditable and shouldSelectCell both return true. It’s not so obvious why stopCellEditing also returns true, unless you consider that we’re done editing the moment the user makes a selection so we can return to rendering immediately. I found the getCellEditorValue implementation difficult since the implication is that the return value can be a new instance of the required data element. In point of fact, the model is not updated this way so this only works if you modify an existing object, in our case by calling setSelected with the current check box state.

Listing 4 shows the code for JCheckTree, which ties everything into a usable component. All we’re doing here is subclassing JTree and setting the CheckTreeCellRenderer and CheckTreeCellEditor, as well as making the tree editable with a call to setEditable. We override the setCellRenderer method and add a new setEditorRenderer method. The first of these is intended to make the use of our CheckTreeCellRenderer completely transparent and the second allows us to set the same renderer for the editor.

Remember that you need to use a separate instance of the renderer you want to apply in the editor to avoid Swing rendering problems. This is a way of insuring success without constraining your ability to use different renderers if you prefer. You can see all this in action in the JCheckTreeTest, which you’ll find online with the rest of the code. JCheckTreeTest creates a hierarchy of CheckTreeNode objects and creates a JCheckTree, setting two instances of the BasicTreeCellRenderer with the setCellRenderer and setEditorRenderer methods, putting the JCheckTree in a JScrollPane for demonstration purposes.

JCheckTree is designed to enable you to easily work with selectable nodes. By supporting custom user objects and renderers, you can still maintain flexibility while reducing the amount of work required to implement the check box behavior. This has become a common requirement in modern applications, so I hope you’ll find this solution as reusable as it was intended. The SwingEventMulticaster class I threw in is one of those classes that should be part of the Swing release but was never implemented. I hope you find this code as useful as I did.

Listing 1

import javax.swing.*;

public class BasicTreeNode
{
  protected String text;
  protected Icon icon, open;
  
  public BasicTreeNode(String text, Icon icon)
  {
    this(text, icon, icon);
  }
  
  public BasicTreeNode(String text, Icon icon, Icon open)
  {
    this.text = text;
    this.icon = icon;
    this.open = open;
  }
  
  public static BasicTreeNode createComputer(String text)
  {
    return new BasicTreeNode(text,
      UIManager.getIcon("FileView.computerIcon"));
  }
  
  public static BasicTreeNode createDrive(String text)
  {
    return new BasicTreeNode(text,
      UIManager.getIcon("FileView.hardDriveIcon"));
  }
  
  public static BasicTreeNode createFile(String text)
  {
    return new BasicTreeNode(text,
      UIManager.getIcon("FileView.fileIcon"));
  }
  
  public static BasicTreeNode createDesk(String text)
  {
    return new BasicTreeNode(text,
      UIManager.getIcon("DesktopIcon.icon"));
  }
  
  public static BasicTreeNode createFolder(String text)
  {
    return new BasicTreeNode(text,
      UIManager.getIcon("Tree.closedIcon"),
      UIManager.getIcon("Tree.openIcon"));
  }

  public String getText()
  {
    return text;
  }
  
  public Icon getIcon()
  {
    return icon;
  }
  
  public Icon getOpen()
  {
    return open;
  }
}

Listing 2

import java.awt.*;
import java.util.*;
import javax.swing.*;
import javax.swing.tree.*;

public class CheckTreeCellRenderer extends JPanel
  implements TreeCellRenderer
{
  protected CheckTreeNode node;
  protected TreeCellRenderer renderer;
  protected JCheckBox check;

  public CheckTreeCellRenderer(JTree tree)
  {
    this(tree, new DefaultTreeCellRenderer());
  }

  public CheckTreeCellRenderer(JTree tree, TreeCellRenderer renderer)
  {
    setOpaque(false);
    setLayout(new BorderLayout());
    
    this.renderer = renderer;
    add(BorderLayout.CENTER, 
      renderer.getTreeCellRendererComponent(
        tree, "", true, true, true, 0, true));
    
    check = new JCheckBox();
    check.setMargin(new Insets(0, 0, 0, 0));
    check.setBorderPaintedFlat(true);
    check.setOpaque(false);
    add(BorderLayout.WEST, check);
  }

  public Component getTreeCellRendererComponent(
    JTree tree, Object value, boolean selected,
    boolean expanded, boolean leaf, int row,
    boolean hasFocus) 
  {
    if (value instanceof CheckTreeNode)
    {
      node = (CheckTreeNode)value;
      check.setSelected(node.isSelected());
      value = node.getUserObject();
    }
    renderer.getTreeCellRendererComponent(
      tree, value, selected, expanded, leaf, row, hasFocus);
    return this;
  }
  
}

Listing 3

import java.awt.*;
import java.util.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.tree.*;
import javax.swing.event.*;

public class CheckTreeCellEditor
  extends CheckTreeCellRenderer
  implements TreeCellEditor, ActionListener
{
  protected CellEditorListener list;

  public CheckTreeCellEditor(JTree tree)
  {
    this(tree, new DefaultTreeCellRenderer());
  }

  public CheckTreeCellEditor(JTree tree, TreeCellRenderer renderer)
  {
    super(tree, renderer);
    check.addActionListener(this);
  }

  public Component getTreeCellEditorComponent(
    JTree tree, Object value, boolean selected,
    boolean expanded, boolean leaf, int row)
  {
    return getTreeCellRendererComponent(
      tree, value, true, expanded, leaf, row, true);
  }

  public boolean stopCellEditing()
  {
    return true;
  }
  
  public Object getCellEditorValue()
  {
    node.setSelected(check.isSelected());
    return node;
  }
    
  public boolean isCellEditable(EventObject event)
  { 
    return true;
  } 

  public boolean shouldSelectCell(EventObject event)
  { 
    return true;
  }
    
  public void  cancelCellEditing()
  { 
    fireEditingCanceled();
  }

  public void addCellEditorListener(CellEditorListener listener)
  {
    list = SwingEventMulticaster.add(list, listener);
  }

  public void removeCellEditorListener(CellEditorListener listener)
  {
    list = SwingEventMulticaster.remove(list, listener);
  }

  protected void fireEditingStopped()
  {
    if (list != null)
      list.editingStopped(new ChangeEvent(this));
  }

  protected void fireEditingCanceled()
  {
    if (list != null)
      list.editingCanceled(new ChangeEvent(this));
  }
  
  public void actionPerformed(ActionEvent event)
  {
    fireEditingStopped();
  }
}

Listing 4

import java.awt.*;
import javax.swing.*;
import javax.swing.tree.*;

public class JCheckTree extends JTree
{
  public JCheckTree(TreeNode root)
  {
    super(root);
    super.setCellRenderer(new CheckTreeCellRenderer(this));
    setCellEditor(new CheckTreeCellEditor(this));
    setEditable(true);
  }
  
  public void setCellRenderer(TreeCellRenderer renderer)
  {
    super.setCellRenderer(new CheckTreeCellRenderer(this, renderer));
  }

  public void setEditorRenderer(TreeCellRenderer renderer)
  {
    setCellEditor(new CheckTreeCellEditor(this, renderer));
  }
}