Recent articles:
Popular archives:
Java: A platform for platforms
Sun's reorg may seem promising to shareholders but it's also a scramble for position. The question now is whether Sun can,
or wants to, maintain its hold on Java technology. Especially with enterprise leaders like SpringSource and RedHat investing
heavily in Java's future as a platform for platforms
Also see:
Discuss: Tim Bray on 'What Sun Should Do'
We'll start with the interface code we developed last time. To this we'll add listeners to enable reading, replying, and posting functionality. We're going to reuse a networking class from a previous installment to implement the networking to the Forum server, which we'll also reuse from a previous installment.
In response to a reader suggestion, we're going to add a JSplitPane between the tree on the left and the tabbed display area on the right.
Before diving into the new code, we should take a closer look at the interface component that will be the focus of most of
our effort: the JTree.
The JTree is the central widget in the Swing Forum. Users interact with the tree to select threads and the articles they contain. The
tree reflects the server content (or a portion of it) at the point in time that the tree refreshes its contents from the server.
The tree appearing in the interface represents an interaction of several types of objects:
JTree handles the view of the tree dataDefaultTreeModel handles the data the view displaysDefaultMutableTreeNode encapsulates a node in the tree modelForumArticle encapsulates article contents

The JTree displays data from the DefaultTreeModel, which is composed of DefaultMutableTreeNodes.
The JTree itself handles the tasks of displaying the tree structure and handling user events. The contents of the tree are stored in
a tree data model that implements the TreeModel interface; in this case, we use the supplied DefaultTreeModel. The tree model is made up of tree nodes, each of which is an instance of DefaultMutableTreeNode.
Each DefaultMutableTreeNode instance contains a "user object" that represents user data at that node in the tree. In our case, the user object is either
a String (the name of a thread) or an instance of ForumArticle (which encapsulates an article).
To allow the user to manipulate the threads and articles on the server, we need to define user actions on the tree for refreshing and navigating.
We'll specify that double mouse clicks make refresh calls for threads and the root node (the Forum server node), and we'll enable navigation of the tree with single mouse clicks and arrow keys.
Because the server is the reference copy of articles and threads, we need to find some way to access the server and display its contents in the tree. Our old friend networking comes to the rescue here.
We'll enable networking with a sockets-based communications object that implements the 1.0 Forum API. The 1.0 Forum API has
three methods, which are called by the Swing Forum event handlers based on user interaction with the GUI. The signatures for
these methods are defined in the SwingForumClientCom interface.
Hashtable loadAllThreads ()Vector loadThreadArticles (String thread)boolean postArticle (String art, String thread)Of course, any communications object that implements this interface will work in place of the sockets-based object we're using; we designed the system way back when for just this sort of flexibility.
For the purposes of the Swing Forum, we've modified an old ForumClientComSocketImpl to take a URL in its constructor. We won't detail the code for this or the server portions of the article -- they're basically
taken whole-cloth from past installments (see Resources for a listing of past Step by Step columns). The main difference is the constructor:
public SwingForumClientComSocketImpl (URL u) {
serverURL = u;
}
The SwingForumClientComSocketImpl implements the SwingForumClientCom interface, which enforces implementation of the Forum 1.0 API.
The Swing Forum uses the 1.1 event model to define event handling for the trees as well as the other elements in the user interface.
Recall that the 1.1 event model is based on adding event listeners to objects that produce the events. For example, a JButton produces an ActionEvent when it's clicked. The following statement adds an ActionListener to a JButton called jb:
// jb is an instance of JButton jb.addActionListener (jbListener);
jb's event code calls the actionPerformed(...) method of jbListener every time the button is clicked.
But how do we get this instance of jbListener? We could go the standard route -- define a new class that implements the ActionListener interface and then pass a new instance of this in the add call above. This works, but the standard approach doesn't take
advantage of a handy 1.1 shortcut -- the anonymous class. This technique lets us do it all at once, plus define an inner class
that has access to class variables and methods.
jb.addActionListener (new ActionListener () {
public void actionPerformed (ActionEvent e) {
// do something here - enclosing scope enabled
}
});
We'll make use of this technique extensively in the Swing Forum.
Now, time for the code.
The following sections of code have been added to the SwingForum code we developed last time.
import java.awt.*; import java.awt.event.*; import java.util.*; import java.net.*; import com.sun.java.swing.*; import com.sun.java.swing.text.*; import com.sun.java.swing.tree.*; import com.sun.java.swing.event.*;
public class SwingForum extends JFrame {
static final String QUOTE_CHARS = "> ";
This section is almost identical to the original SwingForum. Along with several more import statements, the QUOTE_CHARS definition is added to specify the characters that are prepended to each line of existing text in a reply.
replyAction = new AbstractAction ("Reply") {
public void actionPerformed (ActionEvent e) {
quoteReply ();
display.setSelectedComponent (replyTab);
}
};
refreshAction = new AbstractAction ("Refresh Server") {
public void actionPerformed (ActionEvent e) {
loadAllThreads ();
}
};
These are the only two action definitions that are affected by the addition of the new functionality. When the reply action
occurs, the replyArea is reset by the quoteReply() method to contain reply text quoted with the QUOTE_CHARS.
When the refresh action occurs, the client rereads the threads from the server by calling the loadAllThreads() method.
JTree tree; JScrollPane treeScroller; DefaultTreeModel treeModel;
void layOutTree () { tree = new JTree (); tree.getSelectionModel ().setSelectionMode ( TreeSelectionModel.SINGLE_TREE_SELECTION); treeScroller = new JScrollPane (tree); loadAllThreads ();
// load threads for root || arts for thread tree.addMouseListener (new MouseAdapter () { public synchronized void mouseClicked (MouseEvent e) { TreePath path = tree.getPathForLocation (e.getX (), e.getY ()); if (path != null) { DefaultMutableTreeNode node; node = (DefaultMutableTreeNode) path.getLastPathComponent (); if (e.getClickCount () == 2) { if (node.isRoot ()) { loadAllThreads (); tree.setSelectionPath (new TreePath (treeModel.getRoot ())); } else if (((DefaultMutableTreeNode)node.getParent()).isRoot()) { loadThreadArticles (node); tree.setSelectionPath (path); tree.expandPath (path); } } } } });
The layOutTree method is the first of two 1.1 event adapters that we define to handle populating the tree with threads and articles. This
adapter listens to mouse clicks and gets a TreePath using the x and y position of the click. If the path isn't null, the logic gets the last component of the path, which represents the node that has just been clicked.
If the user double-clicks the mouse, the method will load either the threads from the server or the articles in a thread, depending on whether or not the double-click was on the root level or the second level of the tree.
After layOutTree loads the threads or articles, it selects the item that was double-clicked and expands the path to it so that it's visible
in the display.
// display art if selected. ignore root and thread selection
tree.addTreeSelectionListener (new TreeSelectionListener () {
public synchronized void valueChanged (TreeSelectionEvent e) {
TreePath path = e.getPath ();
DefaultMutableTreeNode node;
node = (DefaultMutableTreeNode) path.getLastPathComponent ();
if ((path.getPath ()).length > 2 ) {
ForumArticle art = (ForumArticle) node.getUserObject ();
readArea.setText (art.getText ());
display.setSelectedComponent (readTab);
} else {
readArea.setText ("");
}
}
});
This listener listens to the tree for selection events. Selection events occur whenever a user keyboards to or clicks on a node, or when a node is selected programmatically.
We're interested only in hearing selections that occur to nodes representing articles. Remember, we decided that double-clicks on the server and threads would be required to cause refreshes; otherwise, we would have put the refresh-from-server code in here instead of in a mouse listener.
Note that we could have used the mouse listener (rather than the one above) to drive selections by listening to single mouse clicks. The problem with this solution is that it wouldn't capture keyboard-driven selections.
tree.addKeyListener (new KeyAdapter () {
public synchronized void keyTyped (KeyEvent e) {
if (e.getKeyChar () == '\u0012') {
TreePath path = tree.getSelectionPath ();
if (path != null) {
DefaultMutableTreeNode node;
node = (DefaultMutableTreeNode) path.getLastPathComponent ();
if (node.isRoot ()) {
loadAllThreads ();
} else if (((DefaultMutableTreeNode) node.getParent ()).isRoot ()) {
loadThreadArticles (node);
tree.setSelectionPath (path);
tree.expandPath (path);
}
}
}
}
});
The only problem with our interface so far is that we still rely on double-clicks to refresh threads, so it's impossible to use the interface with a keyboard exclusively.
JTree, see Tomer Meshorer's "Swing's JTree component model" (JavaWorld, October 1998), which explores how to capture hierarchical structures using this new component model http://www.javaworld.com/jw-10-1998/jw-10-jtree.html