Focus Navigation

We’ve brought up the topic of focus many times in our discussion so far, and we’ve told you that the handling and user navigation of focus is mostly done automatically. The focus system is very powerful and can be heavily customized through the use of “focus traversal policy” objects that control keyboard navigation. For typical application behavior, you won’t have to deal with this directly, but we’ll explain a few features you should know about.

Swing handles keyboard focus navigation through the KeyboardFocusManager class. This class uses FocusTraversalPolicy “strategy” objects that implement the actual schemes for locating the next component to receive focus. There are two primary FocusTraversalPolicy types supplied with Java. The first, DefaultFocusTraversalPolicy, is part of the AWT package. It emulates the legacy AWT-style focus management that navigated components in the order in which they were added to their container. The next, LayoutFocusTraversalPolicy, is the default for all Swing applications. It examines the layout and attempts to provide the more typical navigation from left to right and top to bottom, based on component position and size.

The focus traversal policy is inherited from containers and oriented around groups of components known as “root cycles.” By default, each individual window and JInternalFrame is its own root cycle. In other words, focus traverses all of its child components repeatedly (jumping from the last component back to the first), and won’t, by default, leave the container through keyboard navigation.

The default Swing policy uses the following keys for keyboard navigation:

Forward

Tab or Ctrl-Tab (Ctrl-Tab also works inside text areas)

Back

Shift-Tab or Ctrl-Shift-Tab (Ctrl-Shift-Tab also works inside text areas)

You can define your own focus traversal keys for forward and back navigation, as well as for navigation across root cycles using the setFocusTraversalKeys() method of a container. Here is an example that adds the keystroke Ctrl-N to the list of forward key navigation for components in a Frame:

    frame.getFocusTraversalKeys(
        KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS );
    AWTKeyStroke ks = AWTKeyStroke.getAWTKeyStroke(
        KeyEvent.VK_N, InputEvent.CTRL_DOWN_MASK );
    Set new = new HashSet( old );
    set.add( ks );
    frame.setFocusTraversalKeys(
        KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS,set);

Keys are defined by the AWTKeyStroke class, which encapsulates the key and input modifiers—in this case, the Control key. Constants in the KeyboardFocusManager specify forward, back, and up or down root cycle transfer across windows.

Finally, you can also move focus programmatically using the following methods of KeyboardFocusManager:

    focusNextComponent()
    focusPreviousComponent()
    upFocusCycle()
    downFocusCycle()

Trees

One of Swing’s advanced components is JTree. Trees are good for representing hierarchical information, like the contents of a disk drive or a company’s organizational chart. As with all Swing components, the data model is distinct from the visual representation. This means you can do things such as update the data model and trust that the visual component will be updated properly.

JTree is powerful and complex. It’s big enough, in fact, that like the text tools, the classes that support JTree have their own package, javax.swing.tree. However, if you accept the default options for almost everything, JTree is very easy to use. Figure 18-6 shows a JTree running in a Swing application that we’ll describe later.

The JTree class in action

Figure 18-6. The JTree class in action

Nodes and Models

A tree’s data model is made up of interconnected nodes. A node has a name—typically, a parent—and some number of children (possibly 0). In Swing, a node is represented by the TreeNode interface. Nodes that can be modified are represented by MutableTreeNode. A concrete implementation of this interface is DefaultMutableTreeNode. One node, called the root node, usually resides at the top of the hierarchy.

A tree’s data model is represented by the TreeModel interface. Swing provides an implementation of this interface called DefaultTreeModel. You can create a DefaultTreeModel by passing a root TreeNode to its constructor.

You could create a TreeModel with just one node like this:

    TreeNode root = new DefaultMutableTreeNode("Root node");
    TreeModel model = new DefaultTreeModel(root);

Here’s another example with a real hierarchy. The root node contains two nodes, Node 1 and Group. The Group node contains Node 2 and Node 3 as subnodes.

    MutableTreeNode root = new DefaultMutableTreeNode("Root node");
    MutableTreeNode group = new DefaultMutableTreeNode("Group");
    root.insert(group, 0);
    root.insert(new DefaultMutableTreeNode("Node 1"), 1);
    group.insert(new DefaultMutableTreeNode("Node 2"), 0);
    group.insert(new DefaultMutableTreeNode("Node 3"), 1);

The second parameter to the insert() method is the index of the node in the parent. After you organize your nodes, you can create a TreeModel in the same way as before:

    TreeModel model = new DefaultTreeModel(root);

Save a Tree

Once you have a tree model, creating a JTree is simple:

    JTree tree = new JTree(model);

The JTree behaves like a souped-up JList. As Figure 18-6 shows, the JTree automatically shows nodes with no children as a sheet of paper, while nodes that contain other nodes are shown as folders. You can expand and collapse nodes by clicking on the little knobs to the left of the folder icons. You can also expand and collapse nodes by double-clicking on them. You can select nodes; multiple selections are possible using the Shift and Control keys. And, as with a JList, you should put a JTree in a JScrollPane if you want it to scroll.

Tree Events

A tree fires off several flavors of events. You can find out when nodes have been expanded and collapsed, when nodes are about to be expanded or collapsed (because the user has clicked on them), and when selections occur. Three distinct event listener interfaces handle this information.

    TreeExpansionListener
    TreeWillExpandListener
    TreeSelectionListener

Tree selections are a tricky business. You can select any combination of nodes by using the Control key and clicking on nodes. Tree selections are described by a TreePath, which describes how to get from the root node to the selected nodes.

The following example registers an event listener that prints out the last selected node:

    tree.addTreeSelectionListener(new TreeSelectionListener() {
      public void valueChanged(TreeSelectionEvent e) {
        TreePath tp = e.getNewLeadSelectionPath();
        System.out.println(tp.getLastPathComponent());
      }
    });

A Complete Example

This section contains an example that showcases the following tree techniques:

  • Construction of a tree model, using DefaultMutableTreeNode

  • Creation and display of a JTree

  • Listening for tree selection events

  • Modifying the tree’s data model while the JTree is showing

Here’s the source code for the example:

    //file: PartsTree.java
    import java.awt.*;
    import java.awt.event.*;
    import javax.swing.*;
    import javax.swing.event.*;
    import javax.swing.tree.*;

    public class PartsTree {
      public static void main(String[] args) {
        // create a hierarchy of nodes
        MutableTreeNode root = new DefaultMutableTreeNode("Parts");
        MutableTreeNode beams = new DefaultMutableTreeNode("Beams");
        MutableTreeNode gears = new DefaultMutableTreeNode("Gears");
        root.insert(beams, 0);
        root.insert(gears, 1);
        beams.insert(new DefaultMutableTreeNode("1x4 black"), 0);
        beams.insert(new DefaultMutableTreeNode("1x6 black"), 1);
        beams.insert(new DefaultMutableTreeNode("1x8 black"), 2);
        beams.insert(new DefaultMutableTreeNode("1x12 black"), 3);
        gears.insert(new DefaultMutableTreeNode("8t"), 0);
        gears.insert(new DefaultMutableTreeNode("24t"), 1);
        gears.insert(new DefaultMutableTreeNode("40t"), 2);
        gears.insert(new DefaultMutableTreeNode("worm"), 3);
        gears.insert(new DefaultMutableTreeNode("crown"), 4);

        // create the JTree
        final DefaultTreeModel model = new DefaultTreeModel(root);
        final JTree tree = new JTree(model);

        // create a text field and button to modify the data model
        final JTextField nameField = new JTextField("16t");
        final JButton button = new JButton("Add a part");
        button.setEnabled(false);
        button.addActionListener(new ActionListener() {
          public void actionPerformed(ActionEvent e) {
            TreePath tp = tree.getSelectionPath();
            MutableTreeNode insertNode =
                (MutableTreeNode)tp.getLastPathComponent();
            int insertIndex = 0;
            if (insertNode.getParent() != null) {
              MutableTreeNode parent =
                  (MutableTreeNode)insertNode.getParent();
              insertIndex = parent.getIndex(insertNode) + 1;
              insertNode = parent;
            }
            MutableTreeNode node =
                new DefaultMutableTreeNode(nameField.getText());
            model.insertNodeInto(node, insertNode, insertIndex);
          }
        });
        JPanel addPanel = new JPanel(new GridLayout(2, 1));
        addPanel.add(nameField);
        addPanel.add(button);

        // listen for selections
        tree.addTreeSelectionListener(new TreeSelectionListener() {
          public void valueChanged(TreeSelectionEvent e) {
            TreePath tp = e.getNewLeadSelectionPath();
            button.setEnabled(tp != null);
          }
        });

        // create a JFrame to hold the tree
        JFrame frame = new JFrame("PartsTree v1.0");

        frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
        frame.setSize(200, 200);
        frame.add(new JScrollPane(tree));
        frame.add(addPanel, BorderLayout.SOUTH);
        frame.setVisible(true);
      }
    }

The example begins by creating a node hierarchy. The root node is called Parts. It contains two subnodes named Beams and Gears, as shown:

    MutableTreeNode root = new DefaultMutableTreeNode("Parts");
    MutableTreeNode beams = new DefaultMutableTreeNode("Beams");
    MutableTreeNode gears = new DefaultMutableTreeNode("Gears");
    root.insert(beams, 0);
    root.insert(gears, 1);

The Beams and Gears nodes contain a handful of items each.

The “Add a part” button inserts a new item into the tree at the level of the current node, and just after it. You can specify the name of the new node by typing it in the text field above the button. To determine where the node should be added, the current selection is first obtained in the anonymous inner class ActionListener:

    TreePath tp = tree.getSelectionPath();
    MutableTreeNode insertNode =
      (MutableTreeNode)tp.getLastPathComponent();

The new node should be added to the parent node of the current node, so it ends up being a sibling of the current node. The only hitch here is that if the current node is the root node, it won’t have a parent. If a parent does exist, we determine the index of the currently selected node, and then add the new node at the next index:

    int insertIndex = 0;
    if (insertNode.getParent() != null) {
      MutableTreeNode parent =
          (MutableTreeNode)insertNode.getParent();
      insertIndex = parent.getIndex(insertNode) + 1;
      insertNode = parent;
    }
    MutableTreeNode node =
        new DefaultMutableTreeNode(nameField.getText());
    model.insertNodeInto(node, insertNode, insertIndex);

You must add the new node to the tree’s data model using insertNodeInto()—not to the MutableTableNode itself. The model notifies the JTree that it needs to update itself.

We have another event handler in this example, one that listens for tree selection events. Basically, we want to enable our “Add a part” button only if a current selection exists:

    tree.addTreeSelectionListener(new TreeSelectionListener() {
      public void valueChanged(TreeSelectionEvent e) {
        TreePath tp = e.getNewLeadSelectionPath();
        button.setEnabled(tp != null);
      }
    });

When you first start this application, the button is disabled. As soon as you select something, it is enabled, and you can add nodes to the tree with abandon. If you want to see the button disabled again, you can unselect everything by holding the Control key and clicking on the current selection.

Get Learning Java, 4th Edition now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.