Chapter 11. Text Components and Undo

In this chapter:

11.1 Text Components overview

This chapter summarizes the most basic and commonly used text component features, and introduces the undo package. In the next chapter we develop a basic JTextArea application in the process of demonstrating the use of menus and toolbars. In chapter 19 we discuss the inner workings of text components in much more detail. In chapter 20 we develop an extensive JTextPane word processor application with powerful font, style, paragraph, find & replace, and spell-checking dialogs.

11.1.1 JTextComponent

abstract class javax.swing.text.JTextComponent

The JTextComponent class serves as the super-class of each Swing text component. All text component functionality is defined by this class, along with the plethora of supporting classes and interfaces provided in the text package. The text components themselves are members of the javax.swing package: JTextField, JPasswordField, JTextArea, JEditorPane, and JTextPane.

Note: We have purposely left out most of the details behind text components in this chapter to provide only the information that you will most likely need on a regular basis. If, after reading this chapter, you would like a more thorough understanding of how text components work, and how to customize them or take advantage of some of the more advanced features, see chapters 19 and 20.

JTextComponent is an abstract sub-class of JComponent, and implements the Scrollable interface (see chapter 7). Each multi-line text component is designed to be placed in a JScrollPane.

Textual content is maintained in instances of the javax.swing.text.Document interface, which acts as the text component model. The text package includes two concrete Document implementations: PlainDocument and StyledDocument. PlainDocument allows one font, one color, and is limited to character content. StyledDocument is much more complex, allowing multiple fonts, colors, embedded images and components, and various sets of hierarchically resolving textual attributes. JTextField, JPasswordField, and JTextArea each use a PlainDocument model. JEditorPane and JTextPane use a StyledDocument model. We can retrieve a text component’s Document with getDocument(), and assign one with setDocument(). We can also attach DocumentListeners! to a document to listen for changes in that document’s textu! al! content (this is much different than a key listener because all document events are dispatched after a change has been made).

We can assign and retrieve the color of a text component’s Caret with setCaretColor() and getCaretColor(). We can also assign and retrieve the current Caret position in a text component with setCaretPosition() and getCaretPosition().

The disabledColor property allows assignment of a font color to use when in the disabled state. The foreground and background properties inherited from JComponent also apply, where the foreground color is used as the font color when a text component is enabled, and the background color is used as the background for the whole text component. The font property specifies the font to render the text in. Note that the font property, and the foreground and background color properties, do not overpower any attributes assigned to styled text components such as JEditorPane and ! JTextPane.

All text components maintain information about their current selection. We can retrieve the currently selected text as a String with getSelectedText(), and we can assign and retrieve specific background and foreground colors to use for selected text with setSelectionBackground()/getSelectionBackground() and setSelectionForeground()/getSelectionForeground() respectively.

JTextComponent also maintains a bound focusAccelerator property, which is a char that is used to transfer focus to a text component when the corresponding key is pressed simultaneously with the ALT key. This works internally by calling requestFocus() on the text component, and will occur as long as the top-level window containing the given text component is currently active. We can assign/retrieve this character with setFocusAccelerator()/getFocusAccelerator(), and we can turn this functionality off by assigning ‘\0’.

The read() and write() methods provide convenient ways to read and write text documents respectively. The read() method takes a java.io.Reader and an Object describing the Reader stream, and creates a new document model appropriate to the given text component containing the obtained character data. The write() method stores the content of the document model into a given java.io.Writer stream! .

 

Warning: We can customize any text component’s document model. However, it is important to realize that whenever the read() method is invoked a new document will be created. Unless this method is overriden, a custom document that had been assigned with setDocument() will be lost whenever read() is invoked, because the current document will be replaced by a default instance.

 

11.1.2 JTextField

class javax.swing.JTextField

JTextField is a single-line text component using a PlainDocument model. The horizonatlAlignment property specifies text justification within the text field. We can assign/retrive this property with setHorizontalAlignment()/getHorizontalAlignment, and acceptable values are JTextField.LEFT, JTextField.CENTER, and JTextField.RIGHT.

There are several JTextField constructors, two of which allow us to specify a number of columns. We can also assign/retrieve this number, the columns property, with setColumns()/getColumns(). Specifying a specific number of columns rarely corresponds to the actual number of characters that will fit in that text field. Firstly because a text field might not receive its preferred size due to the current layout manager. Second, the width of a column is the width of the character ‘m’ in the current font. Unless a monospaced font is being used, this width will be greater than most other characters.

The following example creates 14 JTextFields with a varying number of columns. Each field contains a number of ‘m’s equal to its number of columns.

The Code: JTextFieldTest.java

see \Chapter11\1

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

public class JTextFieldTest extends JFrame 
{
  public JTextFieldTest() {
    super("JTextField Test");

    getContentPane().setLayout(new FlowLayout());

    JTextField textField1 = new JTextField("m",1);
    JTextField textField2 = new JTextField("mm",2);
    JTextField textField3 = new JTextField("mmm",3);
    JTextField textField4 = new JTextField("mmmm",4);
    JTextField textField5 = new JTextField("mmmmm",5);
    JTextField textField6 = new JTextField("mmmmmm",6);
    JTextField textField7 = new JTextField("mmmmmmm",7);
    JTextField textField8 = new JTextField("mmmmmmmm",8);
    JTextField textField9 = new JTextField("mmmmmmmmm",9);
    JTextField textField10 = new JTextField("mmmmmmmmmm",10);
    JTextField textField11 = new JTextField("mmmmmmmmmmm",11);
    JTextField textField12 = new JTextField("mmmmmmmmmmmm",12);
    JTextField textField13 = new JTextField("mmmmmmmmmmmmm",13);
    JTextField textField14 = new JTextField("mmmmmmmmmmmmmm",14);

    getContentPane().add(textField1);
    getContentPane().add(textField2);
    getContentPane().add(textField3);
    getContentPane().add(textField4);
    getContentPane().add(textField5);
    getContentPane().add(textField6);
    getContentPane().add(textField7);
    getContentPane().add(textField8);
    getContentPane().add(textField9);
    getContentPane().add(textField10);
    getContentPane().add(textField11);
    getContentPane().add(textField12);
    getContentPane().add(textField13);
    getContentPane().add(textField14);

    setSize(300,170);
    setVisible(true);
  }

  public static void main(String argv[]) { 
    new JTextFieldTest(); 
  }
}

Figure 11.1 illustrates. Note that none of the text completely fits in its field. This is because JTextField does not factor in the size of its border when calculating its preferred size, as we might expect. To work around this problem, although not an ideal solution, we can add one more column to each text field. The result is shown in figure 11.2. This solution is more appropriate when a fixed width font (monospaced) is being used. Figure 11.3 illustrates.

Figure 11.1 JTextFields using an equal number of columns and ‘m’ characters.

Figure 11.2 JTextFields using one more column than number of ‘m’ characters.

Figure 11.3 JTextFields using a monospaced font, and one more column than number of ‘m characters.

 

Note: Using a monospaced font is always more appropriate when a fixed character limit is desired.

JTextField also maintains a BoundedRangeModel (see chapter 13) as its horizontalVisibility property. This model is used to keep track of the amount of currently visible text. The minimum is zero (the beginning of the document), and the maximum is equal to the width of the text field or the total length of the text in pixels (whichever is greater). The value is the current offset of the text displayed at the left edge of the field, and the extent is the width of ! the text field in pixels.

By default a KeyStroke (see 2.13.2) is established with ENTER that causes an ActionEvent to be fired. By simply adding an ActionListener to a JTextField we will receive events whenever ENTER is pressed while that field has the current focus. This is very convenient functionality, but it may also get in the way of things. To remove this registered keystroke we can do the following:

KeyStroke enter = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0);

Keymap map = myJTextField.getKeymap();

map.removeKeyStrokeBinding(enter);

JTextField’s document model can be customized to allow only certain forms of input by extending PlainDocument and overriding the insertString() method. The following code shows a class that will only allow six or less digits to be entered. We can assign this document to a JTextField with the setDocument() method (see chapter 19 for more about working with Documents).

class SixDigitDocument extends PlainDocument 
{
      public void insertString(int offset, 
       String str, AttributeSet a)
        throws BadLocationException {
          char[] insertChars = str.toCharArray();       
                
          boolean valid = true;
          boolean fit = true;
          if (insertChars.length + getLength() <= 6) {
            for (int i = 0; i < insertChars.length; i++) {
              if (!Character.isDigit(insertChars[i])) {
                valid = false;
                break;
              }
            }
          }
          else
            fit = false;
          
          if (fit && valid)
            super.insertString(offset, str, a); 
          else if (!fit) 
            getToolkit().beep();
      }      
}

UI Guideline : Don't overly restrict input

Filtering text fields during data entry is a powerful aid to usability. It helps prevent the user from making a mistake and can speed operation by removing the need for validation and correction procedures. However, it is important not to overly restrict the allowable input. Ensure that all reasonable input is expected and accepted.

For example, with a phone number, allow "00 1 44 654 7777" and allow "00+1 44 654 7777" and allow "00-1-1-654-7777" as well as "00144654777". Phone numbers can contain more than just numbers!
Another example might be dates. You should allow "04-06-99", "04/06/99" and "04:06:99" as well as "040699".

 

11.1.3 JPasswordField

class javax.swing.JPasswordField

JPasswordField is a fairly simple extension of JTextField that displays an echo character instead of the actual content that is placed in its model. This echo character defaults to ‘*’, and we can assign a different one with setEchoChar().

Unlike other text components, we cannot retrieve the actual content of a JPasswordField with getText() (this method, along with setText() have been deprecated in JPasswordField). Instead we must use the getPassword() method, which returns an array of chars. Also note that JPasswordField overrides the JTextComponent copy() and cut() methods to do nothing but emit a beep (for security reasons).

Figure 11.4 shows the JTextFieldDemo example of section 11.1.2, using JPasswordFields instead, each using a monospaced font.

Figure 11.4 JPasswordFields using a monospaced font, and one more column than number of characters.

 

11.1.4 JTextArea

class javax.swing.JTextArea

JTextArea allows multiple lines of text and, like JTextField, it uses a PlainDocument model. As we discussed above, JTextArea cannot display multiple fonts or font colors. JTextArea can perform line wrapping, and when line wrapping is enabled, we can specify whether lines break on word boundaries or not. To enable/disable line wrapping we set the lineWrap property with setLineWrap(). To enable/disable wrapping on boundaries (which will only have an effect wh! en lineWrap is set to true) we set the wrapStyleWord property using setWrapStyleWord(). Note that both lineWrap and wrapStyleWord are bound properties.

JTextArea overrides isManagingFocus() (see 2.12) to return true, indicating that the FocusManager will not transfer focus out of a JTextArea when TAB is pressed. Instead a TAB is inserted into the document, which is a number of spaces equal to tabSize. We can assign/retrieve the tab size with setTabSize()/getTabSize() respectively. tabSize is also a bound property.

There are several ways to add text to a JTextArea’s document. We can pass this text in to one of the constructors, append it to the end of the document using the append() method, insert a string at a given character offset using the insert() method, or replace a given range of text with the replaceRange() method. As with any text component, we can also set the text with the JTextComponent setText() method, and we can add and remove text directly from its Document (see chapter 19 for more details about the Document interface).

JTextArea maintains lineCount and rows properties which can easily be confused. The rows property specifies how many rows of text JTextArea is actually displaying. This may change whenever a text area is resized. The lineCount property specifies how many lines of text the document contains. This usually means a set of characters ending with a line break (‘\n’). We can retrieve the character offset of the end of a given line with getLineEndOffset(), the character! offset of the beginning of a given line with getLineStartOffset(), and the line number that contains a given offset with getLineOfOffset().

The rowHeight and columnWidth properties are determined by the height and with of the current font respectively. The width of one column is equal to the width of the ‘m’ character in the current font. We cannot assign new values to the properties, but we can override the getColumnWidth() and getRowHeight() methods in a sub-class to return any value we like. We can explicitly set the number of rows and columns a text area contains with setRows() and setColumns(), and the getRows() and getColumns() methods will only return these explicitly assigned values (not the current row and column count as we might assume at first glance).

Unless placed in a JScrollPane or a container using a layout manager which enforces a certain size (or a layout manager using the preferredSize of its children), JTextArea will resize itself dynamically depending on the amount of text entered. This behavior is rarely desired.

11.1.5 JEditorPane

class javax.swing.JEditorPane

JEditorPane is a multi-line text component capable of displaying and editing various different types of content. Swing provides support for HTML and RTF, but there is nothing stopping us from defining our own, or implementing support for an alternate format.

 

Note: Swing’s support for HTML and RTF is located in the javax.swing.text.html and javax.swing.text.rtf packages respectively. At the time of this writing the html package was still going through major changes. The rtf package was also still under development, but was closer to a finalized state than HTML support. For this reason we devoted chapter 20 to the step-wise construction of an RTF word processor application. We expect to see stronger support for displaying and editing HTML in a future Java 2 release.

Support for different content is accomplished in part through the use of custom EditorKit objects. JEditorPane’s contentType property is a String representing the type of document the editor pane is currently set up to display. The EditorKit maintains this value which, for DefaultEditorKit, defaults to "text/plain." HTMLEditorKit and RTFEditorKit have contentType values of "text/html" and "text/rtf" respectively (see chapter 19 for more about EditorKits).

In chapter 9 we built a simple web browser using a non-editable JEditorPane by passing a URL to its constructor. When in non-editable mode JEditorPane displays HTML pretty much as we might expect (although it has a long way to go to match Netscape). By allowing editing, JEditorPane will display an HTML document with many of its tags specially rendered as shown in figure 11.1 (compare this to figure 9.4).

Figure 11.5 JEditorPane displaying HTML in editable mode.

JEditorPane is smart enough to use an appropriate EditorKit, if one is available, to display a document passed to it. When displaying an HTML document, JEditorPane fires HyperlinkEvents (defined in the javax.swing.event package). We can attach HyperlinkListeners to JEditorPane to listen for hyperlink invocations, as demonstrated by the examples at the end of chapter 9. The following code shows how simple it is to construct an HTML browser using an active ! HyperlinkListener.

  m_browser = new JEditorPane(new URL("http://java.sun.com/products/jfc/tsc/index.html"));
  
  m_browser.setEditable(false);
  
  m_browser.addHyperlinkListener( new HyperlinkListener() {
    public void hyperlinkUpdate(HyperlinkEvent e) {
      if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {
        URL url = e.getURL();
        if (url == null)
          return;
        try { m_browser.setPage(e.getURL); }
        catch (IOException e) { e.printStackTrace(); }
      }
    }
  }

JEditorPane uses a Hashtable to store its editor kit/content type pairs. We can query this table and retrieve the editor kit associated with a particular content type, if there is one, using the getEditorKitForContentType() method. We can get the current editor kit with getEditorKit(), and the current content type with getContentType(). We can set the current content type with setContentType(), and if there is an appropriate editor kit in JEditorPane’s hash table the appropriate editor kit will r! eplace the current one. We can also assign an editor kit for a given content type using the setEditorKitForContentType() method (we will discuss EditorKits, and the ability to construct our own, in chapter 19).

JEditorPane uses a DefaultStyledDocument as its model. In HTML mode an HTMLDocument, which extends DefaultStyledDocument, is used. DefaultStyledDocument is quite powerful, allowing us to associate attributes with characters, paragraphs, and apply logical styles (see chapter 19).

11.1.6 JTextPane

class javax.swing.JTextPane

JTextPane extends JEditorPane and thus inherits its abilities to display various types of content. The most significant functionality JTextPane offers is the ability to programmatically assign attributes to regions of its content, and embed components and images within its document.

To assign attributes to a region of document content we use an AttributeSet implementation. We will describe AttributeSets in detail in chapter 19, but it suffices to say here that they contain a group of attributes such as font type, font style, font color, paragraph and character properties, etc. These attributes are assigned through the use of various static methods defined in the StyleConstants class, which we will also discuss further in chapter 19.

The following example demonstrates embedded icons, components, and stylized text. Figure 11.6 illustrates.

Figure 11.6 JTextPane with inserted ImageIcons, text with attributes, and an active JButton.

The Code: JTextPaneDemo.java

see \Chapter11\2

import java.awt.*;
import java.awt.event.*;
import java.io.*;
import javax.swing.*;
import javax.swing.text.*;

public class JTextPaneDemo extends JFrame 
{
  static SimpleAttributeSet ITALIC_GRAY = new SimpleAttributeSet();
  static SimpleAttributeSet BOLD_BLACK = new SimpleAttributeSet();
  static SimpleAttributeSet BLACK = new SimpleAttributeSet();

  JTextPane m_editor = new JTextPane();

  // Best to reuse attribute sets as much as possible.
  static {
    StyleConstants.setForeground(ITALIC_GRAY, Color.gray);
    StyleConstants.setItalic(ITALIC_GRAY, true);
    StyleConstants.setFontFamily(ITALIC_GRAY, "Helvetica");
    StyleConstants.setFontSize(ITALIC_GRAY, 14);


    StyleConstants.setForeground(BOLD_BLACK, Color.black);
    StyleConstants.setBold(BOLD_BLACK, true);
    StyleConstants.setFontFamily(BOLD_BLACK, "Helvetica");
    StyleConstants.setFontSize(BOLD_BLACK, 14);    


    StyleConstants.setForeground(BLACK, Color.black);
    StyleConstants.setFontFamily(BLACK, "Helvetica");
    StyleConstants.setFontSize(BLACK, 14);
  }

  public JTextPaneDemo() {
    super("JTextPane Demo");

    JScrollPane scrollPane = new JScrollPane(m_editor);
    getContentPane().add(scrollPane, BorderLayout.CENTER);

    setEndSelection();
    m_editor.insertIcon(new ImageIcon("manning.gif"));
    insertText("\nHistory: Distant\n\n", BOLD_BLACK);

    setEndSelection();
    m_editor.insertIcon(new ImageIcon("Lee_fade.jpg"));
    insertText("                                    ", BLACK);
    setEndSelection();
    m_editor.insertIcon(new ImageIcon("Bace_fade.jpg"));

    insertText("\n      Lee Fitzpatrick            "
      + "                                    "
      + "Marjan Bace\n\n", ITALIC_GRAY);
    
    insertText("When we started doing business under " +
      "the Manning name, about 10 years ago, we were a very " +
      "different company. What we are now is the end result of " +
      "an evolutionary process in which accidental " +
      "events played as big a role, or bigger, as planning and " + 
      "foresight.\n", BLACK);

    setEndSelection();
    JButton manningButton = new JButton("Visit Manning");
    manningButton.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        m_editor.setEditable(false);
        try { m_editor.setPage("http://www.manning.com"); }
        catch (IOException ioe) { ioe.printStackTrace(); }
      } 
    });
    m_editor.insertComponent(manningButton);
   
    setSize(500,450);
    setVisible(true);
  }

  protected void insertText(String text, AttributeSet set) {
    try {
      m_editor.getDocument().insertString(
        m_editor.getDocument().getLength(), text, set); 
    } 
    catch (BadLocationException e) {
      e.printStackTrace();
    }
  }

  // Needed for inserting icons in the right places
  protected void setEndSelection() {
    m_editor.setSelectionStart(m_editor.getDocument().getLength());
    m_editor.setSelectionEnd(m_editor.getDocument().getLength());    
  }
	
  public static void main(String argv[]) { 
    new JTextPaneDemo(); 
  }
}

As demonstrated above, we can insert images and components with JTextPane’s insertIcon() and insertComponent() methods. Note that these methods insert the given object by replacing the current selection. If there is no current selection they will be placed at the beginning of the document. This is why we defined the setEndSelection() method in our example above to point the selection to the end of the document where we want to do insertions.

When inserting text, we cannot simply append it to the textpane itself. Instead we retrieve its document and call insertString(). To give attributes to inserted text we can construct AttributeSet implementations, and assign attributes to that set using the StyleConstants class. In the example above we do this by constructing three SimpleAttributeSets as static instances (so that they may be reused as much as possible).

As an extension of JEditorPane, JTextPane uses a DefaultStyledDocument for its model. Text panes use a special editor kit, DefaultStyledEditorKit, to manage their Actions and Views. JTextPane also supports the use of Styles, which are named collections of attributes. We will discuss styles as well as many other advanced features of JTextPane in chapters 19 and 20.

11.2 Undo/Redo

Undo/redo options are commonplace in applications such as paint programs and word processors, and have been used extensively throughout the writing of this book! It is interesting that this functionality is provided as part of the Swing library, as it is completely Swing independent. In this section we will briefly introduce the javax.swing.undo constituents, and in the process of doing so, we will present an example showing how undo/redo functionality can be integrated into any app. The text components come with built-in undo/redo functionality, and we will also discuss how to take advantage of this.

11.2.1 The UndoableEdit interface

abstract interface javax.swing.undo.UndoableEdit

This interface acts as a template definition for anything that can be undone/redone. Implementations should normally be very lightweight, as undo/redo operations commonly occur quickly in succession.

UndoableEdits are designed to have three states: undoable, redoable, and dead. When an UndoableEdit is in the undoable state, calling undo() will perform an undo operation. Similarly, when an UndoableEdit is in the redoable state, calling redo() will perform a redo operation. Methods canUndo() and canRedo() provide ways to check whether or not an UndoableEdit is in the undoable or redoable s! tate respectively. We can use the die() method to explicitly send an UndoableEdit to the dead state. When in the dead state, an UndoableEdit cannot be undone or redone, and any attempt to do so will generate an exception.

UndoableEdits maintains three String properties (normally used as menu item text): presentationName, undoPresentationName, and redoPresentationName. Methods addEdit() and replaceEdit() are meant to be used to merge two edits and replace an edit, respectively. UndoableEdit also defines the concept of significant and insignificant edits. An insignificant edit is one that UndoManager (see 11.2.6) ignores when an undo/redo request is made. CompoundEdit (see 11.2.3), however, will pay attention to both significant and insignificant edits. The significant property of an UndoableEdit can be queried with isSignificant().

11.2.2 AbstractUndoableEdit

class javax.swing.undo.AbstractUndoableEdit

AbstractUndoableEdit implements UndoableEdit and defines two boolean properties representing the three UndoableEdit states. The alive property is true when an edit is not dead. The done property is true when an undo can be performed, and false when a redo can be performed.

The default behavior provided by this class is good enough for most sub-classes. All AbstractUndoableEdits are significant, and the undoPresentationName and redoPresentationName properties are formed by simply appending "Undo" and "Redo" to presentationName.

The following example demonstrates a basic square painting program with undo/redo functionality. This app simply draws a square outline wherever a mouse press occurs. A Vector of Points is maintained which represent the upper left-hand corner of each square that is drawn on the canvas. We create an AbstractUndoableEdit sub-class to maintain a reference to a Point, with undo() and redo() methods that remove and add that Point from the Vector, respectively. Figure 11.7 illustrates.

Figure 11.7 A square painting app with one level of undo/redo.

The Code: UndoRedoPaintApp.java

see \Chapter11\3

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

public class UndoRedoPaintApp extends JFrame
{
  protected Vector m_points = new Vector();
  protected PaintCanvas m_canvas = new PaintCanvas(m_points);
  protected UndoablePaintSquare m_edit;
  protected JButton m_undoButton = new JButton("Undo");
  protected JButton m_redoButton = new JButton("Redo");

  public UndoRedoPaintApp() {
    super("Undo/Redo Demo");

    m_undoButton.setEnabled(false);
    m_redoButton.setEnabled(false);

    JPanel buttonPanel = new JPanel(new GridLayout());
    buttonPanel.add(m_undoButton);
    buttonPanel.add(m_redoButton);

    getContentPane().add(buttonPanel, BorderLayout.NORTH);
    getContentPane().add(m_canvas, BorderLayout.CENTER);

    m_canvas.addMouseListener(new MouseAdapter() {
      public void mousePressed(MouseEvent e) {
        Point point = new Point(e.getX(), e.getY());
        m_points.addElement(point);
        m_edit = new UndoablePaintSquare(point, m_points);
        m_undoButton.setText(m_edit.getUndoPresentationName());
        m_redoButton.setText(m_edit.getRedoPresentationName());
        m_undoButton.setEnabled(m_edit.canUndo());
        m_redoButton.setEnabled(m_edit.canRedo());
        m_canvas.repaint();
      }
    });

    m_undoButton.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        try { m_edit.undo(); }
        catch (CannotRedoException cre) { cre.printStackTrace(); }
        m_canvas.repaint();
        m_undoButton.setEnabled(m_edit.canUndo());
        m_redoButton.setEnabled(m_edit.canRedo());
      }
    });

    m_redoButton.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        try { m_edit.redo(); }
        catch (CannotRedoException cre) { cre.printStackTrace(); }
        m_canvas.repaint();
        m_undoButton.setEnabled(m_edit.canUndo());
        m_redoButton.setEnabled(m_edit.canRedo());
      }
    });

    setSize(400,300);
    setVisible(true);
  }

  public static void main(String argv[]) { 
    new UndoRedoPaintApp(); 
  }
}

class PaintCanvas extends JPanel
{
  Vector m_points;
  protected int width = 50;
  protected int height = 50;

  public PaintCanvas(Vector vect) {
    super();
    m_points = vect;
    setOpaque(true);
    setBackground(Color.white);
  }

  public void paintComponent(Graphics g) {
    super.paintComponent(g);
    g.setColor(Color.black);
    Enumeration enum = m_points.elements();
    while(enum.hasMoreElements()) {
      Point point = (Point) enum.nextElement();
      g.drawRect(point.x, point.y, width, height);
    }
  }
}

class UndoablePaintSquare extends AbstractUndoableEdit 
{
  protected Vector m_points;
  protected Point m_point;

  public UndoablePaintSquare(Point point, Vector vect) {
    m_points = vect;
    m_point = point;
  }   
  
  public String getPresentationName() {
    return "Square Addition";
  }

  public void undo() {
    super.undo();
    m_points.remove(m_point);
  }

  public void redo() {
    super.redo();
    m_points.add(m_point);
  }
}

One thing to note about this example is that it is extremely limited. Because we are not maintaining an ordered collection of UndoableEdits we can only perform one undo/redo. CompoundEdit and UndoManager directly address this limitation.

11.2.3 CompoundEdit

class javax.swing.undo.CompoundEdit

This class extends AbstractUndoableEdit to support an ordered collection of UndoableEdits, maintained as a protected Vector called edits. UndoableEdits can be added to this vector with addEdit(), but they cannot so easily be removed (for this, a sub-class would be necessary).

Even though CompoundEdit is more powerful than AbstractUndoableEdit, it is far from the ideal solution. Edits cannot be undone until all edits have been added. Once all UndoableEdits are added we are expected to call end(), at which point CompoundEdit will no longer accept any additional edits. Once end() is called, a call to undo() will undo all edits, whether significant or not. A redo() ! will then redo them all, and we can continue to cycle back and forth like this as long as the CompoundEdit itself remains alive. For this reason, CompoundEdit is useful for a pre-defined or intentionally limited set of states.

CompoundEdit introduces an additional state property called inProgress, which is true if end() has not been called. We can retrieve the value of inProgess with isInProgress(). The significant property, inherited from UndoableEdit, will be true if one or more of the contained UndoableEdi! ts is significant, and false otherwise.

11.2.4 UndoableEditEvent

class javax.swing.event.UndoableEditEvent

This event encapsulates a source Object and an UndoableEdit, and is meant to be passed to implementations of the UndoableEditListener interface.

11.2.5 The UndoableEditListener interface

class javax.swing.event.UndoableEditListener

This listener is intended for use by any class wishing to listen for operations that can be undone/redone. When such an operation occurs an UndoableEditEvent can be sent to an UndoableEditListener for processing. UndoManager implements this interface so we can simply add it to any class defining undoable/redoable operations. It is important to emphasize that UndoableEditEvents are not fired when an undo or redo actually occurs, but when an operation occurs which has an UndoableEdit associated with it. This interface declares one method, undoableEditHappened(), which accepts an UndoableEditEvent. We are generally responsible for passing UndoableEditEvents to this method. The example in the next section demonstrates.

11.2.6 UndoManager

class javax.swing.undo.UndoManager

UndoManager extends CompoundEdit and relieves us of the limitation where undos and redos cannot be performed until edit() is called. It also relieves us of the limitation where all edits are undone or redone at once. Another major difference from CompoundEdit is that UndoManager simply skips over all insignificant edits when undo() or redo() is called, effectively not paying them any attention. Interestingly, UndoManager allows us to add edits while inProgress is true, but if end() is ever called, UndoManager immediately starts acting like a CompoundEdit.

UndoManager introduces a new state called undoOrRedo which, when true, signifies that calling undo() or redo() is valid. This property can only be true if there is more than one edit stored, and only if there is at least one edit in the undoable state and one in the redoable state. The value of this property can be retrieved with canUndoOrRedo(), and the getUndoOrRedoPresentationName() metho! d will return an appropriate name for use in a menu item or elsewhere.

We can retrieve the next significant UndoableEdit that is scheduled to be undone or redone with editToBeUndone() or editToBeRedone() respectively. We can kill all stored edits with discardAllEdits(). Methods redoTo() and undoTo() can be used to programmatically invoke undo() or redo() on all edits from the current edit to the edit provided as parameter.

We can set the maximum number of edits that can be stored with setLimit(). The value of the limit property (100 by default) can be retrieved with getLimit(), and if it is set to a value smaller than the current number of edits, they edits will be reduced using the protected trimForLimit() method. Based on the index of the current edit within the edits vector, this method will attempt to kill the most balanced number of edits in undoable and redoable states as it can in order to achieve the given limit. The further an edit is (based on its vector index in the edits vector) the more of a candidate it is f! or death when a trim occurs, as edits sentenced to death are taken from the extreme ends of the edits vector.

It is very important to note that when an edit is added to the edits vector, all edits in the redoable state (i.e. those appearing after the index of the current edit) do not simply get moved up one index. Rather, they are killed off. So, for example, suppose in a word processor application you enter some text, change the style of 10 different regions of that text, and then undo the most recent 5 style additions. Then a new style change is made. The first 5 style changes that were made remain in the undoable state, and the new edit is added, also in the undoable state. However, the 5 style changes that were undone (i.e. moved to the redoable state) are now completely lost.

 

Note: All public UndoManager methods are synchronized to enable thread-safety, and make UndoManager a good candidate for use as a central undo/redo manager for any number of functionalities.

The following code shows how we can modify our UndoRedoPaintApp example to allow multiple undos and redos using an UndoManager. Because UndoManager implements UndoableEditListener, we should normally add UndoableEdits to it using the undoableEditHappened() method rather than addEdit(). This is because undoableEditHappened() calls addEdit() for us, and at the same time allows us to keep track of the source of the operation. This enables UndoManager to act as a central location for all undo/redo edits in an app.

The Code: UndoRedoPaintApp.java

see \Chapter11\4

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

public class UndoRedoPaintApp extends JFrame
{
  protected Vector m_points = new Vector();
  protected PaintCanvas m_canvas = new PaintCanvas(m_points);
  protected UndoManager m_undoManager = new UndoManager();
  protected JButton m_undoButton = new JButton("Undo");
  protected JButton m_redoButton = new JButton("Redo");

  public UndoRedoPaintApp() {
    super("Undo/Redo Demo");

    m_undoButton.setEnabled(false);
    m_redoButton.setEnabled(false);

    JPanel buttonPanel = new JPanel(new GridLayout());
    buttonPanel.add(m_undoButton);
    buttonPanel.add(m_redoButton);

    getContentPane().add(buttonPanel, BorderLayout.NORTH);
    getContentPane().add(m_canvas, BorderLayout.CENTER);

    m_canvas.addMouseListener(new MouseAdapter() {
      public void mousePressed(MouseEvent e) {
        Point point = new Point(e.getX(), e.getY());
        m_points.addElement(point);

        m_undoManager.undoableEditHappened(new UndoableEditEvent(m_canvas,
          new UndoablePaintSquare(point, m_points)));

        m_undoButton.setText(m_undoManager.getUndoPresentationName());
        m_redoButton.setText(m_undoManager.getRedoPresentationName());
        m_undoButton.setEnabled(m_undoManager.canUndo());
        m_redoButton.setEnabled(m_undoManager.canRedo());
        m_canvas.repaint();
      }
    });

    m_undoButton.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        try { m_undoManager.undo(); }
        catch (CannotRedoException cre) { cre.printStackTrace(); }
        m_canvas.repaint();
        m_undoButton.setEnabled(m_undoManager.canUndo());
        m_redoButton.setEnabled(m_undoManager.canRedo());
      }
    });

    m_redoButton.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        try { m_undoManager.redo(); }
        catch (CannotRedoException cre) { cre.printStackTrace(); }
        m_canvas.repaint();
        m_undoButton.setEnabled(m_undoManager.canUndo());
        m_redoButton.setEnabled(m_undoManager.canRedo());
      }
    });

    setSize(400,300);
    setVisible(true);
  }

  public static void main(String argv[]) { 
    new UndoRedoPaintApp(); 
  }
}

// Classes PaintCanvas and UndoablePaintSquare unchanged

// from 11.2.2

Run this example and note that we can have up to 100 squares in the undoable or redoable state at any given time. Also note that when several squares are in the redoable state, adding a new square will kill them, and the redo button will become disabled (indicating that no redos can be performed).

11.2.7 The StateEditable interface

abstract interface javax.swing.undo.StateEditable

The StateEditable interface is intended for use by objects that wish to maintain specific before (pre) and after (post) states. This provides an alternative to managing undos and redos in UndoableEdits. Once a before and after state is defined we can use a StateEdit object to switch between the two states. Two methods must be implemented by StateEditable implementations. storeState() is intended to be used by an object to store its state as a set of key/value pairs in a given Hashtable. Normally this entails storing the name of an object and a copy of that object (unless a primitive is stored). restoreState() is intended to be used by an object to restore its state according to the key/vaue pairs stored in a given Hashtable.

11.2.8 StateEdit

class javax.swing.undo.StateEdit

StateEdit extends AbstractUndoableEdit, and is meant to store the before and after Hashtables of a StateEditable instance. When a StateEdit is instantiated it is passed a StateEditable object, and a protected Hashtable called preState is passed to that StateEditable’s storeState() method. Similarly, when end() is called on a StateEdit, a protected Hashtable called postState is passed to the corresponding StateEditable’s storeState() method. After end() is called, undos and redos toggle the state of the StateEditable between postState and preStat! e by passing the appropr! ia! te Hashtable to that StateEditable’s restoreState() method.

11.2.9 UndoableEditSupport

class javax.swing.undo.UndoableEditSupport

This convenience class is used for managing UndoableEditListeners. We can add and remove an UndoableEditListener with addUndoableEditListener() and removeUndoableEditListener() respectively. UndoableEditSupport maintains an updateLevel property which basically specifies how many times the beginUpdate() method has been called. As long as this value is above 0, UndoableEdits added with the postEdit() method will be stored in a temporary CompoundEdit object without being fired. The endEdit() method decrements the updateLevel property. When updateLevel is 0, any calls to postEdit() will fire the edit passed in, or the CompoundEdit that has been accumulating edits up to that point.

 

Warning: The endUpdate() and beginUpdate() methods may call undoableEditHappened() in each UndoableEditListener, possibly resulting in deadlock if these methods are actually invoked from one of the listeners themselves.

11.2.10 CannotUndoException

class javax.swing.undo.CannotUndoException

This exception is thrown when undo() is invoked on an UndoableEdit that cannot be undone.

11.2.11 CannotRedoException

class javax.swing.undo.CannotRedoException

This exception is thrown when redo() is invoked on an UndoableEdit that cannot be redone.

11.2.12 Using built-in text component undo/redo functionality

All default text component Document models fire UndoableEdits. For PlainDocuments this involves keeping track of text insertions and removals, as well as any structural changes. For StyledDocuments, however, this involves keeping track of a much larger group of changes. Fortunately this work has been built into these document models for us. The following example shows how easy it is to add undo/redo support to text components. Figure 11.8 illustrates.

Figure 11.8 Undo/Redo functionality added to a JTextArea.

The Code: UndoRedoTextApp.java

see \Chapter11\5

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.undo.*;
import javax.swing.event.*;

public class UndoRedoTextApp extends JFrame
{
  protected JTextArea m_editor = new JTextArea();
  protected UndoManager m_undoManager = new UndoManager();
  protected JButton m_undoButton = new JButton("Undo");
  protected JButton m_redoButton = new JButton("Redo");

  public UndoRedoTextApp() {
    super("Undo/Redo Demo");

    m_undoButton.setEnabled(false);
    m_redoButton.setEnabled(false);

    JPanel buttonPanel = new JPanel(new GridLayout());
    buttonPanel.add(m_undoButton);
    buttonPanel.add(m_redoButton);

    JScrollPane scroller = new JScrollPane(m_editor);

    getContentPane().add(buttonPanel, BorderLayout.NORTH);
    getContentPane().add(scroller, BorderLayout.CENTER);

    m_editor.getDocument().addUndoableEditListener(new UndoableEditListener() {
      public void undoableEditHappened(UndoableEditEvent e) {
        m_undoManager.addEdit(e.getEdit());
        updateButtons();
      }
    });

    m_undoButton.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        try { m_undoManager.undo(); }
        catch (CannotRedoException cre) { cre.printStackTrace(); }
        updateButtons();
      }
    });

    m_redoButton.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        try { m_undoManager.redo(); }
        catch (CannotRedoException cre) { cre.printStackTrace(); }
        updateButtons();
      }
    });

    setSize(400,300);
    setVisible(true);
  }

  public void updateButtons() {
    m_undoButton.setText(m_undoManager.getUndoPresentationName());
    m_redoButton.setText(m_undoManager.getRedoPresentationName());
    m_undoButton.setEnabled(m_undoManager.canUndo());
    m_redoButton.setEnabled(m_undoManager.canRedo());
  }

  public static void main(String argv[]) { 
    new UndoRedoTextApp(); 
  }
}

}