Chapter 18. More Swing Components

In the previous chapter, we described most of the components that Swing offers for building user interfaces. In this chapter, you’ll find out about the rest. These include Swing’s text components, trees, and tables. These types of components have considerable depth but are quite easy to use if you accept their default options. We’ll show you the easy way to use these components and start to describe the more advanced features of each. Later in this chapter, we’ll also give an example of how to implement your own, custom components in Swing.

Text Components

Swing offers sophisticated text components, from plain-text entry boxes to HTML renderers. For full coverage of Swing’s text capabilities, see O’Reilly’s Java Swing. In that encyclopedic book, several meaty chapters are devoted to text. It’s a huge subject; we’ll just scratch the surface here.

Let’s begin by examining the simpler text components. JTextField is a single-line text editor and JTextArea is a simple, multiline text editor. Both JTextField and JTextArea derive from the JTextComponent class, which provides the functionality they have in common. This includes methods for setting and retrieving the displayed text, specifying whether the text is “editable” or read-only, manipulating the cursor position within the text, and manipulating text selections.

Observing changes in text components requires an understanding of how the components implement the Model-View-Controller (MVC) architecture. You may recall from the last chapter that Swing components implement a true MVC architecture. It’s in the text components that you first get an inkling of a clear separation between the M and VC parts of the MVC architecture. The model for text components is an object called a Document. When you add or remove text from a JTextField or a JTextArea, the corresponding Document is changed. It’s the document itself, not the visual components, that generates text-related events when something changes. To receive notification of JTextArea changes, therefore, you register with the underlying Document, not with the JTextArea component itself:

    JTextArea textArea = new JTextArea();
    Document doc = textArea.getDocument();
    doc.addDocumentListener(someListener);

As you’ll see in an upcoming example, you can easily have more than one visual text component use the same underlying Document data model.

In addition, JTextField components generate ActionEvents whenever the user presses the Return key within the field. To get these events, just implement the ActionListener interface and register your listener using the addActionListener() method.

The next sections contain a couple of simple applications that show you how to work with text areas and fields.

The TextEntryBox Application

Our first example, TextEntryBox, creates a JTextArea and ties it to a JTextField, as you can see in Figure 18-1.

The TextEntryBox application
Figure 18-1. The TextEntryBox application

When the user hits Return in the JTextField, we receive an ActionEvent and add the line to the JTextArea’s display. Try it out. You may have to click your mouse in the JTextField to give it focus before typing in it. If you fill up the display with lines, you can test-drive the scroll bar:

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

    public class TextEntryBox {

      public static void main(String[] args) {
        JFrame frame = new JFrame("Text Entry Box");

        final JTextArea area = new JTextArea();
        area.setFont(new Font("Serif", Font.BOLD, 18));
        area.setText("Howdy!\n");
        final JTextField field = new JTextField();

        frame.add(new JScrollPane(area), BorderLayout.CENTER);
        frame.add(field, BorderLayout.SOUTH);
        field.requestFocus();

        field.addActionListener(new ActionListener() {
          public void actionPerformed(ActionEvent ae) {
            area.append(field.getText() + '\n');
            field.setText("");
          }
        });

        frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
        frame.setSize(200, 300);
        frame.setVisible(true);
      }
    }

TextEntryBox is exceedingly simple; we’ve done a few things to make it more interesting. We give the text area a bigger font using Component’s setFont() method; fonts are discussed in Chapter 20. Finally, we want to be notified whenever the user presses Return in the text field, so we register an anonymous inner class as a listener for action events.

Pressing Return in the JTextField generates an action event, and that’s where the fun begins. We handle the event in the actionPerformed() method of our inner ActionListener implementation. Then, we use the getText() and setText() methods to manipulate the text that the user has typed. These methods can be used for JTextField and JTextArea, as these components are both derived from the JTextComponent class and, therefore, have some common functionality.

The event handler, actionPerformed(), calls field.getText() to read the text that the user typed into our JTextField. It then adds this text to the JTextArea by calling area.append(). Finally, we clear the text field by calling the method field.setText(""), preparing it for more input.

Remember, the text components really are distinct from the text data model, the Document. When you call setText(), getText(), or append(), these methods are shorthand for operations on an underlying Document.

By default, JTextField and JTextArea are editable; you can type and edit in both text components. They can be changed to output-only areas by calling setEditable(false). Both text components also support selections. A selection is a range of text that is highlighted for copying, cutting, or pasting in your windowing system. You select text by dragging the mouse over it; you can then cut, copy, and paste it into other text windows using the default keyboard gestures. On most systems, these are Ctrl-C for copy, Ctrl-V for paste, and Ctrl-X for cut (on the Mac it’s Command-C, Command-V, and Command-X). You can also programmatically manage these operations using the JTextComponent’s cut() , copy(), and paste() methods. You could, for example, create a pop-up menu with the standard cut, copy, and paste options using these methods. The current text selection is returned by getSelectedText(), and you can set the selection using selectText(), which takes an index range or selectAll().

Notice how JTextArea fits neatly inside a JScrollPane. The scroll pane gives us the expected scrollbars and scrolling behavior if the text in the JTextArea becomes too large for the available space.

Formatted Text

The JFormattedTextField component provides explicit support for editing complex formatted values such as numbers and dates. JFormattedTextField acts somewhat like a JTextField, except that it accepts a format-specifying object in its constructor and manages a complex object type (such as Date or Integer) through its setValue() and getValue() methods. The following example shows the construction of a simple form with different types of formatted fields:

    import java.text.*;
    import javax.swing.*;
    import javax.swing.text.*;
    import java.util.Date;

    public class
               
    FormattedFields
    {
        public static void main( String[] args ) throws Exception {
            Box form = Box.createVerticalBox();
            form.add( new JLabel("Name:") );
            form.add( new JTextField("Joe User") );

            form.add( new JLabel("Birthday:") );
            JFormattedTextField birthdayField =
                new JFormattedTextField(new SimpleDateFormat("MM/dd/yy"));
            birthdayField.setValue( new Date() );
            form.add( birthdayField );

            form.add( new JLabel("Age:") );
            form.add(new JFormattedTextField(new Integer(32)));

            form.add( new JLabel("Hairs on Body:") );
            JFormattedTextField hairsField
                = new JFormattedTextField( new DecimalFormat("###,###") );
            hairsField.setValue(new Integer(100000));
            form.add( hairsField );

            form.add( new JLabel("Phone Number:") );
            JFormattedTextField phoneField =
                new JFormattedTextField( new MaskFormatter("(###)###-####") );
            phoneField.setValue("(314)555-1212");
            form.add( phoneField );

            JFrame frame = new JFrame("User Information");
            frame.getContentPane().add(form);
            frame.pack();
            frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
            frame.setVisible(true);
        }
    }

JFormattedTextField can be constructed in a variety of ways. You can use a plain instance of java.lang.Number (e.g., Integer and Float) as a prototype or set the layout explicitly using a formatting object from the java.text package: java.text.NumberFormat, java.text.DateFormat, or the more arbitrary java.text.MaskFormatter. The NumberFormat and DateFormat classes of the java.text package are discussed in Chapters 10 and 11. MaskFormatter allows you to construct arbitrary physical layout conventions. In a moment, we’ll discuss input filtering and component validation, which also allow you to restrict the kinds of characters that could fill the fields or perform arbitrary checks on the data. Finally, we should mention that in this example, we’ve used a Box container. A Box is just a Swing container that uses a BoxLayout, which we’ll discuss more in Chapter 19.

After construction, you can set a valid value using setValue() and retrieve the last valid value with getValue(). To do this, you’ll have to cast the value back to the correct type based on the format you are using. For example, this statement retrieves the date from our birthday field:

    Date bday = (Date)birthdayField.getValue();

JFormattedTextField validates its text when the user attempts to shift focus to a new field (either by clicking with the mouse outside of the field or using keyboard navigation). By default, JFormattedTextField handles invalid input by simply reverting to the last valid value. If you wish to allow invalid input to remain in the field for further editing, you can set the setFocusLostBehavior() method with the value JFormattedTextField.COMMIT (the default is COMMIT_OR_REVERT). In any case, invalid input does not change the value property retrieved by getValue().

Filtering Input

JFormattedTextField does not know about all format types itself; instead, it uses AbstractFormatter objects that know about particular format types. The AbstractFormatters, in turn, provide implementations of two interfaces: DocumentFilter and NavigationFilter. A DocumentFilter attaches to implementations of Document and allows you to intercept editing commands, modifying them as you wish. A NavigationFilter can be attached to JTextComponents to control the movement of the cursor (as in a mask-formatted field). You can implement your own AbstractFormatters for use with JFormattedTextField, and, more generally, you can use the DocumentFilter interface to control how documents are edited in any type of text component. For example, you could create a DocumentFilter that maps characters to uppercase or strange symbols. DocumentFilter provides a low-level, edit-by-edit means of controlling or mapping user input. We will show an example of this now. In the following section, we discuss how to implement higher-level field validation that ensures the correctness of data after it is entered, in the same way that the formatted text field did for us earlier.

DocumentFilter

The following example, DocFilter, applies a document filter to a JTextField. Our DocumentFilter simply maps any input to uppercase. Here is the code:

    import java.text.*;
    import javax.swing.*;
    import javax.swing.text.*;

    public class DocFilter
    {
       public static void main( String[] args ) throws Exception
       {
          JTextField field = new JTextField(30);

          ((AbstractDocument)(field.getDocument())).setDocumentFilter(
             new DocumentFilter()
          {
             public void insertString(
                FilterBypass fb, int offset, String string, AttributeSet attr)
                   throws BadLocationException
                {
                   fb.insertString( offset, string.toUpperCase(), attr );
                }

             public void replace(
                FilterBypass fb, int offset, int length, String string,
                AttributeSet attr) throws BadLocationException
                {
                   fb.replace( offset, length, string.toUpperCase(), attr );
                }
          } );

          JFrame frame = new JFrame("User Information");
          frame.add( field );
          frame.pack();
          frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
          frame.setVisible(true);
       }
    }

The methods insertString() and replace() of the DocumentFilter are called when text is added to the document or modified. Within them, we have an opportunity to filter the text before passing it on. When we are ready to apply the text, we use the FilterBypass reference to pass it along. FilterBypass has the same set of methods, which apply the changes directly to the document. The DocumentFilter remove() method can also be used to intercept edits to the document that remove characters. One thing to note in our example is that not all Documents have a setDocumentFilter() method. Instead, we have to cast our document to an AbstractDocument. Only document implementations that extend AbstractDocument accept filters (unless you implement your own). This sad state of affairs is because the Document Filter API was added in Java 1.4, and it was decided that changes could not be made to the original Document interface.

Validating Data

Low-level input filtering prevents you from doing such things as entering a number where a character should be. In this section, we’re going to talk about high-level validation, which accounts for things like February having only 28 days or a credit card number being for a Visa or MasterCard. Whereas character filtering prevents you from entering incorrect data, field validation happens after data has been entered. Normally, validation occurs when the user tries to change focus and leave the field, either by clicking the mouse or through keyboard navigation. Java 1.4 added the InputVerifier API, which allows you to validate the contents of a component before focus is transferred. Although we are going to talk about this in the context of text fields, an InputVerifier can actually be attached to any JComponent to validate its state in this way.

The following example creates a pair of text fields. The first allows any value to be entered, while the second accepts only numbers between 0 and 100. When both fields are happy, you can freely move between them. However, when you enter an invalid value in the second field and try to leave, the program just beeps and selects the text. The focus remains trapped until you correct the problem.

    import javax.swing.*;

    public class Validator
    {
        public static void main( String[] args ) throws Exception {
            Box form = Box.createVerticalBox();
            form.add( new JLabel("Any Value") );
            form.add( new JTextField("5000") );

            form.add( new JLabel("Only 0-100") );
            JTextField rangeField = new JTextField("50");
            rangeField.setInputVerifier( new InputVerifier() {
                public boolean verify( JComponent comp ) {
                    JTextField field = (JTextField)comp;
                    boolean passed = false;
                    try {
                        int n = Integer.parseInt(field.getText());
                        passed = ( 0 <= n && n <= 100 );
                    } catch (NumberFormatException e) { }
                    if ( !passed ) {
                        comp.getToolkit().beep();
                        field.selectAll();
                    }
                    return passed;
                }
            } );
            form.add( rangeField );

            JFrame frame = new JFrame("User Information");
            frame.add(form);
            frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
            frame.pack();
            frame.setVisible(true);
        }
    }

We’ve created an anonymous inner class extending InputVerifier with this code. The API is very simple; at validation time, our verify() method is called, and we are passed a reference to the component needing checking. Here we cast to the correct type (we know what we are verifying, of course) and parse the number. If it is out of range, we beep and select the text. We then return true or false indicating whether the value passes validation.

You can use an InputVerifier in combination with a JFormattedTextField to both guide user input into the correct format and validate the semantics of what the user entered.

Say the Magic Word

Before we move on from our discussion of formatted text, we should mention that Swing includes a class just for typing passwords, called JPasswordField. A JPasswordField behaves just like a JTextField (it’s a subclass), except every character typed is echoed as the same, obfuscating character, typically an asterisk. Figure 18-2 shows the option dialog example that was presented in Chapter 17. The example includes a JTextField and a JPasswordField.

The creation and use of JPasswordField is basically the same as for JTextField. If you find asterisks distasteful, you can tell the JPasswordField to use a different character using the setEchoChar() method.

Normally, you would use getText() to retrieve the text typed into the JPasswordField. This method, however, is deprecated; you should use getPassword() instead. The getPassword() method returns a character array rather than a String object. This is done because character arrays are a little less vulnerable than Strings to discover by memory-snooping password sniffer programs and they can be erased directly and easily. If you’re not that concerned, you can simply create a new String from the character array. Note that methods in the Java cryptographic classes accept passwords as character arrays, not strings, so you can pass the results of a getPassword() call directly to methods in the cryptographic classes without ever creating a String.

Using a JPasswordField in a dialog
Figure 18-2. Using a JPasswordField in a dialog

Sharing a Data Model

Our next example shows how easy it is to make two or more text components share the same Document; Figure 18-3 shows what the application looks like.

Three views of the same data model
Figure 18-3. Three views of the same data model

Anything the user types into any text area is reflected in all of them. All we had to do is make all the text areas use the same data model, like this:

    JTextArea areaFiftyOne = new JTextArea();
    JTextArea areaFiftyTwo = new JTextArea();
    areaFiftyTwo.setDocument(areaFiftyOne.getDocument());
    JTextArea areaFiftyThree = new JTextArea();
    areaFiftyThree.setDocument(areaFiftyOne.getDocument());

We could just as easily make seven text areas sharing the same document—or seventy. While this example may not look very useful, keep in mind that you can scroll different text areas to different places in the same document. That’s one of the beauties of putting multiple views on the same data; you get to examine different parts of it. Another useful technique is viewing the same data in different ways. You could, for example, view some tabular numerical data as both a spreadsheet and a pie chart. The MVC architecture that Swing uses means that it’s possible to do this in an intelligent way so that if numbers in a spreadsheet are updated, a pie chart that uses the same data is automatically updated, too.

This example works because, behind the scenes, there are a lot of events flying around. When you type in one of the text areas, the text area receives the keyboard events. It calls methods in the document to update its data. In turn, the document sends events to the other text areas telling them about the updates so that they can correctly display the document’s new data. But don’t worry about any of this; you just tell the text areas to use the same data, and Swing takes care of the rest:

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

    public class SharedModel {
        public static void main(String[] args) {
        JFrame frame = new JFrame("Shared Model");

        JTextArea areaFiftyOne = new JTextArea();
        JTextArea areaFiftyTwo = new JTextArea();
        areaFiftyTwo.setDocument(areaFiftyOne.getDocument());
        JTextArea areaFiftyThree = new JTextArea();
        areaFiftyThree.setDocument(areaFiftyOne.getDocument());

        frame.setLayout(new GridLayout(3, 1));
        frame.add(new JScrollPane(areaFiftyOne));
        frame.add(new JScrollPane(areaFiftyTwo));
        frame.add(new JScrollPane(areaFiftyThree));

        frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
        frame.setSize(300, 300);
        frame.setVisible(true);
      }
    }

Setting up the display is simple. We use a GridLayout (discussed in the next chapter) and add three text areas to the layout. Then, all we have to do is tell the text areas to use the same Document.

HTML and RTF for Free

Most user interfaces will use only two subclasses of JTextComponent. These are the simple JTextField and JTextArea classes that we just covered. That’s just the tip of the iceberg, however. Swing offers sophisticated text capabilities through two other subclasses of JTextComponent: JEditorPane and JTextPane.

The first of these, JEditorPane, can display HTML and Rich Text Format (RTF) documents out of the box and provides a plug-in framework for support of other content types. It fires one more type of event, a HyperlinkEvent. Subtypes of this event are fired off when the mouse enters, exits, or clicks on a hyperlink. Combined with JEditorPane’s HTML display capabilities, it’s easy to build a simple browser. The following browser, as shown in Figure 18-4, has only about 70 lines of code.

    //file: CanisMinor.java
    import java.awt.*;
    import java.awt.event.*;
    import java.net.*;
    import javax.swing.*;
    import javax.swing.event.*;

    public class CanisMinor extends JFrame {
      protected JEditorPane mEditorPane;
      protected JTextField mURLField;

      public CanisMinor(String urlString) {
        super("CanisMinor v1.0");
        createGUI(urlString);
      }

      protected void createGUI( String urlString ) {
        setLayout(new BorderLayout());

        JToolBar urlToolBar = new JToolBar();
        mURLField = new JTextField(urlString, 40);
        urlToolBar.add(new JLabel("Location "));
        urlToolBar.add(mURLField);
        add(urlToolBar, BorderLayout.NORTH);

        mEditorPane = new JEditorPane();
        mEditorPane.setEditable(false);
        add(new JScrollPane(mEditorPane), BorderLayout.CENTER);

        openURL(urlString);

        mURLField.addActionListener(new ActionListener() {
          public void actionPerformed(ActionEvent ae) {
            openURL(ae.getActionCommand());
          }
        });

        mEditorPane.addHyperlinkListener(new LinkActivator());

        setSize(500, 600);
        setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
      }

      protected void openURL(String urlString) {
        try {
          URL url = new URL(urlString);
          mEditorPane.setPage(url);
          mURLField.setText(url.toExternalForm());
        }
        catch (Exception e) {
          System.out.println("Couldn't open " + urlString + ":" + e);
        }
      }

      class LinkActivator implements HyperlinkListener {
        public void hyperlinkUpdate(HyperlinkEvent he) {
          HyperlinkEvent.EventType type = he.getEventType();
          if (type == HyperlinkEvent.EventType.ACTIVATED)
            openURL(he.getURL().toExternalForm());
        }
      }

      public static void main(String[] args) {
        String urlString = "http://en.wikinews.org/wiki/Special:Random";
        if (args.length > 0)
           urlString = args[0];
        new CanisMinor( urlString ).setVisible( true );
      }
    }
The CanisMinor application, a simple web browser
Figure 18-4. The CanisMinor application, a simple web browser

JEditorPane is the center of this little application. Passing a URL to setPage() causes the JEditorPane to load a new page, either from a local file or from somewhere across the Internet. To go to a new page, enter it in the text field at the top of the window and press Return. This fires an ActionEvent that sets the new page location of the JEditorPane. It can display RTF files, too (RTF is the text or nonbinary storage format for Microsoft Word documents).

Responding to hyperlinks correctly is simply a matter of responding to the HyperlinkEvents thrown by the JEditorPane. This behavior is encapsulated in the LinkActivator inner class. In this case, the only activity we are interested in is when the user “activates” the hyperlink by clicking on it. We respond by setting the location of the JEditorPane to the location given under the hyperlink. Surf away!

Behind the scenes, something called an EditorKit handles displaying documents for the JEditorPane. Different kinds of EditorKits can display different kinds of documents. For HTML, the HTMLEditorKit class (in the javax.swing.text.html package) handles the display. Currently, this class supports HTML 3.2. Sun says that future enhancements will move the HTMLEditorKit toward the HTML 4.0 standard, but even with Java 7 this area hasn’t seen much progress. The HTMLEditorKit handles other features of HTML, including HTML forms, in the expected way—automatically submitting results when a submit button is pushed. A FormSubmitEvent enables programmatic involvement in form submission.

If you browse around with this example browser, you will quickly find that most modern web pages can’t be rendered well by the current HTMLEditorKit. In their current state, JEditorPane and HTMLEditorKit are best suited for simple uses such as an HTML help system. There is an excellent commercial Java browser component from JadeLiquid called WebRenderer.

There’s another component here that we haven’t covered before—the JToolBar. This nifty container houses our URL text field. Initially, the JToolBar starts out at the top of the window. But you can pick it up by clicking on the little dotted box near its left edge, then drag it around to different parts of the window. You can place this toolbar at the top, left, right, or bottom of the window, or you can drag it outside the window entirely, where it will inhabit a window of its own. This behavior comes for free from the JToolBar class. We only had to create a JToolBar and add some components to it. The JToolBar is just a container, so we add it to the content pane of our window to give it an initial location.

Managing Text Yourself

Swing offers one last subclass of JTextComponent that can do just about anything you want: JTextPane. The basic text components, JTextField and JTextArea, are limited to a single font in a single style. But JTextPane, a subclass of JEditorPane, can display multiple fonts and multiple styles in the same component. It also includes support for highlighting, image embedding, and other advanced features.

We’ll take a peek at JTextPane by creating a text pane with some styled text. Remember, the text itself is stored in an underlying data model, the Document. To create styled text, we simply associate a set of text attributes with different parts of the document’s text. Swing includes classes and methods for manipulating sets of attributes, like specifying a bold font or a different color for the text. Attributes themselves are contained in a class called SimpleAttributeSet; these attribute sets are manipulated with static methods in the StyleConstants class. For example, to create a set of attributes that specifies the color red, you could do this:

    SimpleAttributeSet redstyle = new SimpleAttributeSet();
    StyleConstants.setForeground(redstyle, Color.red);

To add some red text to a document, you would just pass the text and the attributes to the document’s insertString() method, like this:

    document.insertString(6, "Some red text", redstyle);

The first argument to insertString() is an offset into the text. An exception is thrown if you pass in an offset that’s greater than the current length of the document. If you pass null for the attribute set, the text is added in the JTextPane’s default font and style.

Our simple example creates several attribute sets and uses them to add plain and styled text to a JTextPane, as shown in Figure 18-5:

    //file: Styling.java
    import java.awt.*;
    import java.awt.event.*;
    import javax.swing.*;
    import javax.swing.text.*;

    public class Styling extends JFrame {
      private JTextPane textPane;

      public Styling() {
        super("Stylin' v1.0");
        setSize(300, 200);

        textPane = new JTextPane();
        textPane.setFont(new Font("Serif", Font.PLAIN, 24));

        // create some handy attribute sets
        SimpleAttributeSet red = new SimpleAttributeSet();
        StyleConstants.setForeground(red, Color.red);
        StyleConstants.setBold(red, true);
        SimpleAttributeSet blue = new SimpleAttributeSet();
        StyleConstants.setForeground(blue, Color.blue);
        SimpleAttributeSet italic = new SimpleAttributeSet();
        StyleConstants.setItalic(italic, true);
        StyleConstants.setForeground(italic, Color.orange);

        // add the text
        append("In a ", null);
        append("sky", blue);
        append(" full of people\nOnly some want to ", null);
        append("fly", italic);
        append("\nIsn't that ", null);
        append("crazy", red);
        append("?", null);

        add(new JScrollPane(textPane), BorderLayout.CENTER);
        setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
      }

      protected void append(String s, AttributeSet attributes) {
        Document d = textPane.getDocument();
        try { d.insertString(d.getLength(), s, attributes); }
        catch (BadLocationException ble) {}
      }

      public static void main(String[] args) {
        new Styling().setVisible(true);
      }
    }
Using styled text in a JTextPane
Figure 18-5. Using styled text in a JTextPane

This example creates a JTextPane, which is saved in a member variable. Three different attribute sets are created using combinations of text styles and foreground colors. Then, using a helper method called append(), text is added to the JTextPane.

The append() method tacks a text String on the end of the JTextPane’s document, using the supplied attributes. Remember that if the attributes are null, the text is displayed with the JTextPane’s default font and style.

You can go ahead and add your own text if you wish. If you place the caret inside one of the differently styled words and type, the new text comes out in the appropriate style. Pretty cool, eh? You’ll also notice that JTextPane gives us word-wrapping behavior for free. And because we’ve wrapped the JTextPane in a JScrollPane, we get scrolling for free, too. Swing allows you to do some really cool stuff without breaking a sweat. Just wait—there’s plenty more to come.

This simple example should give you some idea of what JTextPane can do. It’s reasonably easy to build a simple word processor with JTextPane, and complex commercial-grade word processors are definitely possible.

If JTextPane still isn’t good enough for you, or you need some finer control over character, word, and paragraph layout, you can actually draw text, carets, and highlight shapes yourself. A class in the 2D API called TextLayout simplifies much of this work, but it’s outside the scope of this book. For coverage of TextLayout and other advanced text drawing topics, see Java 2D Graphics by Jonathan Knudsen (O’Reilly).

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.

Tables

Tables present information in orderly rows and columns. This is useful for presenting financial figures or representing data from a relational database. Like trees, tables in Swing are incredibly powerful and customizable. If you go with the default options, they’re also pretty easy to use.

The JTable class represents a visual table component. A JTable is based on a TableModel, one of a dozen or so supporting interfaces and classes in the javax.swing.table package.

A First Stab: Freeloading

JTable has one constructor that creates a default table model for you from arrays of data. You just need to supply it with the names of your column headers and a 2D array of Objects representing the table’s data. The first index selects the table’s row; the second index selects the column. The following example shows how easy it is to get going with tables using this constructor:

    //file: DullShipTable.java
    import java.awt.*;
    import java.awt.event.*;
    import javax.swing.*;
    import javax.swing.table.*;

    public class DullShipTable {
      public static void main(String[] args) {
        // create some tabular data
        String[] headings =
          new String[] {"Number", "Hot?", "Origin",
                        "Destination", "Ship Date", "Weight" };
        Object[][] data = new Object[][] {
          { "100420", Boolean.FALSE, "Des Moines IA", "Spokane WA",
              "02/06/2000", new Float(450) },
          { "202174", Boolean.TRUE, "Basking Ridge NJ", "Princeton NJ",
              "05/20/2000", new Float(1250) },
          { "450877", Boolean.TRUE, "St. Paul MN", "Austin TX",
              "03/20/2000", new Float(1745) },
          { "101891", Boolean.FALSE, "Boston MA", "Albany NY",
              "04/04/2000", new Float(88) }
        };

        // create the data model and the JTable
        JTable table = new JTable(data, headings);

        JFrame frame = new JFrame("DullShipTable v1.0");
        frame.add(new JScrollPane(table));

        frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
        frame.setSize(500, 200);
        frame.setVisible(true);
      }
    }

This small application produces the display shown in Figure 18-7.

A rudimentary JTable
Figure 18-7. A rudimentary JTable

For very little typing, we’ve gotten some pretty impressive stuff. Here are a few things that come for free:

Column headings

The JTable has automatically formatted the column headings differently than the table cells. It’s clear that they are not part of the table’s data area.

Cell overflow

If a cell’s data is too long to fit in the cell, it is automatically truncated and shown with an ellipsis (...). This is shown in the Origin cell in the second row in Figure 18-7.

Row selection

You can click on any cell in the table to select its entire row. This behavior is controllable; you can select single cells, entire rows, entire columns, or some combination of these. To configure the JTable’s selection behavior, use the setCellSelectionEnabled(), setColumnSelectionAllowed(), and setRowSelectionAllowed() methods.

Cell editing

Double-clicking on a cell opens it for editing; you’ll get a little cursor in the cell. You can type directly into the cell to change the cell’s data.

Column sizing

If you position the mouse cursor between two column headings, you’ll get a little left-right arrow cursor. Click and drag to change the size of the column to the left. Depending on how the JTable is configured, the other columns may also change size. The resizing behavior is controlled with the setAutoResizeMode() method.

Column reordering

If you click and drag on a column heading, you can move the entire column to another part of the table.

Play with this for a while. It’s fun!

Round Two: Creating a Table Model

JTable is a very powerful component. You get a lot of very nice behavior for free. However, the default settings are not quite what we wanted for this simple example. In particular, we intended the table entries to be read-only; they should not be editable. Also, we’d like entries in the Hot? column to be checkboxes instead of words. Finally, it would be nice if the Weight column were formatted appropriately for numbers rather than for text.

To achieve more flexibility with JTable, we’ll write our own data model by implementing the TableModel interface. Fortunately, Swing makes this easy by supplying a class that does most of the work, AbstractTableModel. To create a table model, we’ll just subclass AbstractTableModel and override whatever behavior we want to change.

At a minimum, all AbstractTableModel subclasses have to define the following three methods:

public int getRowCount(), public int getColumnCount()

Returns the number of rows and columns in this data model

public Object getValueAt(int row , int column )

Returns the value for the given cell

When the JTable needs data values, it calls the getValueAt() method in the table model. To get an idea of the total size of the table, JTable calls the getRowCount() and getColumnCount() methods in the table model.

A very simple table model looks like this:

    public static class ShipTableModel extends AbstractTableModel {
      private Object[][] data = new Object[][] {
        { "100420", Boolean.FALSE, "Des Moines IA", "Spokane WA",
            "02/06/2000", new Float(450) },
        { "202174", Boolean.TRUE, "Basking Ridge NJ", "Princeton NJ",
            "05/20/2000", new Float(1250) },
        { "450877", Boolean.TRUE, "St. Paul MN", "Austin TX",
            "03/20/2000", new Float(1745) },
        { "101891", Boolean.FALSE, "Boston MA", "Albany NY",
            "04/04/2000", new Float(88) }
      };

      public int getRowCount() { return data.length; }
      public int getColumnCount() { return data[0].length; }

      public Object getValueAt(int row, int column) {
        return data[row][column];
      }
    }

We’d like to use the same column headings that we used in the previous example. The table model supplies these through a method called getColumnName(). We could add column headings to our simple table model like this:

    private String[] headings = new String[] {
      "Number", "Hot?", "Origin", "Destination", "Ship Date", "Weight"
    };

    public String getColumnName(int column) {
      return headings[column];
    }

By default, AbstractTableModel makes all its cells noneditable, which is what we wanted. No changes need to be made for this.

The final modification is to have the Hot? column and the Weight column formatted specially. To do this, we give our table model some knowledge about the column types. JTable automatically generates checkbox cells for Boolean column types and specially formatted number cells for Number types. To give the table model some intelligence about its column types, we override the getColumnClass() method. The JTable calls this method to determine the data type of each column. It may then represent the data in a special way. This table model returns the class of the item in the first row of its data:

    public Class getColumnClass(int column) {
      return data[0][column].getClass();
    }

That’s really all there is to do. The following complete example illustrates how you can use your own table model to create a JTable using the techniques just described:

    //file: ShipTable.java
    import java.awt.*;
    import java.awt.event.*;
    import javax.swing.*;
    import javax.swing.table.*;

    public class ShipTable {
      public static class ShipTableModel extends AbstractTableModel {
        private String[] headings = new String[] {
          "Number", "Hot?", "Origin", "Destination", "Ship Date", "Weight"
        };
        private Object[][] data = new Object[][] {
          { "100420", Boolean.FALSE, "Des Moines IA", "Spokane WA",
              "02/06/2000", new Float(450) },
          { "202174", Boolean.TRUE, "Basking Ridge NJ", "Princeton NJ",
              "05/20/2000", new Float(1250) },
          { "450877", Boolean.TRUE, "St. Paul MN", "Austin TX",
              "03/20/2000", new Float(1745) },
          { "101891", Boolean.FALSE, "Boston MA", "Albany NY",
              "04/04/2000", new Float(88) }
        };

        public int getRowCount() { return data.length; }
        public int getColumnCount() { return data[0].length; }

        public Object getValueAt(int row, int column) {
          return data[row][column];
        }

        public String getColumnName(int column) {
          return headings[column];
        }

        public Class getColumnClass(int column) {
          return data[0][column].getClass();
        }
      }

      public static void main(String[] args)
      {
        // create the data model and the JTable
        TableModel model = new ShipTableModel();
        JTable table = new JTable(model);

        table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);

        JFrame frame = new JFrame("ShipTable v1.0");
        frame.getContentPane().add(new JScrollPane(table));
        frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
        frame.setSize(500, 200);
        frame.setVisible(true);
      }
    }

The running application is shown in Figure 18-8.

Customizing a table
Figure 18-8. Customizing a table

Round Three: A Simple Spreadsheet

To illustrate just how powerful and flexible the separation of the data model from the GUI can be, we’ll show a more complex model. In the following example, we’ll implement a very slim but functional spreadsheet (see Figure 18-9) using almost no customization of the JTable. All of the data processing is in a TableModel called SpreadSheetModel.

A simple spreadsheet
Figure 18-9. A simple spreadsheet

Our spreadsheet does the expected stuff—allowing you to enter numbers or mathematical expressions such as (A1*B2)+C3 into each cell.[41] All cell editing and updating is driven by the standard JTable. We implement the methods necessary to set and retrieve cell data. Of course, we don’t do any real validation here, so it’s easy to break our table. (For example, there is no check for circular dependencies, which may be undesirable.)

As you will see, the bulk of the code in this example is in the inner class used to parse the value of the equations in the cells. If you don’t find this part interesting, you might want to skip ahead. But if you have never seen an example of this kind of parsing before, we think you will find it to be very cool. Through the magic of recursion and Java’s powerful String manipulation, it takes us only about 50 lines of code to implement a parser capable of handling basic arithmetic with arbitrarily nested parentheses.

Here’s the code:

    //file: SpreadsheetModel.java
    import java.util.StringTokenizer;
    import javax.swing.*;
    import javax.swing.table.AbstractTableModel;
    import java.awt.event.*;

    public class SpreadsheetModel extends AbstractTableModel {
      Expression [][] data;

      public SpreadsheetModel( int rows, int cols ) {
        data = new Expression [rows][cols];
      }

      public void setValueAt(Object value, int row, int col) {
        data[row][col] = new Expression( (String)value );
        fireTableDataChanged();
      }

      public Object getValueAt( int row, int col ) {
        if ( data[row][col] != null )
          try { return data[row][col].eval() + ""; }
          catch ( BadExpression e ) { return "Error"; }
        return "";
      }
      public int getRowCount() { return data.length; }
      public int getColumnCount() { return data[0].length; }
      public boolean isCellEditable(int row, int col) { return true; }

      class Expression {
        String text;
        StringTokenizer tokens;
        String token;

        Expression( String text ) { this.text = text.trim(); }

        float eval() throws BadExpression {
          tokens = new StringTokenizer( text, " */+-()", true );
          try { return sum(); }
          catch ( Exception e ) { throw new BadExpression(); }
        }

        private float sum() {
          float value = term();
          while( more() && match("+-") )
            if ( match("+") ) { consume(); value = value + term(); }
            else { consume(); value = value - term(); }
          return value;
        }
        private float term() {
          float value = element();
          while( more() && match( "*/") )
            if ( match("*") ) { consume(); value = value * element(); }
            else { consume(); value = value / element(); }
          return value;
        }
        private float element() {
          float value;
          if ( match( "(") ) { consume(); value = sum(); }
          else {
            String svalue;
            if ( Character.isLetter( token().charAt(0) ) ) {
            int col = findColumn( token().charAt(0) + "" );
            int row = Character.digit( token().charAt(1), 10 );
            svalue = (String)getValueAt( row, col );
          } else
            svalue = token();
            value = Float.parseFloat( svalue );
          }
          consume(); // ")" or value token
          return value;
        }
        private String token() {
          if ( token == null )
            while ( (token=tokens.nextToken()).equals(" ") );
          return token;
        }
        private void consume() { token = null; }
        private boolean match( String s ) { return s.indexOf( token() )!=-1; }
        private boolean more() { return tokens.hasMoreTokens(); }
      }

      class BadExpression extends Exception { }

      public static void main( String [] args ) {
        JFrame frame = new JFrame("Excelsior!");
        JTable table = new JTable( new SpreadsheetModel(15, 5) );
        table.setPreferredScrollableViewportSize( table.getPreferredSize() );
        table.setCellSelectionEnabled(true);
        frame.getContentPane().add( new JScrollPane( table ) );
        frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
        frame.pack();
        frame.show();
      }
    }

Our model extends AbstractTableModel and overrides just a few methods. As you can see, our data is stored in a 2D array of Expression objects. The setValueAt() method of our model creates Expression objects from the strings typed by the user and stores them in the array. The getValueAt() method returns a value for a cell by calling the expression’s eval() method. If the user enters some invalid text in a cell, a BadExpression exception is thrown, and the word error is placed in the cell as a value. The only other methods of TableModel we must override are getRowCount(), getColumnCount(), and isCellEditable() in order to determine the dimensions of the spreadsheet and to allow the user to edit the fields. That’s it! The helper method findColumn() is inherited from the AbstractTableModel.

Now on to the good stuff. We’ll employ our old friend StringTokenizer to read the expression string as separate values and the mathematical symbols (+-*/()) one by one. These tokens are then processed by the three parser methods: sum(), term(), and element(). The methods call one another generally from the top down, but it might be easier to read them in reverse to see what’s happening.

At the bottom level, element() reads individual numeric values or cell names (e.g., 5.0 or B2). Above that, the term() method operates on the values supplied by element() and applies any multiplication or division operations. And at the top, sum() operates on the values that are returned by term() and applies addition or subtraction to them. If the element() method encounters parentheses, it makes a call to sum() to handle the nested expression. Eventually, the nested sum returns (possibly after further recursion), and the parenthesized expression is reduced to a single value, which is returned by element(). The magic of recursion has untangled the nesting for us. The other small piece of magic here is in the ordering of the three parser methods. Having sum() call term() and term() call element() imposes the precedence of operators; that is, “atomic” values are parsed first (at the bottom), then multiplication, and finally, addition or subtraction.

The grammar parsing relies on four simple helper methods that make the code more manageable: token(), consume(), match(), and more(). token() calls the string tokenizer to get the next value, and match() compares it with a specified value. consume() is used to move to the next token, and more() indicates when the final token has been processed.

Sorting and Filtering

Java 6 introduced easy-to-use sorting and filtering for JTables. The following example demonstrates use of the default TableRowSorter and a simple regular expression filter.

    //file: SortFilterTable.java
    import javax.swing.*;
    import javax.swing.table.*;
    import javax.swing.event.*;
    
    import java.awt.BorderLayout;
    import java.util.regex.PatternSyntaxException;
    
    public class SortFilterTable extends JFrame {
    
        private JTable table;
        private JTextField filterField;
    
        public SortFilterTable() {
            
            super("Table Sorting & Filtering");
            setSize(500, 200);
            setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
    
            // Create a simple table model
            TableModel model = new AbstractTableModel() {
                private String[] columns = {"Name", "Pet", "Children"};
                
                private Object[][] people = {
                        {"Dan Leuck", "chameleon", 1},
                        {"Pat Niemeyer", "sugar glider", 2},
                        {"John Doe", "dog", 3},
                        {"Jane Doe", "panda", 2}
                        };            
                
                public int getColumnCount() { return columns.length; }
    
                public int getRowCount() { return people.length; }
    
                public Object getValueAt(int row, int col) { 
                    return people[row][col]; 
                }

                public Class getColumnClass(int col) {
                    return getValueAt(0, col).getClass();
                }

            };
            
            table = new JTable(model);
            table.setAutoCreateRowSorter(true);
            table.setFillsViewportHeight(true);
            
            // Create the filter area
            JPanel filterPanel = new JPanel(new BorderLayout());
            JLabel filterLabel = new JLabel("Filter ", 
                SwingConstants.TRAILING);
            filterPanel.add(filterLabel, BorderLayout.WEST);
            filterField = new JTextField();
            filterLabel.setLabelFor(filterField);
            filterPanel.add(filterField);
            
            // Apply the filter when the filter text field changes
            filterField.getDocument().addDocumentListener(
                new DocumentListener() {
                    public void changedUpdate(DocumentEvent e) {
                        filter();
                    }
                    public void insertUpdate(DocumentEvent e) {
                        filter();
                    }
                    public void removeUpdate(DocumentEvent e) {
                        filter();
                    }
                });
            
            filterPanel.setBorder(BorderFactory.createEmptyBorder(2, 
                2, 2, 2));
    
            add(filterPanel, BorderLayout.NORTH);
    
            add(new JScrollPane(table));
        }
    
        // Filter on the first column
        private void filter() {
            RowFilter<TableModel, Object> filter = null;
            
            // Update if the filter expression is valid
            try {
                // Apply the regular expression to columns 0 and 1
                filter = RowFilter.regexFilter(filterField.getText(),
                    0, 1);
            } catch (PatternSyntaxException e) {
                return;
            }
            ((TableRowSorter)table.getRowSorter()).setRowFilter(
                filter);
        }
    
        public static void main(String[] args) {
            new SortFilterTable().setVisible(true);       
        }
    }

Try clicking on the column headers to sort. We are using the default sorting behavior, which utilizes the natural sort order of cell values (i.e., alphabetical for strings, value for numbers, etc.). You can easily override the sorting behavior by implementing your own comparator and setting it on the TableRowSorter:

    TableRowSorter<TableModel> reverseSorter 
        = new TableRowSorter<TableModel>(table.getModel()); 
    reverseSorter.setComparator(0, new Comparator<String>() {
        public int compare(String a, String b) {
            return -a.compareTo(b);
        }
    });
    table.setRowSorter(reverseSorter);

If you require more advanced sorting, you can subclass TableRowSorter or its superclass, DefaultRowSorter.

Entering text in the field above the table will apply a filter using the text as a regular expression over the values in the first two columns (indices 0 and 1). For example, try entering “Doe”. The table will now display only John Doe and Jane Doe.

Printing JTables

Swing makes the printing of JTables a snap. Think we’re kidding? If you accept the basic default behavior, all that is required to pop up a print dialog box is the following:

    myJTable.print();

That’s it. The default behavior scales the printed table to the width of the page. This is called “fit width” mode. You can control that setting using the PrintMode enumeration of JTable, which has values of NORMAL and FIT_WIDTH:

    table.print( JTable.PrintMode.NORMAL );

The “normal” (ironically, nondefault) mode will allow the table to split across multiple pages horizontally to print without sizing down. In both cases, the table rows may span multiple pages vertically.

Other forms of the JTable print() method allow you to add header and footer text to the page and to take greater control of the printing process and attributes. We’ll talk a little more about printing when we cover 2D drawing in Chapter 20.

Desktops

At this point, you might be thinking that there’s nothing more that Swing could possibly do, but it just keeps getting better. If you’ve ever wished that you could have windows within windows in Java, Swing makes it possible with JDesktopPane and JInternalFrame. Figure 18-10 shows how this appears.

You get a lot of behavior for free from JInternalFrame. Internal frames can be moved by clicking and dragging the titlebar. They can be resized by clicking and dragging on the window’s borders. Internal frames can be iconified, which means reducing them to a small icon representation on the desktop. Internal frames may also be made to fit the entire size of the desktop (maximized). To you, the programmer, the internal frame is just a kind of special container. You can put your application’s data inside an internal frame just as with any other type of container.

Using internal frames on a JDesktopPane
Figure 18-10. Using internal frames on a JDesktopPane

The following brief example shows how to create the windows shown in Figure 18-10:

    //file: Desktop.java
    import java.awt.*;
    import java.awt.event.*;
    import javax.swing.*;
    import javax.swing.border.*;

    public class Desktop {
      public static void main(String[] args) {
        JFrame frame = new JFrame("Desktop");

        JDesktopPane desktop = new JDesktopPane();
        for (int i = 0; i < 5; i++) {
          JInternalFrame internal =
              new JInternalFrame("Frame " + i, true, true, true, true);
          internal.setSize(180, 180);
          internal.setLocation(i * 20, i * 20);
          internal.setVisible(true);
          desktop.add(internal);
        }

        frame.setSize(300, 300);
        frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
        frame.setContentPane(desktop);
        frame.setVisible(true);
      }
    }

All we’ve done here is to create a JDesktopPane and add internal frames to it. When each JInternalFrame is constructed, we specify a window title. The four true values passed in the constructor specify that the new window should be resizable, closable, maximizable, and iconifiable.

JInternalFrames fire off their own set of events. However, InternalFrameEvent and InternalFrameListener are just like WindowEvent and WindowListener with the names changed. If you want to hear about a JInternalFrame closing, just register an InternalFrameListener and define the internalFrameClosing() method. This is just like defining the windowClosing() method for a JFrame.

Pluggable Look-and-Feel

We mentioned before that Swing components can easily change their appearance, like master spies or thespians. Generally, different kinds of components within an application have coordinated appearances that are similar in some way. For example, they probably use the same font and the same basic color scheme. The collection of appearances and common behavior of GUI components is called a look-and-feel (L&F).

Part of the job of designing a GUI for an operating system is designing the L&F. Mac OS, therefore, has its own distinctive L&F, as does Windows. Java offers several different L&F schemes for Swing components. If you’re adept at graphic design, you can write your own L&F schemes and easily convince Swing to use them. This chameleon-like ability to change appearance is called pluggable look-and-feel, sometimes abbreviated PLAF (don’t pronounce that out loud if others are eating).

Seeing is believing. Here’s an example that creates a handful of Swing components. Menu items allow you to change the L&F dynamically while the application is running:

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

    public class QuickChange extends JFrame {

      public QuickChange() {
        super("QuickChange v1.0");
        createGUI();
      }

      protected void createGUI() {
        setSize(300, 200);

        // create a simple File menu
        JMenu file = new JMenu("File", true);
        JMenuItem quit = new JMenuItem("Quit");
        file.add(quit);
        quit.addActionListener(new ActionListener() {
          public void actionPerformed(ActionEvent e) { System.exit(0); }
        });

        // create the Look & Feel menu
        JMenu lnf = new JMenu("Look & Feel", true);
        ButtonGroup buttonGroup = new ButtonGroup();
        final UIManager.LookAndFeelInfo[] info =
            UIManager.getInstalledLookAndFeels();
        for (int i = 0; i < info.length; i++) {
          JRadioButtonMenuItem item = new
              JRadioButtonMenuItem(info[i].getName(), i == 0);
          final String className = info[i].getClassName();
          item.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent ae) {
              try { UIManager.setLookAndFeel(className); }
              catch (Exception e) { System.out.println(e); }
              SwingUtilities.updateComponentTreeUI(QuickChange.this);
            }
          });
          buttonGroup.add(item);
          lnf.add(item);
        }

        // add the menu bar
        JMenuBar mb = new JMenuBar();
        mb.add(file);
        mb.add(lnf);
        setJMenuBar(mb);

        // add some components
        JPanel jp = new JPanel();
        jp.add(new JCheckBox("JCheckBox"));
        String[] names =
          new String[] { "Tosca", "Cavaradossi", "Scarpia",
                         "Angelotti", "Spoletta", "Sciarrone",
                         "Carceriere", "Il sagrestano", "Un pastore" };
        jp.add(new JComboBox(names));
        jp.add(new JButton("JButton"));
        jp.add(new JLabel("JLabel"));
        jp.add(new JTextField("JTextField"));
        JPanel main = new JPanel(new GridLayout(1, 2));
        main.add(jp);
        main.add(new JScrollPane(new JList(names)));
        setContentPane(main);
        setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
      }

      public static void main(String[] args) {
        new QuickChange().setVisible(true);
      }
    }

The interesting part of this application is creating a menu of the available L&Fs. First, we ask a class called UIManager to tell us all about the available L&Fs on our computer:

    final UIManager.LookAndFeelInfo[] info =
            UIManager.getInstalledLookAndFeels();

Information about L&Fs is returned as instances of UIManager.LookAndFeelInfo. Despite the long name, there’s not much to this class; it just associates a name, such as Metal, and the name of the class that implements the L&F, such as javax.swing.plaf.metal.MetalLookAndFeel. In the QuickChange example, we create a menu item from each L&F name. If the menu item is selected, we tell the UIManager to use the selected L&F class. To make sure all the components are redrawn with the new L&F, we call updateComponentTreeUI(), a static method in the SwingUtilities class.

The JDK includes several L&Fs: Windows, OS X, Motif, the original Metal L&F, the highly customizable Synth, and the newest edition, Nimbus. Windows, OS X, and Motif are recreations of their corresponding native desktop environments. If you’re running Swing on Mac OS X, the default L&F is an implementation of Aqua, the UI design for all new Mac applications. Unfortunately, you cannot use this L&F on any other platforms because of licensing issues (the Windows L&F has similar restrictions).

The Metal L&F and its Ocean theme are the default on some platforms, but at this point they appear dated compared to modern windowing systems. The newest edition, Nimbus, is a far superior alternative. Its aethetics are on par with modern windowing systems and its use of scaleable vector graphics allows it to shine on high-density displays at various sizes. Nimbus is actually a highly evolved subclass of the Synth L&F.

Synth accommodates the new trend in skinnable user interfaces. Many applications now allow users to customize the L&F very easily, using only images and simple preferences files to create new appearances. Skinnability is not the same as a full-blown pluggable L&F, but it lets you do a lot without any programming required. Synth acts like an ordinary L&F, but can be customized through the use of images and XML description files to create new looks. For example, the borders of components (such as the shiny metal look of the Metal L&F) can be described by providing an example image and then specifying the offsets of the interior “corners” as well as the method to use (stretch or tile) to cover larger areas. Synth then uses the image to paint the borders of whatever components you specify. Synth can do quite a lot and even allows you to specify Java objects to be involved in painting, so you can resort to programming again if your L&F gets too complex.

Creating Custom Components

In this chapter and the previous one, we’ve worked with different user interface objects. We’ve used Swing’s impressive repertoire of components as building blocks and extended their functionality, but we haven’t actually created any new components. In this section, we create an entirely new component from scratch, a dial.

Until now, our examples have been fairly self-contained; they generally know everything about what to do and don’t rely on additional parts to do processing. Our menu example created a DinnerFrame class that had a menu of dinner options, but it included all the processing needed to handle the user’s selections. If we wanted to process the selections differently, we’d have to modify the class. A true component separates the detection of user input from the handling of those choices. It lets the user take some action and then informs other interested parties by emitting events.

Generating Events

Because we want our new classes to be components, they should communicate the way components communicate: by generating event objects and sending those events to listeners. So far, we’ve written a lot of code that listened for events but haven’t seen an example that generated its own custom events.

Generating events sounds like it might be difficult, but it isn’t. You can either create new kinds of events by subclassing java.util.EventObject, or use one of the standard event types. In either case, you just need to allow registration of listeners for your events and provide a means to deliver events to those listeners. Swing’s JComponent class provides a protected member variable called listenerList, which you can use to keep track of event listeners. It’s an instance of EventListenerList; basically it acts like the maître d’ at a restaurant, keeping track of all event listeners, sorted by type.

Often, you won’t need to worry about creating a custom event type. JComponent has methods that support firing of generic PropertyChangeEvents whenever one of a component’s properties changes. The example we’ll look at next uses this infrastructure to fire PropertyChangeEvents whenever a value changes.

A Dial Component

The standard Swing classes don’t have a component that’s similar to an old-fashioned dial—for example, the volume control on your radio. (The JSlider fills this role, of course.) In this section, we implement a Dial class. The dial has a value that can be adjusted by clicking and dragging to “twist” the dial (see Figure 18-11). As the value of the dial changes, DialEvents are fired off by the component. The dial can be used just like any other Java component. We even have a custom DialListener interface that matches the DialEvent class.

The Dial component
Figure 18-11. The Dial component

Here’s the Dial code:

    //file: Dial.java
    import java.awt.*;
    import java.awt.event.*;
    import java.util.*;
    import javax.swing.*;

    public class Dial extends JComponent {
      int minValue, nvalue, maxValue, radius;

      public Dial() { this(0, 100, 0); }

      public Dial(int minValue, int maxValue, int value) {
        setMinimum( minValue );
        setMaximum( maxValue );
        setValue( value );
        setForeground( Color.lightGray );

        addMouseListener(new MouseAdapter() {
          public void mousePressed(MouseEvent e) { spin(e); }
        });
        addMouseMotionListener(new MouseMotionAdapter() {
          public void mouseDragged(MouseEvent e) { spin(e); }
        });
      }

      protected void spin( MouseEvent e ) {
        int y = e.getY();
        int x = e.getX();
        double th = Math.atan((1.0 * y - radius) / (x - radius));
        int value=(int)(th / (2 * Math.PI) * (maxValue - minValue));
        if (x < radius)
          setValue( value + (maxValue-minValue) / 2 + minValue);
        else if (y < radius)
          setValue( value + maxValue );
        else
          setValue( value + minValue);
      }

      public void paintComponent(Graphics g) {
        Graphics2D g2 = (Graphics2D)g;
        int tick = 10;
        radius = Math.min( getSize().width,getSize().height )/2 - tick;
        g2.setPaint( getForeground().darker() );
        g2.drawLine( radius * 2 + tick / 2, radius,
           radius * 2 + tick, radius);
        g2.setStroke( new BasicStroke(2) );
        draw3DCircle( g2, 0, 0, radius, true );
        int knobRadius = radius / 7;
        double th = nvalue * (2 * Math.PI) / (maxValue - minValue);
        int x = (int)(Math.cos(th) * (radius - knobRadius * 3)),
        y = (int)(Math.sin(th) * (radius - knobRadius * 3));
        g2.setStroke(new BasicStroke(1));
        draw3DCircle(g2, x + radius - knobRadius,
           y + radius - knobRadius, knobRadius, false );
      }

      private void draw3DCircle(
          Graphics g, int x, int y, int radius, boolean raised)
      {
        Color foreground = getForeground();
        Color light = foreground.brighter();
        Color dark = foreground.darker();
        g.setColor(foreground);
        g.fillOval(x, y, radius * 2, radius * 2);
        g.setColor(raised ? light : dark);
        g.drawArc(x, y, radius * 2, radius * 2, 45, 180);
        g.setColor(raised ? dark : light);
        g.drawArc(x, y, radius * 2, radius * 2, 225, 180);
      }

      public Dimension getPreferredSize() {
        return new Dimension(100, 100);
      }

      public void setValue( int value ) {
        this.nvalue = value - minValue;
        repaint();
        fireEvent();
      }
      public int getValue()  { return nvalue+minValue; }
      public void setMinimum(int minValue)  { this.minValue = minValue; }
      public int getMinimum()  { return minValue; }
      public void setMaximum(int maxValue)  { this.maxValue = maxValue; }
      public int getMaximum()  { return maxValue; }

      public void addDialListener(DialListener listener) {
        listenerList.add( DialListener.class, listener );
      }
      public void removeDialListener(DialListener listener) {
        listenerList.remove( DialListener.class, listener );
      }

      void fireEvent() {
        Object[] listeners = listenerList.getListenerList();
        for ( int i = 0; i < listeners.length; i += 2 )
          if ( listeners[i] == DialListener.class )
            ((DialListener)listeners[i + 1]).dialAdjusted(
              new DialEvent(this, getValue()) );
      }

      public static void main(String[] args) {
        JFrame frame = new JFrame("Dial v1.0");
        final JLabel statusLabel = new JLabel("Welcome to Dial v1.0");
        final Dial dial = new Dial();
        frame.add(dial, BorderLayout.CENTER);
        frame.add(statusLabel, BorderLayout.SOUTH);

        dial.addDialListener(new DialListener() {
          public void dialAdjusted(DialEvent e) {
            statusLabel.setText("Value is " + e.getValue());
          }
        });

        frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
        frame.setSize( 150, 150 );
        frame.setVisible( true );
      }
    }

Here’s DialEvent, a simple subclass of java.util.EventObject:

    //file: DialEvent.java
    import java.awt.*;

    public class DialEvent extends java.util.EventObject {
        int value;

        DialEvent( Dial source, int value ) {
            super( source );
            this.value = value;
        }

        public int getValue() {
            return value;
        }
    }

Finally, here’s the code for DialListener:

    //file: DialListener.java
    public interface DialListener extends java.util.EventListener {
        void dialAdjusted( DialEvent e );
    }

Let’s start from the top of the Dial class. We’ll focus on the structure and leave you to figure out the trigonometry on your own.

Dial’s main() method demonstrates how to use the dial to build a user interface. It creates a Dial and adds it to a JFrame. Then main() registers a dial listener on the dial. Whenever a DialEvent is received, the value of the dial is examined and displayed in a JLabel at the bottom of the frame window.

The constructor for the Dial class stores the dial’s minimum, maximum, and current values; a default constructor provides a minimum of 0, a maximum of 100, and a current value of 0. The constructor sets the foreground color of the dial and registers listeners for mouse events. If the mouse is pressed or dragged, Dial’s spin() method is called to update the dial’s value. spin() performs some basic trigonometry to figure out what the new value of the dial should be.

paintComponent() and draw3DCircle() do a lot of trigonometry to figure out how to display the dial. draw3DCircle() is a private helper method that draws a circle that appears either raised or depressed; we use this to make the dial look three-dimensional.

The next group of methods provides ways to retrieve or change the dial’s current setting and the minimum and maximum values. The important thing to notice here is the pattern of get and set methods for all of the important values used by the Dial. We will talk more about this in Chapter 22. Also, notice that the setValue() method does two important things: it repaints the component to reflect the new value and fires the DialEvent signifying the change.

The final group of methods in the Dial class provides the plumbing necessary for our event firing. addDialListener() and removeDialListener() take care of maintaining the listener list. Using the listenerList member variable we inherited from JComponent makes this an easy task. The fireEvent() method retrieves the registered listeners for this component. It sends a DialEvent to any registered DialListeners.

Model and View Separation

The Dial example is overly simplified. All Swing components, as we’ve discussed, keep their data model and view separate. In the Dial component, we’ve combined these elements in a single class, which limits its reusability. To have Dial implement the MVC paradigm, we would have developed a dial data model and something called a UI-delegate that handled displaying the component and responding to user events. For a full treatment of this subject, see the JogShuttle example in O’Reilly’s Java Swing.

In Chapter 19, we’ll take what we know about components and containers and put them together using layout managers to create complex GUIs.



[41] You may need to double-click on a cell to edit it.