CONTENTS

Chapter 17. 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.

17.1 Text Components

Swing offers sophisticated text components, from plain-text entry boxes to HTML interpreters. 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. JTextArea is a multiline text editor, and JTextField is a simple, single-line 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 d = textArea.getDocument( );
d.addDocumentListener(someListener);

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

In addition, JTextField components generate ActionEvents whenever the user presses the Return key within the field. To get these events, 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.

17.1.1 The TextEntryBox Application

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

Figure 17-1. The TextEntryBox application

figs/LJ2.1701.gif

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 scrollbar:

//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(  );
  
    Container content = frame.getContentPane(  );
    content.add(new JScrollPane(area), BorderLayout.CENTER);
    content.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 19. 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 the user has typed. These methods can be used for JTextField and JTextArea since 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. 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.

17.1.2 Formatted Text

Java 1.4 introduced JFormattedTextField . This 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);
    }
}

The JFormattedTextField can be constructed with a variety of format-specifying objects including java.lang.Number (e.g., Integer and Float), java.text.NumberFormat, java.text.DateFormat, and the more arbitrary java.text.MaskFormatter. The NumberFormat and DateFormat classes of the java.text package are discussed in Chapters Chapter 9 and Chapter 10. 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 18.

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, you can set the setFocusLostBehavior() method with the value JFormattedTextField.COMMIT (the default is COMMIT_OR_REVERT).

17.1.3 Filtering Input

JFormattedTextField does not know about all format types itself and 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. In the next section we discuss high-level field validation to ensure the correctness of data once it is entered.

17.1.3.1 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.getContentPane(  ).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. FilterBypass has the same 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 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.

17.1.4 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 rejects any value that is not a number 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.getContentPane(  ).add(form);
        frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
        frame.pack(  );
        frame.setVisible(true);
    }
}

Here we have created an anonymous inner class extending InputVerifier. 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.

17.1.5 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 a single character, typically an asterisk. Figure 17-2 shows the option dialog example that was presented in Chapter 16. The example includes a JTextField and a JPasswordField.

Figure 17-2. Using a JPasswordField in a dialog

figs/LJ2.1702.gif

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 less vulnerable than Strings to discovery by memory-snooping password sniffer programs. 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.

17.1.6 Sharing a Data Model

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

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

figs/LJ2.1703.gif

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 also.

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(  ));
  
    Container content = frame.getContentPane(  );
    content.setLayout(new GridLayout(3, 1));
    content.add(new JScrollPane(areaFiftyOne));
    content.add(new JScrollPane(areaFiftyTwo));
    content.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.

17.1.7 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 ofJTextComponent: JEditorPane and JTextPane.

The first of these, JEditorPane, can display HTML and RTF documents. It also 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 17-4, has only about 70 lines of code.

Figure 17-4. The CanisMinor application, a simple web browser

figs/LJ2.1704.gif

//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 ) {
    Container content = getContentPane(  );
    content.setLayout(new BorderLayout(  ));
  
    JToolBar urlToolBar = new JToolBar(  );
    mURLField = new JTextField(urlString, 40);
    urlToolBar.add(new JLabel("Location:"));
    urlToolBar.add(mURLField);
    content.add(urlToolBar, BorderLayout.NORTH);
  
    mEditorPane = new JEditorPane(  );
    mEditorPane.setEditable(false);
    content.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://www.oreilly.com/catalog/learnjava2/";
    if (args.length > 0)
       urlString = args[0];
    new CanisMinor( urlString ).setVisible( true );
  }
}

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 (Rich Text Format 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. Subsequent releases of the SDK will contain enhancements to the capabilities of HTMLEditorKit; eventually, it will support HTML 4.0.

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. It will then inhabit a window of its own. All this behavior comes for free from the JToolBar class. All we had to do was 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.

17.1.8 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 a cursor (caret), highlighting, image embedding, and other advanced features.

We'll just take a peek at JTextPane here 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 17-5.

Figure 17-5. Using styled text in a JTextPane

figs/LJ2.1705.gif

//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);
  
    Container content = getContentPane(  );
    content.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);
  }
}

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 since 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).

17.2 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. This is largely due to a new focus system introduced in Java 1.4. The new focus system is very powerful and can be heavily customized through the use of "focus traversal policy" objects that control keyboard navigation. In general, 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 old AWT-style focus management by navigating 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 expected navigation from left to right, 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 every window and JInternalFrame is a root cycle. That means that 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(  )

17.3 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 so complicated, 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 17-6 shows a JTree running in a Swing application that we'll describe later.

Figure 17-6. The JTree class in action

figs/LJ2.1706.gif

17.3.1 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. Once you've got your nodes organized, you can create a TreeModel in the same way as before:

TreeModel model = new DefaultTreeModel(root);

17.3.2 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 17-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, like a JList, you should put a JTree in a JScrollPane if you want it to scroll.

17.3.3 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( ));
  }
});

17.3.4 A Complete Example

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

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.getContentPane( ).add(new JScrollPane(tree));
    frame.getContentPane(  ).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, 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.

17.4 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.

17.4.1 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.getContentPane(  ).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 17-7.

Figure 17-7. A rudimentary JTable

figs/LJ2.1707.gif

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 ellipses (...). This is shown in the Origin cell in the first two rows in Figure 17-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 set-RowSelectionAllowed() 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.

17.4.2 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 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 17-8.

Figure 17-8. Customizing a table

figs/LJ2.1708.gif

17.4.3 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 17-9) using almost no customization of the JTable. All of the data processing is in a TableModel called SpreadSheetModel.

Figure 17-9. A simple spreadsheet

figs/LJ2.1709.gif

Our spreadsheet does the expected stuff—allowing you to enter numbers or mathematical expressions such as (A1*B2)+C3 into each cell.[1] 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() 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; i.e., "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.

17.5 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 now makes it possible with JDesktopPane and JInternalFrame. Figure 17-10 shows how this works.

Figure 17-10. Using internal frames on a JDesktopPane

figs/LJ2.1710.gif

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.

The following brief example shows how to create the windows shown in Figure 17-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.

17.6 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 have 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 for 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. MacOS, therefore, has its own distinctive L&F, as does Windows. Java's standard edition 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, as 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. Then, to make sure all the components are redrawn with the new L&F, we call a static method in the SwingUtilities class called updateComponentTreeUI() .

The regular SDK includes several L&Fs: one that resembles Windows, one that resembles Motif, and a L&F called Metal. Metal is used by default on most platforms; you've been staring at it through all the examples in this chapter and the last chapter.

If you're running Swing on MacOS, there's a MacOS L&F you can install and use. It does not, however, run on any other platforms because of licensing issues (the Windows L&F has similar restrictions).

17.7 Creating Custom Components

In this chapter and the previous, 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.

Up 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.

17.7.1 Generating Events

Since 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, listenerList, 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.

17.7.2 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 17-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.

Figure 17-11. The Dial component

figs/LJ2.1711.gif

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.getContentPane(  ).add(dial, BorderLayout.CENTER);
    frame.getContentPane(  ).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 21. 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.

17.7.3 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 18, we'll take what we know about components and containers and put them together using layout managers to create complex GUIs.

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

CONTENTS