Chapter 20. Constructing a Word Processor

In this chapter:

This chapter is devoted to the construction of a fully-functional RTF word processor application. Though Swing’s HTML and RTF capabilities are very powerful, they are not yet complete. RTF support is further along than HTML, and this is why we chose to design our word processor for use with RTF documents. The examples in this chapter demonstrate practical applications of many of the topics covered in chapter 19. The main focus throughout is working with styled text documents, and the techniques discussed here can be applied to any styled text editor.

 

Note: When running the examples in this chapter, do not be surprised when you see a series of ‘unknown keyword’ warnings or exception problems with various Views. You will also see the following message displayed to emphasize the fact that RTF support is still in the works: "Problems encountered: Note that RTF support is still under development."

 

20.1 Word Processor: part I - Introducing RTF

This basic example uses the capabilities of JTextPane and RTFEditorKit to display and edit RTF documents. It demonstrates very basic word processor functionality, opening and saving an RTF file, and serves as the foundation for our word processor application to be expanded upon throughout this chapter.

 

Note: In this series of examples our goal is to demonstrate the most significant available features of advanced text editing in Swing (even if they do not all currently work properly). To avoid losing focus of this goal we intentionally omit several typical word processor features such as an MDI interface, status bar, and prompts to save the current file before closing.

Figure 20.1 JTextPane displaying an RTF document.

<<file figure20-1.gif>>

The Code: WordProcessor.java

see \Chapter20\1

import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.*;

import javax.swing.*;
import javax.swing.text.*;
import javax.swing.event.*;
import javax.swing.border.*;
import javax.swing.text.rtf.*;

public class WordProcessor extends JFrame 
{
  protected JTextPane m_monitor;
  protected StyleContext m_context; 
  protected DefaultStyledDocument m_doc;
  protected RTFEditorKit m_kit;
  protected JFileChooser m_chooser;
  protected SimpleFilter m_rtfFilter;
  protected JToolBar m_toolBar;

  public WordProcessor() {
    super("RTF Word Processor");
    setSize(600, 400);

    // Make sure we install the editor kit before creating
    // the initial document.
    m_monitor = new JTextPane();
    m_kit = new RTFEditorKit();
    m_monitor.setEditorKit(m_kit);
    m_context = new StyleContext();
    m_doc = new DefaultStyledDocument(m_context);
    m_monitor.setDocument(m_doc);

    JScrollPane ps = new JScrollPane(m_monitor);
    getContentPane().add(ps, BorderLayout.CENTER);

    JMenuBar menuBar = createMenuBar();
    setJMenuBar(menuBar);

    m_chooser = new JFileChooser(); 
    m_chooser.setCurrentDirectory(new File("."));
    m_rtfFilter = new SimpleFilter("rtf", "RTF Documents");
    m_chooser.setFileFilter(m_rtfFilter);

    WindowListener wndCloser = new WindowAdapter() {
      public void windowClosing(WindowEvent e) {
        System.exit(0);
      }
    };
    addWindowListener(wndCloser);
        
    setVisible(true);
  }

  protected JMenuBar createMenuBar() {
    JMenuBar menuBar = new JMenuBar();
        
    JMenu mFile = new JMenu("File");
    mFile.setMnemonic('f');

    ImageIcon iconNew = new ImageIcon("file_new.gif");
    Action actionNew = new AbstractAction("New", iconNew) { 
      public void actionPerformed(ActionEvent e) {
        m_doc = new DefaultStyledDocument(m_context);
        m_monitor.setDocument(m_doc);
      }
    };
    JMenuItem item =  mFile.add(actionNew);  
    item.setMnemonic('n');

    ImageIcon iconOpen = new ImageIcon("file_open.gif");
    Action actionOpen = new AbstractAction("Open...", iconOpen) { 
      public void actionPerformed(ActionEvent e) {
        WordProcessor.this.setCursor(
          Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
        Thread runner = new Thread() {
          public void run() {
            if (m_chooser.showOpenDialog(WordProcessor.this) != 
             JFileChooser.APPROVE_OPTION)
              return;
            WordProcessor.this.repaint();
            File fChoosen = m_chooser.getSelectedFile();

            // Recall that text component read/write operations are 
            // thread safe. Its ok to do this in a separate thread.
            try {
              InputStream in = new FileInputStream(fChoosen);
              m_doc = new DefaultStyledDocument(m_context);
              m_kit.read(in, m_doc, 0);
              m_monitor.setDocument(m_doc);
              in.close();
            } 
            catch (Exception ex) {
              ex.printStackTrace();
            }
            WordProcessor.this.setCursor(Cursor.getPredefinedCursor(
              Cursor.DEFAULT_CURSOR));
          }
        };
        runner.start();
      }
    };
    item =  mFile.add(actionOpen);  
    item.setMnemonic('o');

    ImageIcon iconSave = new ImageIcon("file_save.gif");
    Action actionSave = new AbstractAction("Save...", iconSave) {
      public void actionPerformed(ActionEvent e) {
        WordProcessor.this.setCursor(
          Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
        Thread runner = new Thread() {
          public void run() {
            if (m_chooser.showSaveDialog(WordProcessor.this) !=
             JFileChooser.APPROVE_OPTION)
              return;
            WordProcessor.this.repaint();
            File fChoosen = m_chooser.getSelectedFile();

            // Recall that text component read/write operations are
            // thread safe. Its ok to do this in a separate thread.
            try {
              OutputStream out = new FileOutputStream(fChoosen);
              m_kit.write(out, m_doc, 0, m_doc.getLength());
              out.close();
            } 
            catch (Exception ex) {
              ex.printStackTrace();
            }
           
            // Make sure chooser is updated to reflect new file
            m_chooser.rescanCurrentDirectory();
            WordProcessor.this.setCursor(Cursor.getPredefinedCursor(
              Cursor.DEFAULT_CURSOR));
          }
        };
        runner.start();
      }
    };
    item = mFile.add(actionSave);  
    item.setMnemonic('s');

    mFile.addSeparator();

    Action actionExit = new AbstractAction("Exit") { 
      public void actionPerformed(ActionEvent e) {
        System.exit(0);
      }
    };

    item =  mFile.add(actionExit);  
    item.setMnemonic('x');
    menuBar.add(mFile);

    m_toolBar = new JToolBar();
    JButton bNew = new SmallButton(actionNew, "New document");
    m_toolBar.add(bNew);

    JButton bOpen = new SmallButton(actionOpen, "Open RTF document");
    m_toolBar.add(bOpen);
        
    JButton bSave = new SmallButton(actionSave, "Save RTF document");
    m_toolBar.add(bSave);
    
    getContentPane().add(m_toolBar, BorderLayout.NORTH);
       
    return menuBar;
  }

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

// Class SmallButton unchanged from section 12.4

// Class SimpleFilter unchanged from section 14.1.9

Understanding the Code

Class WordProcessor

This class extends JFrame to provide the supporting frame for this example. Several instance variables are declared:

JTextPane m_monitor: text component.

StyleContext m_context: a group of styles and their associated resources for the documents in this example.

DefaultStyledDocument m_doc: current document model.

RTFEditorKit m_kit: editor kit that knows how to read/write RTF documents.

JFileChooser m_chooser: file chooser used to load and save RTF files.

SimpleFilter m_rtfFilter: file filter for ".rtf" files.

JToolBar m_toolBar: toolbar containing open, save, and new document buttons.

The WordProcessor constructor first instantiates our JTextPane and RTFEditorKit, and assigns the editor kit to the text pane (it is important that this is done before any documents are created). Next our StyleContext is instantiated and we build our DefaultStyledDocument with it. The DefaultStyledDocument is then set as our text pane’s current document.

The createMenuBar() method creates a menu bar with a single menu titled "File." Menu items "New," "Open," "Save," and "Exit" are added to the menu. The first three items are duplicated in the toolbar. This code is very similar to the code used in the examples of chapter 12. The important difference is that we use InputStreams and OutputStreams rather than Readers and Writers. The reason for this is that RTF uses 1-byte encoding which is incompatible with the 2-byte encoding used by readers and writers.

 

Warning: An attempt to invoke read() will throw an exception when JTextPane is using an RTFEditorKit.

Running the Code

Use menu or toolbar buttons to open an RTF file (a sample RTF file is provided in the \swing\Chapter20 directory). Save the RTF file and open it in another RTF-aware application (such as Microsoft Word) to verify compatibility.

20.2 Word Processor: part II - Managing fonts

The following example adds the ability to select any font available on the system. This functionality is similar to the "Font" menu used in the examples of chapter 12. The important difference here is that the selected font applies not to the whole text component (the only possible thing with plain text documents), but to the selected region of our RTF styled document text.

Figure 20.2 JTextPane word processor allowing font attribute assignments to selected text.

<<file figure20-1.gif>>

The Code: WordProcessor.java

see \Chapter20\2

import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.*;

import javax.swing.*;
import javax.swing.text.*;
import javax.swing.event.*;
import javax.swing.border.*;
import javax.swing.text.rtf.*;

public class WordProcessor extends JFrame 
{
  protected JTextPane m_monitor;
  protected StyleContext m_context; 
  protected DefaultStyledDocument m_doc;
  protected RTFEditorKit m_kit;
  protected JFileChooser m_chooser;
  protected SimpleFilter m_rtfFilter;
  protected JToolBar m_toolBar;

  protected JComboBox m_cbFonts;
  protected JComboBox m_cbSizes;
  protected SmallToggleButton m_bBold;
  protected SmallToggleButton m_bItalic;

  protected String m_fontName = "";
  protected int m_fontSize = 0;
  protected boolean m_skipUpdate;

  protected int m_xStart = -1;
  protected int m_xFinish = -1;

  public WordProcessor() {

    // Unchanged code from section 20.1

    CaretListener lst = new CaretListener() {
      public void caretUpdate(CaretEvent e) {
        showAttributes(e.getDot());
      }
    };
    m_monitor.addCaretListener(lst);

    FocusListener flst = new FocusListener() { 
      public void focusGained(FocusEvent e) {
        if (m_xStart>=0 && m_xFinish>=0)
          if (m_monitor.getCaretPosition()==m_xStart) {
            m_monitor.setCaretPosition(m_xFinish);
            m_monitor.moveCaretPosition(m_xStart);
          }
          else
            m_monitor.select(m_xStart, m_xFinish);
      }

      public void focusLost(FocusEvent e) {
        m_xStart = m_monitor.getSelectionStart();
        m_xFinish = m_monitor.getSelectionEnd();
      }
    };
    m_monitor.addFocusListener(flst);

    WindowListener wndCloser = new WindowAdapter() {
      public void windowClosing(WindowEvent e) {
        System.exit(0);
      }
    };
    addWindowListener(wndCloser);
        
    showAttributes(0);
    setVisible(true);
  }

  protected JMenuBar createMenuBar() {

    // Unchaged code from section 20.1
 
    // The following line is added to the end of the
    // actionNew and actionOpen actionPerformed() methods:
    // 
    //   showAttributes(0);
    //
    // (see source code; these methods are not shown here
    //  to conserve space)

    // Unchaged code from section 20.1

    GraphicsEnvironment ge = GraphicsEnvironment.
      getLocalGraphicsEnvironment();
    String[] fontNames = ge.getAvailableFontFamilyNames();

    m_toolBar.addSeparator();
    m_cbFonts = new JComboBox(fontNames);
    m_cbFonts.setMaximumSize(m_cbFonts.getPreferredSize());
    m_cbFonts.setEditable(true);

    ActionListener lst = new ActionListener() { 
      public void actionPerformed(ActionEvent e) {
        m_fontName = m_cbFonts.getSelectedItem().toString();
        MutableAttributeSet attr = new SimpleAttributeSet();
        StyleConstants.setFontFamily(attr, m_fontName);
        setAttributeSet(attr);
        m_monitor.grabFocus();
      }
    };
    m_cbFonts.addActionListener(lst);
    m_toolBar.add(m_cbFonts);

    m_toolBar.addSeparator();
    m_cbSizes = new JComboBox(new String[] {"8", "9", "10", 
      "11", "12", "14", "16", "18", "20", "22", "24", "26", 
      "28", "36", "48", "72"});
    m_cbSizes.setMaximumSize(m_cbSizes.getPreferredSize());
    m_cbSizes.setEditable(true);

    lst = new ActionListener() { 
      public void actionPerformed(ActionEvent e) {
        int fontSize = 0;
        try {
          fontSize = Integer.parseInt(m_cbSizes.
            getSelectedItem().toString());
        }
        catch (NumberFormatException ex) { return; }

        m_fontSize = fontSize;
        MutableAttributeSet attr = new SimpleAttributeSet();
        StyleConstants.setFontSize(attr, fontSize);
        setAttributeSet(attr);
        m_monitor.grabFocus();
      }
    };
    m_cbSizes.addActionListener(lst);
    m_toolBar.add(m_cbSizes);

    m_toolBar.addSeparator();
    ImageIcon img1 = new ImageIcon("font_bold1.gif");
    ImageIcon img2 = new ImageIcon("font_bold2.gif");
    m_bBold = new SmallToggleButton(false, img1, img2, 
      "Bold font");
    lst = new ActionListener() { 
      public void actionPerformed(ActionEvent e) {
        MutableAttributeSet attr = new SimpleAttributeSet();
        StyleConstants.setBold(attr, m_bBold.isSelected());
        setAttributeSet(attr);
        m_monitor.grabFocus();
      }
    };
    m_bBold.addActionListener(lst);
    m_toolBar.add(m_bBold);
        
    img1 = new ImageIcon("font_italic1.gif");
    img2 = new ImageIcon("font_italic2.gif");
    m_bItalic = new SmallToggleButton(false, img1, img2, 
      "Italic font");
    lst = new ActionListener() { 
      public void actionPerformed(ActionEvent e) {
        MutableAttributeSet attr = new SimpleAttributeSet();
        StyleConstants.setItalic(attr, m_bItalic.isSelected());
        setAttributeSet(attr);
        m_monitor.grabFocus();
      }
    };
    m_bItalic.addActionListener(lst);
    m_toolBar.add(m_bItalic);

    getContentPane().add(m_toolBar, BorderLayout.NORTH);        

    return menuBar;
  }

  protected void showAttributes(int p) {
    m_skipUpdate = true;
    AttributeSet a = m_doc.getCharacterElement(p).
      getAttributes();
    String name = StyleConstants.getFontFamily(a);
    if (!m_fontName.equals(name)) {
      m_fontName = name;
      m_cbFonts.setSelectedItem(name);
    }
    int size = StyleConstants.getFontSize(a);
    if (m_fontSize != size) {
      m_fontSize = size;
      m_cbSizes.setSelectedItem(Integer.toString(m_fontSize));
    }
    boolean bold = StyleConstants.isBold(a);
    if (bold != m_bBold.isSelected())
      m_bBold.setSelected(bold);
    boolean italic = StyleConstants.isItalic(a);
    if (italic != m_bItalic.isSelected())
      m_bItalic.setSelected(italic);
    m_skipUpdate = false;
  }

  protected void setAttributeSet(AttributeSet attr) {
    if (m_skipUpdate)
      return;
    int xStart = m_monitor.getSelectionStart();
    int xFinish = m_monitor.getSelectionEnd();
    if (!m_monitor.hasFocus()) {
      xStart = m_xStart;
      xFinish = m_xFinish;
    }
    if (xStart != xFinish) {
      m_doc.setCharacterAttributes(xStart, xFinish - xStart, 
        attr, false);
    } 
    else {
      MutableAttributeSet inputAttributes = 
        m_kit.getInputAttributes();
      inputAttributes.addAttributes(attr);
    }
  }

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

// Unchanged code from section 20.1

// Class SmallToggleButton unchanged from section 12.4

Understanding the Code

Class WordProcessor

Several new instance variables have been added:

JComboBox m_cbFonts: toolbar component to select the font name.

JComboBox m_cbSizes: toolbar component to select the font size.

SmallToggleButton m_bBold: toolbar component to select the bold font style.

SmallToggleButton m_bItalic: toolbar component to select the italic font style.

String m_fontName: current font name.

int m_fontSize: current font size.

boolean m_skipUpdate: flag used to skip word processor update (see below).

int m_xStart: used to store the selection start position.

int m_xFinish: used to store the selection end position.

The constructor of the WordProcessor class adds a CaretListener to our m_monitor text pane. The caretUpdate() method of this listener is invoked whenever the caret position is changed. The showAttributes() (see below) will be called in response to update the toolbar components and display the currently selected font attributes.

A FocusListener is also added to our m_monitor component. The two methods of this listener, focusGained() and focusLost(), will be invoked when the editor gains and loses the focus respectively. The purpose of this implementation is to save and restore the starting and end positions of the text selection. The reason we do this is because Swing supports only one text selection at any time throughout an app. This means that if the user selects some text in the editor component to modify it's attributes, and then goes off and makes a text selection in some other component, the original text selection will disappear. This can potentially be very annoying to the user. To fix thi! s problem we save the selection before the editor component loses the focus. When the focus is gained we restore the previously saved selection. We distinguish between two possible situations: when the caret is located at the beginning of the selection and when it is located at the end of the selection. In the first case we position the caret at the end of the stored interval with the setCaretPosition() method, and then move the caret backward to the beginning of the stored interval with the moveCaretPosition() method. The second situation is easily handled using the select() method.

The showAttributes() method is now called prior to the display of a new document or a newly loaded document.

The createMenuBar() method creates new components to manage font properties for the selected text interval. First, the m_cbFonts combo box is used to select the font family name. Unlike the example in chapter 12, which used several pre-defined font names, this example uses all fonts available to the user’s system. A complete list of the available font names can be obtained through the getAvailableFontFamilyNames() method of GraphicsEnvironment (see 2.8). Also note that the editable property of this combobox component is set to true,! so the font name can be both selected from the drop-down list and enetered in by hand.

Once a new font name is selected, it is applied to the selected text through the use of an attached ActionListener. The selected font family name is assigned to a SimpleAttributeSet instance with the StyleConstants.setFontFamily() method. Then our custom setAttributeSet() (see below) is called to modify the attributes of the selected text according to this SimpleAttributeSet.

The m_cbSizes combo box is used to select the font size. It is initiated with a set of pre-defined sizes. The editable property is set to true so the font size can be both selected from the drop-down list and entered by hand. Once a new font size is selected, it is applied to the selected text through the use of an attached ActionListener. The setup is similar to that used for the m_cbFonts component. The StyleConstants.setFontSize() method is used to set the font size. Our custom setAttributeSet()! method is then used to apply this attribute set to the selected text.

The bold and italic properties are managed by two SmallToggleButtons (a custom button class we developed in chapter 12): m_bBold and m_bItalic respectively. These buttons receive ActionListeners which create a SimpleAttributeSet instance with the bold or italic property with StyleConstants.setBold() or StyleConstants.setItalic(). Then our custom setAttributeSet() method is called to apply th! is attribute set.

The showAttributes() method is called to set the state of the toolbar components described above according to the font properties of the text at the given caret position. This method sets the m_skipUpdate flag to true at the beginning and false at the end of it's execution (the purpose of this will be explained soon below). Then an AttributeSet instance corresponding to the character element at the current caret position in the editor’s document is retrieved with the getAttributes() method. The Sty! leConstants.getFontFamily() method is used to retrieve the current font name from this attribute set. If it is not equal to the previously selected font name (stored in the m_fontName instance variable) it is selected in the m_cbFonts combobox. The other toolbar controls are handled in a similar way.

Our setAttributeSet() method is used to assign a given set of attributes to the currently selected text. Note that this method does nothing (simply returns) if the m_skipUpdate flag is set to true. This is done to prevent the backward link with the showAttributes() method. As soon as we assign some value to a combo boc in the showAttributes() method (e.g. font size) this internally triggers a call to the setAttributeSet() method (because ActionListeners attached! to combo boxes are invoked even when selection changes occur programmatically). The purpose of showAttributes() is to simply make sure that the attributes corresponding to the character element at the current text position are accurately reflected in the toolbar components. To prevent the combo box ActionListeners from invoking unnecessary operations we prohibit any text property updates from occuring in setAttributeSet() while the showAttributes() method is being executed (this is the whole purpose of the m_skipUpdate flag).

The setAttributeSet() method first determines the start and end positions of the selected text. If m_monitor currently does not have the focus, the stored bounds, m_xStart and m_xFinish, are used instead. If the selection is not empty (xStart != xFinish), the setCharacterAttributes() method is called to assign the given set of attributes to the selection. Note that this new attribute set does not have to contain a complete set of attributes. It simply replaces only the existing attributes for which it has new values, leaving the rem! ainder unchanged. If the selection is empty, the new attributes are added to the input attributes of the editor kit (recall that StyledEditorKit’s input attributes are those attributes that will be applied to newly inserted text).

Running the Code

Open an existing RTF file and move the cursor to various positions in the text. Note that the text attributes displayed in the toolbar components are updated correctly. Select a portion of text and use the toolbar components to modify the selection’s font attributes. Type a new font name or font size in the editable combobox and press "Enter." This has the same effect as selecting a choice from the drop-down list. Save the RTF file and open it in another RTF-aware application to verify that your changes were saved correctly.

 

Bug Alert! Bold and italic font properties are often not updated on the screen properly, even though they are assigned and saved correctly. We hope that this problem will be fixed in future Swing releases.

 

20.3 Word Processor: part III - Colors and images

Important RTF features we will exploit in this section include the ability to use foreground and background colors and insert images into the text. In this example we show how to add these capabilities to our growing RTF word processor application.

Figure 20.3 JTextPane with diverse font styles, foreground colors, and an embedded image.

<<file figure20-3.gif>>

The Code: WordProcessor.java

see \Chapter20\3

import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.*;

import javax.swing.*;
import javax.swing.text.*;
import javax.swing.event.*;
import javax.swing.border.*;
import javax.swing.text.rtf.*;

public class WordProcessor extends JFrame 
{
  // Unchanged code from section 20.2

  protected SimpleFilter m_jpgFilter;
  protected SimpleFilter m_gifFilter;

  protected ColorMenu m_foreground;
  protected ColorMenu m_background;

  public WordProcessor() {

    // Unchanged code from section 20.2

    m_chooser = new JFileChooser(); 
    m_chooser.setCurrentDirectory(new File("."));
    m_rtfFilter = new SimpleFilter("rtf", "RTF Documents");
    m_chooser.setFileFilter(m_rtfFilter);

    m_gifFilter = new SimpleFilter("gif", "GIF images");
    m_jpgFilter = new SimpleFilter("jpg", "JPG images");
 
    // Unchanged code from section 20.2       
  }

  protected JMenuBar createMenuBar() {

    // Unchanged code from section 20.2

    JMenu mFormat = new JMenu("Format");
    mFormat.setMnemonic('o');

    m_foreground = new ColorMenu("Foreground");
    m_foreground.setColor(m_monitor.getForeground());
    m_foreground.setMnemonic('f');
    lst = new ActionListener() { 
      public void actionPerformed(ActionEvent e) {
        MutableAttributeSet attr = new SimpleAttributeSet();
        StyleConstants.setForeground(attr, m_foreground.getColor());
        setAttributeSet(attr);
      }
    };
    m_foreground.addActionListener(lst);
    mFormat.add(m_foreground);

    MenuListener ml = new MenuListener() {
      public void menuSelected(MenuEvent e) {
        int p = m_monitor.getCaretPosition();
        AttributeSet a = m_doc.getCharacterElement(p).
          getAttributes();
        Color c = StyleConstants.getForeground(a);
        m_foreground.setColor(c);
      }

      public void menuDeselected(MenuEvent e) {}

      public void menuCanceled(MenuEvent e) {}
    };
    m_foreground.addMenuListener(ml);

    // Bug Alert! JEditorPane background color 
    // doesn't work as of Java 2 FCS.
    m_background = new ColorMenu("Background");
    m_background.setColor(m_monitor.getBackground());
    m_background.setMnemonic('b');
    lst = new ActionListener()  { 
      public void actionPerformed(ActionEvent e) {
        MutableAttributeSet attr = new SimpleAttributeSet();
        StyleConstants.setBackground(attr, m_background.getColor());
        setAttributeSet(attr);
      }
    };
    m_background.addActionListener(lst);
    mFormat.add(m_background);

    ml = new MenuListener() {
      public void menuSelected(MenuEvent e) {
        int p = m_monitor.getCaretPosition();
        AttributeSet a = m_doc.getCharacterElement(p).
          getAttributes();
        Color c = StyleConstants.getBackground(a);
        m_background.setColor(c);
      }

      public void menuDeselected(MenuEvent e) {}

      public void menuCanceled(MenuEvent e) {}
    };
    m_background.addMenuListener(ml);

    // Bug Alert! Images do not get saved.
    mFormat.addSeparator();
    item = new JMenuItem("Insert Image");  
    item.setMnemonic('i');
    lst = new ActionListener() { 
      public void actionPerformed(ActionEvent e) {
        m_chooser.addChoosableFileFilter(m_gifFilter);
        m_chooser.addChoosableFileFilter(m_jpgFilter);
        m_chooser.setFileFilter(m_gifFilter);
        m_chooser.removeChoosableFileFilter(m_rtfFilter);
        Thread runner = new Thread() {
          public void run() {
            if (m_chooser.showOpenDialog(WordProcessor.this) != 
            JFileChooser.APPROVE_OPTION)
              return;
            WordProcessor.this.repaint();
            File fChoosen = m_chooser.getSelectedFile();
            ImageIcon icon = new ImageIcon(fChoosen.getPath());
            int w = icon.getIconWidth();
            int h = icon.getIconHeight();
            if (w<=0 || h<=0) {
              JOptionPane.showMessageDialog(WordProcessor.this, 
                "Error reading image file\n"+
                fChoosen.getPath(), "Warning", 
                JOptionPane.WARNING_MESSAGE);
                return;
            }
            MutableAttributeSet attr = new SimpleAttributeSet();
            StyleConstants.setIcon(attr, icon);
            int p = m_monitor.getCaretPosition();
            try {
              m_doc.insertString(p, " ", attr);
            }
            catch (BadLocationException ex) {}
    
            // Its ok to do this outside of the event-dispatching
            // thread because the chooser is not visible here.
            m_chooser.addChoosableFileFilter(m_rtfFilter);
            m_chooser.setFileFilter(m_rtfFilter);
            m_chooser.removeChoosableFileFilter(m_gifFilter);
            m_chooser.removeChoosableFileFilter(m_jpgFilter);
          }
        };
        runner.start();
      }
    };
    item.addActionListener(lst);
    mFormat.add(item);

    menuBar.add(mFormat);

    getContentPane().add(m_toolBar, BorderLayout.NORTH);   
 
    return menuBar;
  }

  // Unchanged code from section 20.2
}

// Unchanged code from section 20.2

// Class ColorMenu unchanged from section 12.5

Understanding the Code

Class WordProcessor

Four new instance variables have been added:

SimpleFilter m_jpgFilter: Used for JPEG image selection with JFileChooser (see chapter 14 for more about SimpleFilter).

SimpleFilter m_gifFilter: Used for GIF image selection with JFileChooser.

ColorMenu m_foreground: used to choose the selected text foreground color.

ColorMenu m_background: used to choose the selected text background color.

The ColorMenu class was constructed and discussed in chapter 12, and is used here without modification. This class represents a custom menu component used to select a color from a collection of 64 pre-defined colors. To deploy this component in our application we add a menu titled "Format" with menu items titled "Foreground" and "Background." We also add, after a menu separator, the menu item "Insert Image" (see below).

The ColorMenu m_foreground receives an ActionListener which retrieves the selected color with the getColor() method and then applies it to the selected text by making calls to StyleConstants.setForeground() and setAttributeSet() methods, similar to how we assigned new font attributes in the previous example. In order to maintain consistency we need to update the ColorMenu component prior to displaying it to make the initial color selection consistent with the text foreground at the cursor position. For this purpose we add a MenuListener to the m_foreground component. The menuSelected() method will be called prior to menu selection. Similar to the showAttributes() method discussed above, this code retrieves an AttributeSet instance corresponding to the current caret location and determines the selected foreground color with getForeground(). Then this color is passed to the ColorMenu component for use as the selection.

The m_background ColorMenu works similar to m_foreground, but manages selected text background color. Note that this feature doesn't work with the current RTF API release: it is neither displayed nor saved in the file. The background color menu is added to our word processor for the sake of completeness.

The "Insert Image" menu item receives an ActionListener which uses our JFileChooser to select an image file. If an image file is successfully selected and read, we create a MutableAttributeSet instance and pass our image to it with StyleConstants.setIcon(). Then we insert a dummy single space character with that icon attribute using insertString(). Note that all this occurs in a separate thread to avoid the possibility of clogging the event-dispathing thread.

Running the Code

Open an existing RTF file, select a portion of text and use the custom color menu component to modify it's foreground. Save the RTF file and open it in another RTF-aware application to verify that your changes have been saved correctly. Try using the "Insert Image" menu to bring up a file chooser and select an image for insertion.

 

Bug Alert! Unfortunately embedded images are neither saved to file or read from an existing RTF document. We hope that this problem/limitation will be fixed soon in a future release.

 

20.4 Word Processor: part IV - Working with styles

Using Styles to manage a set of attributes as a single named entity can greatly simplify text editing. The user only has to apply a known style to a selected region of text rather than selecting all appropriate text attributes from the provided toolbar components. By adding a combo box allowing the choice of styles, we can not only save the user time and effort, but we can also provide more uniform text formatting throughout the resulting document (or potentially set of documents). In this section we'll add style management to our word processor. We’ll also show how it is possible to create a new style, modify an existing style, or reapply a style to modified text.

Figure 20.4 RTF word processor application with Styles management.

<<file figure20-4.gif>>

The Code: WordProcessor.java

see \Chapter20\4

import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.*;

import javax.swing.*;
import javax.swing.text.*;
import javax.swing.event.*;
import javax.swing.border.*;
import javax.swing.text.rtf.*;

public class WordProcessor extends JFrame 
{
  // Unchanged code from section 20.3

  protected JComboBox m_cbStyles;

  public WordProcessor() {
    // Unchanged code from section 20.3

    showAttributes(0);
    showStyles();
    setVisible(true);
  }

  protected JMenuBar createMenuBar() {
    // Unchanged code from section 20.3

    // The following line is added to the end of the
    // actionNew and actionOpen actionPerformed() methods:
    // 
    //   showStyles();
    //
    // (see source code; these methods are not shown here
    //  to conserve space)
        
    // Unchanged code from section 20.3

    JMenu mFormat = new JMenu("Format");
    mFormat.setMnemonic('o');

    JMenu mStyle = new JMenu("Style");
    mStyle.setMnemonic('s');
    mFormat.add(mStyle);

    item = new JMenuItem("Update");
    item.setMnemonic('u');
    lst = new ActionListener() { 
      public void actionPerformed(ActionEvent e) {
        String name = (String)m_cbStyles.getSelectedItem();
        Style style = m_doc.getStyle(name);
        int p = m_monitor.getCaretPosition();
        AttributeSet a = m_doc.getCharacterElement(p).
          getAttributes();
        style.addAttributes(a);
        m_monitor.repaint();
      }
    };
    item.addActionListener(lst);
    mStyle.add(item);

    item = new JMenuItem("Reapply");
    item.setMnemonic('r');
    lst = new ActionListener() { 
      public void actionPerformed(ActionEvent e) {
        String name = (String)m_cbStyles.getSelectedItem();
        Style style = m_doc.getStyle(name);
        setAttributeSet(style);
      }
    };
    item.addActionListener(lst);
    mStyle.add(item);

    mFormat.addSeparator();

    // Unchanged code from section 20.3

    menuBar.add(mFormat);

    m_toolBar.addSeparator();
    m_cbStyles = new JComboBox();
    m_cbStyles.setMaximumSize(m_cbStyles.getPreferredSize());
    m_cbStyles.setEditable(true);
    m_toolBar.add(m_cbStyles);
        
    lst = new ActionListener() { 
      public void actionPerformed(ActionEvent e) {
        if (m_skipUpdate || m_cbStyles.getItemCount()==0)
          return;
        String name = (String)m_cbStyles.getSelectedItem();
        int index = m_cbStyles.getSelectedIndex();
        int p = m_monitor.getCaretPosition();
                
        // New name entered
        if (index == -1) {
          m_cbStyles.addItem(name);
          Style style = m_doc.addStyle(name, null);
          AttributeSet a = m_doc.getCharacterElement(p).
            getAttributes();
          style.addAttributes(a);
          return;
        }

        // Apply the selected style
        Style currStyle = m_doc.getLogicalStyle(p);
        if (!currStyle.getName().equals(name)) {
          Style style = m_doc.getStyle(name);
          setAttributeSet(style);
        }
      }
    };
    m_cbStyles.addActionListener(lst);
        
    getContentPane().add(m_toolBar, BorderLayout.NORTH);

    return menuBar;
  }

  protected void showAttributes(int p) {
    // Unchanged code from section 20.2

    Style style = m_doc.getLogicalStyle(p);
    name = style.getName();
    m_cbStyles.setSelectedItem(name);

    m_skipUpdate = false;
  }

  // Unchanged code from section 20.3

  protected void showStyles() {
    m_skipUpdate = true;
    if (m_cbStyles.getItemCount() > 0)
      m_cbStyles.removeAllItems();
    Enumeration en = m_doc.getStyleNames();
    while (en.hasMoreElements()) {
      String str = en.nextElement().toString();
      m_cbStyles.addItem(str);
    }
    m_skipUpdate = false;
  }

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

// Unchanged code from section 20.3

Understanding the Code

Class WordProcessor

One new instance variable has been added:

JComboBox m_cbStyles: toolbar component to manage styles.

Note that a new custom method showStyles() (see below) is now called after creating a new document or after loading an existing one.

The createMenuBar() method creates a new menu with two new menu items for updating and reapplying styles, and a new combo box for style selection. The editable styles combobox, m_cbStyles, will hold a list of styles declared in the current document (we will see how this component is populated below). It receives an ActionListener which checks whether the currently selected style name is present among the existing styles. If not, we add it to the drop-down list and retrieve a new Style instance for the selected name using StyledDocument’s addStyle() method. This new Style instance is associated with the text attributes of the character element at the current caret position. Otherwise, if the given style name is known already, we retrieve the selected style using StyledDocument’s getStyle() method and apply it to the selected text by passing it to our custom setAttributeSet() method (as we did in previous examples when assigning text attributes).

An ambiguous situation occurs when the user selects a style for text which already has the same style, but whose attributes have been modified. The user may either want to update the selected style using the selected text as a base, or reapply the existing style to the selected text. To resolve this situation we need to ask the user what to do. We chose to add two menu items which allow the user to either update or reapply the current selection.

 

Note: In ambiguous situations such as this, making the decision to allow users to choose between two options, or enforcing a single behavior, can be a tough one to make. In general, the less experienced the target audience is, the less choices that audience should need to become familiar with. In this case we would suggest that a selected style override all attributes of the selected text.

The menu items to perform these tasks are titled "Update" and "Reapply," and are grouped into the "Style" menu. The "Style" menu is added to the "Format" menu. The "Update" menu item receives an ActionListener which retrieves the text attributes of the character element at the current caret position, and assigns them to the selected style. The "Reapply" menu item receives an ActionListener which applies the selected style to the selected text (one might argue that this menu item would be more appropriately titled "Apply" -- the implications are ambiguous either way).

Our showAttributes() method receives additional code to manage the new styles combobox, m_cbStyles, when the caret moves through the document. It retrieves the style corresponding to the current caret position with StyledDocument’s getLogicalStyle() method, and selects the appropriate entry in the combobox.

The new showStyles() method is called to populate the m_cbStyles combobox with the style names from a newly created or loaded document. First it removes the current content of the combobox if it is not empty (another work around due to the fact that if you call removeAllItems() on an empty JComboBox, an exception will be thrown). An Enumeration of style names is then retrieved with StyledDocument’s getStyleNames() method, and these names are added to the combobo! x.

Running the Code

Open an existing RTF file, and note how the styles combobox is populated by the style names defined in this document. Verify that the selected style is automatically updated while the caret moves through the document. Select a portion of text and select a different style from the styles combobox. Note how all text properties are updated according to the new style.

Try selecting a portion of text and modifying its attributes (for instance, foreground color). Type a new name in the styles combobox and press Enter. This will create a new style which can be applied to any other document text.

 

Note: New styles will not be saved along with an RTF document under the current version of RTFEditorKit.

Try modifying an attribute of a selected region of text (e.g. the font size) and select the "Style|Update" menu item. This will update the style to incorporate the newly selected attributes. Apply the modified style to another portion of text and verify that it applies according to the updated style.

 

Note: When a style is updated, any regions of text that had been applied with this style do not automatically get updated accordingly. This is another ambiguity that must be considered, depending on what the user expects and what level or experience the target audience has. In this case we assume that the user only wants selected text to be affected by a style update.

Now try modifying some attributes of a portion of selected text and then select the "Style|Reapply" menu item. This will restore the original text attributes associated with the appropriate style.

 

Note: Recall that we are using one StyleContext instance, m_context, for all documents. This object collects all document styles. These styles are always available when a new document is created or loaded. We might develop a document template mechanism by serializing this StyleContext instance into a file and restoring it with the appropriate document.

 

20.5 Word Processor: part V - Clipboard and Undo/Redo

Clipboard and undo/redo operations have become common and necessary components of all modern text editing environments. We have discussed these features in chapters 11 and 19, and in this section we show how to integrate them into our RTF word processor.

Figure 20.5 RTF word processor with undo/redo and clipboard functionality.

<<file figure20-5.gif>>

The Code: WordProcessor.java

see \Chapter20\5

import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.*;

import javax.swing.*;
import javax.swing.text.*;
import javax.swing.event.*;
import javax.swing.border.*;
import javax.swing.text.rtf.*;
import javax.swing.undo.*;

public class WordProcessor extends JFrame 
{>    
  // Unchanged code from section 20.4

  protected UndoManager m_undo = new UndoManager();
  protected Action m_undoAction;
  protected Action m_redoAction;

  public WordProcessor() {
    // Unchanged code from section 20.4

    showAttributes(0);
    showStyles();
    m_doc.addUndoableEditListener(new Undoer());
    setVisible(true);
  }

  protected JMenuBar createMenuBar() {
    // The following line is added to the end of the
    // actionNew and actionOpen actionPerformed() methods:
    // 
    //   m_doc.addUndoableEditListener(new Undoer());
    //
    // (see source code; these methods are not shown here
    //  to conserve space)
        
    // Unchanged code from section 20.4

    JButton bSave = new SmallButton(actionSave, "Save RTF document");
    m_toolBar.add(bSave);

    JMenu mEdit = new JMenu("Edit");
    mEdit.setMnemonic('e');

    Action action = new AbstractAction("Copy", 
     new ImageIcon("edit_copy.gif")) 
    { 
      public void actionPerformed(ActionEvent e) {
        m_monitor.copy();
      }
    };
    item = mEdit.add(action);  
    item.setMnemonic('c');
    item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_C, 
      KeyEvent.CTRL_MASK));

    action = new AbstractAction("Cut", 
     new ImageIcon("edit_cut.gif")) 
    { 
      public void actionPerformed(ActionEvent e) {
        m_monitor.cut();
      }
    };
    item = mEdit.add(action);  
    item.setMnemonic('t');
    item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_X, 
      KeyEvent.CTRL_MASK));

    action = new AbstractAction("Paste", 
     new ImageIcon("edit_paste.gif")) 
    { 
      public void actionPerformed(ActionEvent e) {
        m_monitor.paste();
      }
    };
    item = mEdit.add(action);  
    item.setMnemonic('p');
    item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_V, 
      KeyEvent.CTRL_MASK));

    mEdit.addSeparator();

    m_undoAction = new AbstractAction("Undo", 
     new ImageIcon("edit_undo.gif")) 
    { 
      public void actionPerformed(ActionEvent e) {
        try {
          m_undo.undo();
        } 
        catch (CannotUndoException ex) {
          System.err.println("Unable to undo: " + ex);
        }
        updateUndo();
      }
    };
    item = mEdit.add(m_undoAction);  
    item.setMnemonic('u');
    item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Z, 
      KeyEvent.CTRL_MASK));

    m_redoAction = new AbstractAction("Redo", 
     new ImageIcon("edit_redo.gif")) 
    { 
      public void actionPerformed(ActionEvent e) {
        try {
          m_undo.redo();
        } 
        catch (CannotRedoException ex) {
          System.err.println("Unable to redo: " + ex);
        }
        updateUndo();
      }
    };
    item =  mEdit.add(m_redoAction);  
    item.setMnemonic('r');
    item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Y, 
      KeyEvent.CTRL_MASK));

    menuBar.add(mEdit);

    GraphicsEnvironment ge = GraphicsEnvironment.
      getLocalGraphicsEnvironment();
    String[] fontNames = ge.getAvailableFontFamilyNames();

    // Unchanged code from section 20.4

    return menuBar;
  }

  // Unchanged code from section 20.4

  protected void updateUndo() {
    if(m_undo.canUndo()) {
      m_undoAction.setEnabled(true);
      m_undoAction.putValue(Action.NAME, 
      m_undo.getUndoPresentationName());
    }
    else {
      m_undoAction.setEnabled(false);
      m_undoAction.putValue(Action.NAME, "Undo");
    }
    if(m_undo.canRedo()) {
      m_redoAction.setEnabled(true);
      m_redoAction.putValue(Action.NAME, 
      m_undo.getRedoPresentationName());
    }
    else {
      m_redoAction.setEnabled(false);
      m_redoAction.putValue(Action.NAME, "Redo");
    }
  }

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

  class Undoer implements UndoableEditListener
  {
    public Undoer() {
      m_undo.die();
      updateUndo();
    }

    public void undoableEditHappened(UndoableEditEvent e) {
      UndoableEdit edit = e.getEdit();
      m_undo.addEdit(e.getEdit());
      updateUndo();
    }
  }
}

// Unchanged code from section 20.4

Understanding the Code

Class WordProcessor

We now import the javax.swing.undo package and add three new instance variables:

UndoManager m_undo: used to manage undo/redo operations.

Action m_undoAction: used for a menu item/action to perform undo operations.

Action m_redoAction: used for a menu item/action to perform redo operations.

Note that a new Undoer instance (see below) is now added as an UndoableEditListener to all newly created or loaded documents.

The createMenuBar() method now creates a menu titled "Edit" (which traditionally follows the "File" menu) containing menu items titled "Copy," "Cut," "Paste," "Undo," and "Redo." The first three items merely trigger calls to the copy(), cut(), and paste() methods of our m_monitor text pane. These methods perform clipboard operations using plain text without any attributes. They are available when the editor has the current focus and the appropriate keyboard accelerator is pressed. These items are added to our "Edit" menu to ! provide a convenient and informative interface.

The "Undo" menu item is created from an AbstractAction whose actionPerformed() method first invokes undo() on the UndoManager, and then invokes our custom updateUndo() method to update our undo/redo menu items appropriately. Similarly, the "Redo" menu item is created from an AbstractAction which invokes redo() on the UndoManager, and then calls our updateUndo() method.

The updateUndo() method enables or disables the undo and redo menu items, and updates their names according to the operation which can be undone/redone (if any). If the UndoManager’s canUndo() method returns true, the m_undoAction is enabled and its name is set to the string returned by getUndoPresentationName(). Otherwise it is disabled and its name is set to "Undo." The "Redo" menu item is handled similarly.

Class WordProcessor.Undoer

This inner class implements the UndoableEditListener interface to receive notifications about undoable operations. The undoableEditHappened() method receives UndoableEditEvents, retrieves their encapsulated UndoableEdit instances, and passes them to the UndoManager. Our updateUndo() method is also invoked to update the undo/redo menu items appropriately.

Running the Code

Open an existing RTF file and verify that copy, cut, and paste clipboard operations transfer plain text successfully. Make some changes to the textual content or styles and note that the title of the "Undo" menu item is updated. Select this menu item, or press its keyboard accelerator (Ctrl-Z) to undo a series of changes. This will enable the "Redo" menu item. Use this menu item or press its keyboard accelerator (Ctrl-Y) to redo a series of changes.

20.6 Word Processor: part VI - Advanced font management

In section 20.2 we used toolbar components to change manipulate font properties. This is useful for making a quick modification without leaving the main application frame, and is typical for word processor applications. However, all serious editor applications also provide a dialog for the editing of all available font properties from one location. In the following example we'll show how to create such a dialog, which includes components to select various font properties and preview the result.

Figure 20.6 RTF word processor with custom font properties and preview dialog.

<<file figure20-6.gif>>

Figure 20.7 Font dialog wth custom list and list cell renderer for foreground color selection.

<<file figure20-7.gif>>

The Code: WordProcessor.java

see \Chapter20\6

import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.*;

import javax.swing.*;
import javax.swing.text.*;
import javax.swing.event.*;
import javax.swing.border.*;
import javax.swing.text.rtf.*;
import javax.swing.undo.*;

public class WordProcessor extends JFrame 
{
  // Unchanged code from section 20.5

  protected String[] m_fontNames;
  protected String[] m_fontSizes;

  protected FontDialog m_fontDialog;

  protected JMenuBar createMenuBar() {

    // Unchanged code from section 20.5

    GraphicsEnvironment ge = GraphicsEnvironment.
      getLocalGraphicsEnvironment();
    m_fontNames = ge.getAvailableFontFamilyNames();

    m_toolBar.addSeparator();
    m_cbFonts = new JComboBox(m_fontNames);
    m_cbFonts.setMaximumSize(m_cbFonts.getPreferredSize());
    m_cbFonts.setEditable(true);

    // Unchanged code from section 20.5

    m_toolBar.addSeparator();
    m_fontSizes = new String[] {"8", "9", "10", "11", "12", "14",
      "16", "18", "20", "22", "24", "26", "28", "36", "48", "72"};
    m_cbSizes = new JComboBox(m_fontSizes);
    m_cbSizes.setMaximumSize(m_cbSizes.getPreferredSize());
    m_cbSizes.setEditable(true);

    m_fontDialog = new FontDialog(this, m_fontNames, m_fontSizes);

    // Unchanged code from section 20.5

    JMenu mFormat = new JMenu("Format");
    mFormat.setMnemonic('o');

    item = new JMenuItem("Font...");
    item.setMnemonic('o');
    lst = new ActionListener() { 
      public void actionPerformed(ActionEvent e) {
        WordProcessor.this.repaint();
        AttributeSet a = m_doc.getCharacterElement(
          m_monitor.getCaretPosition()).getAttributes();
        m_fontDialog.setAttributes(a);

        Dimension d1 = m_fontDialog.getSize();
        Dimension d2 = WordProcessor.this.getSize();
        int x = Math.max((d2.width-d1.width)/2, 0);
        int y = Math.max((d2.height-d1.height)/2, 0);
        m_fontDialog.setBounds(x + WordProcessor.this.getX(),
          y + WordProcessor.this.getY(), d1.width, d1.height);

        m_fontDialog.show();
        if (m_fontDialog.getOption()==JOptionPane.OK_OPTION) {
          setAttributeSet(m_fontDialog.getAttributes());
          showAttributes(m_monitor.getCaretPosition());
        }
      }
    };
    item.addActionListener(lst);
    mFormat.add(item);

    mFormat.addSeparator();

    // Unchanged code from section 20.5
     
    return menuBar;
  }

  // Unchanged code from section 20.5
}

 // Unchanged code from section 20.5

class FontDialog extends JDialog
{
  protected int m_option = JOptionPane.CLOSED_OPTION;
  protected OpenList m_lstFontName;
  protected OpenList m_lstFontSize;
  protected MutableAttributeSet m_attributes;
  protected JCheckBox m_chkBold;
  protected JCheckBox m_chkItalic;
  protected JCheckBox m_chkUnderline;
    
  protected JCheckBox m_chkStrikethrough;
  protected JCheckBox m_chkSubscript;
  protected JCheckBox m_chkSuperscript;
    
  protected JComboBox m_cbColor;
  protected JLabel m_preview;

  public FontDialog(JFrame parent, 
    String[] names, String[] sizes)
  {
    super(parent, "Font", true);
    getContentPane().setLayout(new BoxLayout(getContentPane(), 
      BoxLayout.Y_AXIS));

    JPanel p = new JPanel(new GridLayout(1, 2, 10, 2));
    p.setBorder(new TitledBorder(new EtchedBorder(), "Font"));
    m_lstFontName = new OpenList(names, "Name:");
    p.add(m_lstFontName);

    m_lstFontSize = new OpenList(sizes, "Size:");
    p.add(m_lstFontSize);
    getContentPane().add(p);

    p = new JPanel(new GridLayout(2, 3, 10, 5));
    p.setBorder(new TitledBorder(new EtchedBorder(), "Effects"));
    m_chkBold = new JCheckBox("Bold");
    p.add(m_chkBold);
    m_chkItalic = new JCheckBox("Italic");
    p.add(m_chkItalic);
    m_chkUnderline = new JCheckBox("Underline");
    p.add(m_chkUnderline);
    m_chkStrikethrough = new JCheckBox("Strikeout");
    p.add(m_chkStrikethrough);
    m_chkSubscript = new JCheckBox("Subscript");
    p.add(m_chkSubscript);
    m_chkSuperscript = new JCheckBox("Superscript");
    p.add(m_chkSuperscript);
    getContentPane().add(p);

    getContentPane().add(Box.createVerticalStrut(5));
    p = new JPanel();
    p.setLayout(new BoxLayout(p, BoxLayout.X_AXIS));
    p.add(Box.createHorizontalStrut(10));
    p.add(new JLabel("Color:"));
    p.add(Box.createHorizontalStrut(20));
    m_cbColor = new JComboBox();

    int[] values = new int[] { 0, 128, 192, 255 };
    for (int r=0; r<values.length; r++) {
      for (int g=0; g<values.length; g++) {
        for (int b=0; b<values.length; b++) {
          Color c = new Color(values[r], values[g], values[b]);
          m_cbColor.addItem(c);
        }
      }
    }

    m_cbColor.setRenderer(new ColorComboRenderer());
    p.add(m_cbColor);
    p.add(Box.createHorizontalStrut(10));
    getContentPane().add(p);

    p = new JPanel(new BorderLayout());
    p.setBorder(new TitledBorder(new EtchedBorder(), "Preview"));
    m_preview = new JLabel("Preview Font", JLabel.CENTER);
    m_preview.setBackground(Color.white);
    m_preview.setForeground(Color.black);
    m_preview.setOpaque(true);
    m_preview.setBorder(new LineBorder(Color.black));
    m_preview.setPreferredSize(new Dimension(120, 40));
    p.add(m_preview, BorderLayout.CENTER);
    getContentPane().add(p);

    p = new JPanel(new FlowLayout());
    JPanel p1 = new JPanel(new GridLayout(1, 2, 10, 2));
    JButton btOK = new JButton("OK");
    ActionListener lst = new ActionListener() { 
      public void actionPerformed(ActionEvent e) {
        m_option = JOptionPane.OK_OPTION;
        setVisible(false);
      }
    };
    btOK.addActionListener(lst);
    p1.add(btOK);

    JButton btCancel = new JButton("Cancel");
    lst = new ActionListener() { 
      public void actionPerformed(ActionEvent e) {
        m_option = JOptionPane.CANCEL_OPTION;
        setVisible(false);
      }
    };
    btCancel.addActionListener(lst);
    p1.add(btCancel);
    p.add(p1);
    getContentPane().add(p);

    pack();
    setResizable(false);
    Dimension d1 = getSize();
    Dimension d2 = parent.getSize();
    int x = Math.max((d2.width-d1.width)/2, 0);
    int y = Math.max((d2.height-d1.height)/2, 0);
    setBounds(x, y, d1.width, d1.height);

    ListSelectionListener lsel = new ListSelectionListener() {
      public void valueChanged(ListSelectionEvent e) {
        updatePreview();
      }
    };
    m_lstFontName.addListSelectionListener(lsel);
    m_lstFontSize.addListSelectionListener(lsel);

    lst = new ActionListener() { 
      public void actionPerformed(ActionEvent e) {
        updatePreview();
      }
    };
    m_chkBold.addActionListener(lst);
    m_chkItalic.addActionListener(lst);
    m_cbColor.addActionListener(lst);
  }

  public void setAttributes(AttributeSet a) {
    m_attributes = new SimpleAttributeSet(a);
    String name = StyleConstants.getFontFamily(a);
    m_lstFontName.setSelected(name);
    int size = StyleConstants.getFontSize(a);
    m_lstFontSize.setSelectedInt(size);
    m_chkBold.setSelected(StyleConstants.isBold(a));
    m_chkItalic.setSelected(StyleConstants.isItalic(a));
    m_chkUnderline.setSelected(StyleConstants.isUnderline(a));
    m_chkStrikethrough.setSelected(
      StyleConstants.isStrikeThrough(a));
    m_chkSubscript.setSelected(StyleConstants.isSubscript(a));
    m_chkSuperscript.setSelected(StyleConstants.isSuperscript(a));
    m_cbColor.setSelectedItem(StyleConstants.getForeground(a));
    updatePreview();
  }

  public AttributeSet getAttributes() {
    if (m_attributes == null)
      return null;
    StyleConstants.setFontFamily(m_attributes, 
      m_lstFontName.getSelected());
    StyleConstants.setFontSize(m_attributes, 
      m_lstFontSize.getSelectedInt());
    StyleConstants.setBold(m_attributes, 
      m_chkBold.isSelected());
    StyleConstants.setItalic(m_attributes, 
      m_chkItalic.isSelected());
    StyleConstants.setUnderline(m_attributes, 
      m_chkUnderline.isSelected());
    StyleConstants.setStrikeThrough(m_attributes, 
      m_chkStrikethrough.isSelected());
    StyleConstants.setSubscript(m_attributes, 
      m_chkSubscript.isSelected());
    StyleConstants.setSuperscript(m_attributes, 
      m_chkSuperscript.isSelected());
    StyleConstants.setForeground(m_attributes, 
      (Color)m_cbColor.getSelectedItem());
    return m_attributes;
  }

  public int getOption() { return m_option; }

  protected void updatePreview() {
    String name = m_lstFontName.getSelected();
    int size = m_lstFontSize.getSelectedInt();
    if (size <= 0)
      return;
    int style = Font.PLAIN;
    if (m_chkBold.isSelected())
      style |= Font.BOLD;
    if (m_chkItalic.isSelected())
      style |= Font.ITALIC;

    // Bug Alert! This doesn't work if only style is changed.
    Font fn = new Font(name, style, size);
    m_preview.setFont(fn);

    Color c = (Color)m_cbColor.getSelectedItem();
    m_preview.setForeground(c);
    m_preview.repaint();
  }
}

class OpenList extends JPanel implements ListSelectionListener, ActionListener
{
  protected JLabel m_title;
  protected JTextField m_text;
  protected JList m_list;
  protected JScrollPane m_scroll;

  public OpenList(String[] data, String title) {
    setLayout(null);
    m_title = new JLabel(title, JLabel.LEFT);
    add(m_title);
    m_text = new JTextField();
    m_text.addActionListener(this);
    add(m_text);
    m_list = new JList(data);
    m_list.setVisibleRowCount(4);
    m_list.addListSelectionListener(this);
    m_scroll = new JScrollPane(m_list);
    add(m_scroll);
  }

  public void setSelected(String sel) {
    m_list.setSelectedValue(sel, true);
    m_text.setText(sel);
  }

  public String getSelected() { return m_text.getText(); }

  public void setSelectedInt(int value) {
    setSelected(Integer.toString(value));
  }

  public int getSelectedInt() {
    try { 
      return Integer.parseInt(getSelected());
    }
    catch (NumberFormatException ex) { return -1; }
  }

  public void valueChanged(ListSelectionEvent e) {
    Object obj = m_list.getSelectedValue();
    if (obj != null)
      m_text.setText(obj.toString());
  }

  public void actionPerformed(ActionEvent e) {
    ListModel model = m_list.getModel();
    String key = m_text.getText().toLowerCase();
    for (int k=0; k<model.getSize(); k++) {
      String data = (String)model.getElementAt(k);
      if (data.toLowerCase().startsWith(key)) {
        m_list.setSelectedValue(data, true);
        break;
      }
    }
  }

  public void addListSelectionListener(ListSelectionListener lst) {
    m_list.addListSelectionListener(lst);
  }

  public Dimension getPreferredSize() {
    Insets ins = getInsets();
    Dimension d1 = m_title.getPreferredSize();
    Dimension d2 = m_text.getPreferredSize();
    Dimension d3 = m_scroll.getPreferredSize();
    int w = Math.max(Math.max(d1.width, d2.width), d3.width);
    int h = d1.height + d2.height + d3.height;
    return new Dimension(w+ins.left+ins.right, 
      h+ins.top+ins.bottom);
  }

  public Dimension getMaximumSize() {
    Insets ins = getInsets();
    Dimension d1 = m_title.getMaximumSize();
    Dimension d2 = m_text.getMaximumSize();
    Dimension d3 = m_scroll.getMaximumSize();
    int w = Math.max(Math.max(d1.width, d2.width), d3.width);
    int h = d1.height + d2.height + d3.height;
    return new Dimension(w+ins.left+ins.right, 
      h+ins.top+ins.bottom);
  }

  public Dimension getMinimumSize() {
    Insets ins = getInsets();
    Dimension d1 = m_title.getMinimumSize();
    Dimension d2 = m_text.getMinimumSize();
    Dimension d3 = m_scroll.getMinimumSize();
    int w = Math.max(Math.max(d1.width, d2.width), d3.width);
    int h = d1.height + d2.height + d3.height;
    return new Dimension(w+ins.left+ins.right, 
      h+ins.top+ins.bottom);
  }

  public void doLayout() {
    Insets ins = getInsets();
    Dimension d = getSize();
    int x = ins.left;
    int y = ins.top;
    int w = d.width-ins.left-ins.right;
    int h = d.height-ins.top-ins.bottom;

    Dimension d1 = m_title.getPreferredSize();
    m_title.setBounds(x, y, w, d1.height);
    y += d1.height;
    Dimension d2 = m_text.getPreferredSize();
    m_text.setBounds(x, y, w, d2.height);
    y += d2.height;
    m_scroll.setBounds(x, y, w, h-y);
  }
}

class ColorComboRenderer extends JPanel implements ListCellRenderer
{
  protected Color m_color = Color.black;
  protected Color m_focusColor = 
    (Color) UIManager.get("List.selectionBackground");
  protected Color m_nonFocusColor = Color.white;

  public Component getListCellRendererComponent(JList list,
   Object obj, int row, boolean sel, boolean hasFocus)
  {
    if (hasFocus || sel)
      setBorder(new CompoundBorder(
        new MatteBorder(2, 10, 2, 10, m_focusColor),
        new LineBorder(Color.black)));
    else
      setBorder(new CompoundBorder(
        new MatteBorder(2, 10, 2, 10, m_nonFocusColor),
        new LineBorder(Color.black)));

    if (obj instanceof Color) 
      m_color = (Color) obj;
    return this;
  }
    
  public void paintComponent(Graphics g) {
    setBackground(m_color);
    super.paintComponent(g);
  }
}

Understanding the Code

Class WordProcessor

Three new instance variables are added:

String[] m_fontNames: array of available font family names.

String[] m_fontSizes: array of font sizes.

FontDialog m_fontDialog: custom font properties and preview dialog.

These arrays were used earlier as local variables to create the toolbar combobox components. Since we need to use them in our font dialog as well, we decided to make them public instance variables (this requires minimal changes to the createMenuBar() method).

 

Note: Reading the list of available fonts takes a significant amount of time. For performance reasons it is best to do this once in a program.

A new menu item titled "Font…" is now added to the "Format" menu. When the corresponding ActionListener is invoked, the application is repainted, the attributes of the character element corresponding to the current caret position are retrieved as an AttributeSet instance and passed to the dialog for selection (using its setAttributes()), and the dialog is centered relative to the parent frame and displayed. If the dialog is closed with the "OK" button (determined by checking a value returned by FontDialog’s getOption() method), we retrieve the new font attri! butes with FontDialog.getAttributes(), and assign these attributes to the selected text with our setAttributeSet() method. Finally, our toolbar components are updated with our showAttributes() method.

Class FontDialog

This class extends JDialog and acts as a font properties editor and previewer for our word processor application. Several instance variables are declared:

int m_option: indicates how the dialog is closed: by pressing the "OK" button, by pressing the "Cancel" button, or by closing the dialog window directly from the title bar. The constants defined in JOptionPane are reused for this variable.

MutableAttributeSet m_attributes: a collection of font attributes used to preserve the user’s selection.

OpenList m_lstFontName: custom JList sub-class for selecting the font family name.

OpenList m_lstFontSize: custom JList sub-class for selecting the font size.

JCheckBox m_chkBold: checkbox to select the bold attribute.

JCheckBox m_chkItalic: checkbox to select the italic attribute.

JCheckBox m_chkUnderline: checkbox to select the font underline attribute.

JCheckBox m_chkStrikethrough: checkbox to select the font strikethrough attribute.

JCheckBox m_chkSubscript: checkbox to select the font subscript attribute.

JCheckBox m_chkSuperscript: checkbox to select the font superscript attribute.

JComboBox m_cbColor: combobox to select the font foreground color.

JLabel m_preview: label to preview the selections.

The FontDialog constructor first creates a super-class modal dialog titled "Font." The constructor creates and initializes all GUI components used in this dialog. A y-oriented BoxLayout is used to place component groups from top to bottom.

Two OpenList components (see below) are placed at the top to select an available font family name and font size. These components encapsulate a label, text box and list components which work together. They are comparable to editable comboboxes that always keep their drop-down list open. Below the OpenLists, a group of six checkboxes are placed for selecting bold, italic, underline, strikethrough, subscript, and superscript font attributes. JComboBox m_cbColor is placed below this group, and is used to select the font foreground color. 64 Colors are added, and an instance of our custom ColorComboRenderer class (see below) is used as its list cell renderer. JLabel m_preview is used to preview the selected font before applying it to the editing text, and is placed below the foreground color combo box.

Two buttons labeled "OK" and "Cancel" are placed at the bottom of the dialog. They are placed in a panel managed by a 1x2 GridLayout, which is in turn placed in a panel managed by a FlowLayout. This is to ensure the equal sizing and central placement of the buttons. Both receive ActionListeners which hide the dialog and set the m_option instance variable to JOptionPane.OK_OPTION and JOptionPane.CANCEL_OPTION respectively. An application (WordProcessor in our case) will normally check this value once the modal dialog is dismissed by calling its getOption() method. This tells the application whether or not the changes should be ignored (CANCEL_OPTION) or applied (OK_OPTION).

The dialog window is packed to give it a natural size, and is then centered with respect to the parent frame. The m_lstFontName and m_lstFontSize OpenList components each receive the same ListSelectionListener instance which calls our custom updatePreview() method (see below) whenever the list selection is changed. Similarly, two checkboxes and the foreground color combobox receive an ActionListener which does the same thing. This provides dynamic preview of the selected font attributes as soon as any is changed.!

 

Bug Alert! Underline, strikethrough, subscript, and superscript font properties are not supported by the AWT Font class, so they cannot be shown in the JLabel component. This is why the corresponding checkbox components do not receive an ActionListener. As we will see, these properties also do not work properly in RTF documents. They are included in this dialog for completeness, in the hopes that they will work properly in a future Swing release.

The setAttributes() method takes an AttributeSet instance as parameter. It copies this attribute set into a SimpleAttributeSet stored as our m_attributes instance variable. Appropriate font attributes are extracted using StyleConstants methods, and used to assign values to the dialog’s controls. Finally the preview label is updated according to these new settings by calling our updatePreview() method. Note that the setAttributes() method is public and is used for data exchange between this dialog and it’s owner (in our case WordProcessor).

The getAttributes() method plays an opposite role with respect to setAttributes(). It retrieves data from dialog's controls, packs them into an AttributeSet instance using StyleConstants methods, and returns this set to the caller

The getOption() method returns a code indicating how the dialog was closed by the user. This value should be checked prior to retrieving data from the dialog to determine whether or not the user canceled (JOptionPane.CANCEL_OPTION) or ok’d (JOptionPane.OK_OPTION) the changes.

The updatePreview() method is called to update the font preview label when a font attribute is changed. It retrieves the selected font attributes (family name, size, bold and italic properties) and creates a new Font instance to render the label. The selected color is retrieved from the m_cbColor combobox and set as the label’s foreground.

Class OpenList

This component consists of a title label, a text field, and a list in a scroll pane. The user can either select a value from the list, or enter it in the text box manually. OpenList extends JPanel and maintains the following four instance variables:

JLabel m_title: title label used to identify the purpose of this component.

JTextField m_text: editable text field.

JList m_list: list component.

JScrollPane m_scroll: scroll pane containing the list component.

The OpenList constructir assigns a null layout manager because this container manages its child components on its own. The four components listed above are instantiated and simply added to this container.

The setSelected() method sets the text field text to that of the given String, and selects the corresponding item in the list (which is scrolled to show display the newly selected value). The getSelected() method retrieves and returns the selected item as a String.

Methods setSelectedInt()/getSelectedInt() do the same but with int values. These methods are implemented to simplify working with a list of ints.

The valueChanged() and actionPerformed() methods provide coordination between the list component and the text field. The valueChanged() method is called whenever the list selection changes, and will assign the result of a toString() call on the selected item as the text field’s text. The actionPerformed() method will be called when the user presses Enter while the text field has the current focus. This implementation performs a case-insensitive search through the list items in an effort to find an item which begins with the entered text. If such an item is found, it is selected.

The public addListSelectionListener() method adds a ListSelectionListener to our list component (which is protected). In this way, external objects can dynamically receive notifications about changes in that list’s selection.

The getPreferredSize(), getMaximumSize(), and getMinimumSize() methods calculate and return a preferred, maximum, and minimum dimension of this container respectively. They assume that the three child components (label, text field, and scroll pane containing the list) will be laid out one under another from top to bottom, receiving an equal width and their preferable heights. The doLayout() method actually lays out the components according to this scheme. Note that the insets (resulting from an assigned border, for instance) must always be taken into account (see chapter 4 for more about custom layout management).

Class ColorComboRenderer

This class implements the ListCellRenderer interface (discussed in chapters 9 and 10) and is used to represent various Colors. Three instance variables are defined:

Color m_color: used for the main background color to represent a Color.

Color m_focusColor: used for the thick border color of a selected item.

Color m_nonFocusColor: used for the thick border color of an unselected item.

The getListCellRendererComponent() method is called prior to the rendering of each list item (in our WordProcessor example this list is contained within our foreground colors combo box). The Color instance is retrieved and stored in the m_color instance variable. This color is used as the renderer’s background, while a white matte border is used to surround unselected cells, and a light blue matte border is used to surround a selected cell. The paintComponent() method simply sets the background to m_color and calls the super-class ! paintComponent() method.

Running the Code

Open an existing RTF file, select a portion of text, and bring up the font dialog. Verify that the initial values correspond to the font attributes of the character element at the current caret position. Try selecting different font attributes and note that the preview component is updated dynamically. Press the "OK" button to apply the selected attributes to the selected text. Also verify that pressing the "Cancel" button does not apply any changes. Figures 20.6 and 20.7 illustrate.

20.7 Word Processor: part VII - Paragraph formatting

Control over paragraph formatting attributes (e.g. line spacing, text alignment, left and right margins, etc.) is just as necessary in word processor applications as font attribute control. Swing supports a number of paragraph settings discussed below which we discussed briefly in chapter 19. In this section we'll add a dialog specifically for editing these settings. The most interesting aspect of this dialog is a special component we’ve designed to allow a preview of formatted text. In this way the user can get a feeling for how a setting change, or group of changes, will affect the actual document.

Figure 20.8 RTF word processor displaying a custom paragraph attributes dialog.

<<file figure20-8.gif>>

The Code: WordProcessor.java

see \Chapter20\7

import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.*;

import javax.swing.*;
import javax.swing.text.*;
import javax.swing.event.*;
import javax.swing.border.*;
import javax.swing.text.rtf.*;
import javax.swing.undo.*;

public class WordProcessor extends JFrame 
{
  // Unchanged code from section 20.6

  protected FontDialog m_fontDialog;
  protected ParagraphDialog m_paragraphDialog;

  protected JMenuBar createMenuBar() {

    // Unchanged code from section 20.6

    m_fontDialog = new FontDialog(this, m_fontNames, m_fontSizes);
    
    m_paragraphDialog = new ParagraphDialog(this);

    // Unchanged code from section 20.6
    
    item = new JMenuItem("Paragraph...");
    item.setMnemonic('p');
    lst = new ActionListener() { 
      public void actionPerformed(ActionEvent e) {
        WordProcessor.this.repaint();
        AttributeSet a = m_doc.getCharacterElement(
          m_monitor.getCaretPosition()).getAttributes();
        m_paragraphDialog.setAttributes(a);

        Dimension d1 = m_paragraphDialog.getSize();
        Dimension d2 = WordProcessor.this.getSize();
        int x = Math.max((d2.width-d1.width)/2, 0);
        int y = Math.max((d2.height-d1.height)/2, 0);
        m_paragraphDialog.setBounds(x + WordProcessor.this.getX(),
          y + WordProcessor.this.getY(), d1.width, d1.height);

        m_paragraphDialog.show();
        if (m_paragraphDialog.getOption()==JOptionPane.OK_OPTION) {
          setAttributeSet(dlg.getAttributes(), true);
          showAttributes(m_monitor.getCaretPosition());
        }
      }
    };
    item.addActionListener(lst);
    mFormat.add(item);

    mFormat.addSeparator();

    // Unchanged code from section 20.6

    return menuBar;
  }

  // Unchanged code from section 20.6

  protected void setAttributeSet(AttributeSet attr) {
    setAttributeSet(attr, false);
  }

  protected void setAttributeSet(AttributeSet attr, boolean setParagraphAttributes)
  {
    if (m_skipUpdate)
      return;
    int xStart = m_monitor.getSelectionStart();
    int xFinish = m_monitor.getSelectionEnd();
    if (!m_monitor.hasFocus()) {
      xStart = m_xStart;
      xFinish = m_xFinish;
    }
    if (setParagraphAttributes)
      m_doc.setParagraphAttributes(xStart, 
      xFinish - xStart, attr, false);
    else if (xStart != xFinish) 
      m_doc.setCharacterAttributes(xStart, 
        xFinish - xStart, attr, false);
    else {
      MutableAttributeSet inputAttributes = 
        m_kit.getInputAttributes();
      inputAttributes.addAttributes(attr);
    }
  }

  // Unchanged code from section 20.6
}

// Unchanged code from section 20.6

class ParagraphDialog extends JDialog
{
  protected int m_option = JOptionPane.CLOSED_OPTION;
  protected MutableAttributeSet m_attributes;
  protected JTextField m_lineSpacing;
  protected JTextField m_spaceAbove;
  protected JTextField m_spaceBelow;
  protected JTextField m_firstIndent;
  protected JTextField m_leftIndent;
  protected JTextField m_rightIndent;
  protected SmallToggleButton m_btLeft;
  protected SmallToggleButton m_btCenter;
  protected SmallToggleButton m_btRight;
  protected SmallToggleButton m_btJustified;

  protected ParagraphPreview m_preview;

  public ParagraphDialog(JFrame parent) {
    super(parent, "Paragraph", true);
    getContentPane().setLayout(new BoxLayout(getContentPane(), 
      BoxLayout.Y_AXIS));

    JPanel p = new JPanel(new GridLayout(1, 2, 5, 2));

    JPanel ps = new JPanel(new GridLayout(3, 2, 10, 2));
    ps.setBorder(new TitledBorder(new EtchedBorder(), "Space"));
    ps.add(new JLabel("Line spacing:"));
    m_lineSpacing = new JTextField();
    ps.add(m_lineSpacing);
    ps.add(new JLabel("Space above:"));
    m_spaceAbove = new JTextField();
    ps.add(m_spaceAbove);
    ps.add(new JLabel("Space below:"));
    m_spaceBelow = new JTextField();
    ps.add(m_spaceBelow);
    p.add(ps);

    JPanel pi = new JPanel(new GridLayout(3, 2, 10, 2));
    pi.setBorder(new TitledBorder(new EtchedBorder(), "Indent"));
    pi.add(new JLabel("First indent:"));
    m_firstIndent = new JTextField();
    pi.add(m_firstIndent);
    pi.add(new JLabel("Left indent:"));
    m_leftIndent = new JTextField();
    pi.add(m_leftIndent);
    pi.add(new JLabel("Right indent:"));
    m_rightIndent = new JTextField();
    pi.add(m_rightIndent);
    p.add(pi);
    getContentPane().add(p);

    getContentPane().add(Box.createVerticalStrut(5));
    p = new JPanel();
    p.setLayout(new BoxLayout(p, BoxLayout.X_AXIS));
    p.add(Box.createHorizontalStrut(10));
    p.add(new JLabel("Alignment:"));
    p.add(Box.createHorizontalStrut(20));

    ButtonGroup bg = new ButtonGroup();
    ImageIcon img = new ImageIcon("al_left.gif");
    m_btLeft = new SmallToggleButton(false, img, img, "Left");
    bg.add(m_btLeft);
    p.add(m_btLeft);
    img = new ImageIcon("al_center.gif");
    m_btCenter = new SmallToggleButton(false, img, img, "Center");
    bg.add(m_btCenter);
    p.add(m_btCenter);
    img = new ImageIcon("al_right.gif");
    m_btRight = new SmallToggleButton(false, img, img, "Right");
    bg.add(m_btRight);
    p.add(m_btRight);
    img = new ImageIcon("al_justify.gif");
    m_btJustified = new SmallToggleButton(false, img, img, 
      "Justify");
    bg.add(m_btJustified);
    p.add(m_btJustified);
    getContentPane().add(p);

    p = new JPanel(new BorderLayout());
    p.setBorder(new TitledBorder(new EtchedBorder(), "Preview"));
    m_preview = new ParagraphPreview();
    p.add(m_preview, BorderLayout.CENTER);
    getContentPane().add(p);

    p = new JPanel(new FlowLayout());
    JPanel p1 = new JPanel(new GridLayout(1, 2, 10, 2));
    JButton btOK = new JButton("OK");
    ActionListener lst = new ActionListener() { 
      public void actionPerformed(ActionEvent e) {
        m_option = JOptionPane.OK_OPTION;
        setVisible(false);
      }
    };
    btOK.addActionListener(lst);
    p1.add(btOK);

    JButton btCancel = new JButton("Cancel");
    lst = new ActionListener() { 
      public void actionPerformed(ActionEvent e) {
        m_option = JOptionPane.CANCEL_OPTION;
        setVisible(false);
      }
    };
    btCancel.addActionListener(lst);
    p1.add(btCancel);
    p.add(p1);
    getContentPane().add(p);

    pack();
    setResizable(false);

    FocusListener flst = new FocusListener() { 
      public void focusGained(FocusEvent e) {}

      public void focusLost(FocusEvent e) { updatePreview(); }
    };
    m_lineSpacing.addFocusListener(flst);
    m_spaceAbove.addFocusListener(flst);
    m_spaceBelow.addFocusListener(flst);
    m_firstIndent.addFocusListener(flst);
    m_leftIndent.addFocusListener(flst);
    m_rightIndent.addFocusListener(flst);

    lst = new ActionListener() { 
      public void actionPerformed(ActionEvent e) {
        updatePreview();
      }
    };
    m_btLeft.addActionListener(lst);
    m_btCenter.addActionListener(lst);
    m_btRight.addActionListener(lst);
    m_btJustified.addActionListener(lst);
  }

  public void setAttributes(AttributeSet a) {
    m_attributes = new SimpleAttributeSet(a);
    m_lineSpacing.setText(Float.toString(
      StyleConstants.getLineSpacing(a)));
    m_spaceAbove.setText(Float.toString(
      StyleConstants.getSpaceAbove(a)));
    m_spaceBelow.setText(Float.toString(
      StyleConstants.getSpaceBelow(a)));
    m_firstIndent.setText(Float.toString(
      StyleConstants.getFirstLineIndent(a)));
    m_leftIndent.setText(Float.toString(
      StyleConstants.getLeftIndent(a)));
    m_rightIndent.setText(Float.toString(
      StyleConstants.getRightIndent(a)));

    int alignment = StyleConstants.getAlignment(a);
    if (alignment == StyleConstants.ALIGN_LEFT)
      m_btLeft.setSelected(true);
    else if (alignment == StyleConstants.ALIGN_CENTER)
      m_btCenter.setSelected(true);
    else if (alignment == StyleConstants.ALIGN_RIGHT)
      m_btRight.setSelected(true);
    else if (alignment == StyleConstants.ALIGN_JUSTIFIED)
      m_btJustified.setSelected(true);

    updatePreview();
  }

  public AttributeSet getAttributes() {
    if (m_attributes == null)
      return null;
    float value;
    try { 
      value = Float.parseFloat(m_lineSpacing.getText());
      StyleConstants.setLineSpacing(m_attributes, value); 
    } catch (NumberFormatException ex) {}
    try { 
      value = Float.parseFloat(m_spaceAbove.getText());
      StyleConstants.setSpaceAbove(m_attributes, value); 
    } catch (NumberFormatException ex) {}
    try { 
      value = Float.parseFloat(m_spaceBelow.getText());
      StyleConstants.setSpaceBelow(m_attributes, value); 
    } catch (NumberFormatException ex) {}
    try { 
      value = Float.parseFloat(m_firstIndent.getText());
      StyleConstants.setFirstLineIndent(m_attributes, value); 
    } catch (NumberFormatException ex) {}
    try { 
      value = Float.parseFloat(m_leftIndent.getText());
      StyleConstants.setLeftIndent(m_attributes, value); 
    } catch (NumberFormatException ex) {}
    try { 
      value = Float.parseFloat(m_rightIndent.getText());
      StyleConstants.setRightIndent(m_attributes, value); 
    } catch (NumberFormatException ex) {}

    StyleConstants.setAlignment(m_attributes, getAlignment());

    return m_attributes;
  }

  public int getOption() {
    return m_option;
  }

  protected void updatePreview() {
    m_preview.repaint();
  }

  protected int getAlignment() {
    if (m_btLeft.isSelected())
      return StyleConstants.ALIGN_LEFT;
    if (m_btCenter.isSelected())
      return StyleConstants.ALIGN_CENTER;
    else if (m_btRight.isSelected())
      return StyleConstants.ALIGN_RIGHT;
    else
      return StyleConstants.ALIGN_JUSTIFIED;
  }

  class ParagraphPreview extends JPanel
  {
    protected Font m_fn = new Font("Monospace", Font.PLAIN, 6);
    protected String m_dummy = "abcdefghjklm";
    protected float  m_scaleX = 0.25f;
    protected float  m_scaleY = 0.25f;
    protected Random m_random = new Random();

    public ParagraphPreview() {
      setBackground(Color.white);
      setForeground(Color.black);
      setOpaque(true);
      setBorder(new LineBorder(Color.black));
      setPreferredSize(new Dimension(120, 56));
    }

    public void paintComponent(Graphics g) {
      super.paintComponent(g);
      float lineSpacing = 0;
      float spaceAbove  = 0;
      float spaceBelow  = 0;
      float firstIndent = 0;
      float leftIndent  = 0;
      float rightIndent = 0;

      try { 
        lineSpacing = Float.parseFloat(m_lineSpacing.getText());
      } catch (NumberFormatException ex) {}
      try { 
        spaceAbove = Float.parseFloat(m_spaceAbove.getText()); 
      } catch (NumberFormatException ex) {}
      try { 
        spaceBelow = Float.parseFloat(m_spaceBelow.getText()); 
      } catch (NumberFormatException ex) {}
      try { 
        firstIndent = Float.parseFloat(m_firstIndent.getText()); 
      } catch (NumberFormatException ex) {}
      try { 
        leftIndent = Float.parseFloat(m_leftIndent.getText()); 
      } catch (NumberFormatException ex) {}
      try { 
        rightIndent = Float.parseFloat(m_rightIndent.getText()); 
      } catch (NumberFormatException ex) {}

      m_random.setSeed(1959);    // Use same seed every time

      g.setFont(m_fn);
      FontMetrics fm = g.getFontMetrics();
      int h = fm.getAscent();
      int s  = Math.max((int)(lineSpacing*m_scaleY), 1);
      int s1 = Math.max((int)(spaceAbove*m_scaleY), 0) + s;
      int s2 = Math.max((int)(spaceBelow*m_scaleY), 0) + s;
      int y = 5+h;

      int xMarg = 20;
      int x0 = Math.max((int)(firstIndent*m_scaleX)+xMarg, 3);
      int x1 = Math.max((int)(leftIndent*m_scaleX)+xMarg, 3);
      int x2 = Math.max((int)(rightIndent*m_scaleX)+xMarg, 3);
      int xm0 = getWidth()-xMarg;
      int xm1 = getWidth()-x2;
            
      int n = (int)((getHeight()-(2*h+s1+s2-s+10))/(h+s));
      n = Math.max(n, 1);

      g.setColor(Color.lightGray);
      int x = xMarg;
      drawLine(g, x, y, xm0, xm0, fm, StyleConstants.ALIGN_LEFT);
      y += h+s1;

      g.setColor(Color.gray);
      int alignment = getAlignment();
      for (int k=0; k<n; k++) {
        x = (k==0 ? x0 : x1);
        int xLen = (k==n-1 ? xm1/2 : xm1);
        if (k==n-1 && alignment==StyleConstants.ALIGN_JUSTIFIED)
          alignment = StyleConstants.ALIGN_LEFT;
        drawLine(g, x, y, xm1, xLen, fm, alignment);
        y += h+s;
      }

      y += s2-s;
      x = xMarg;
      g.setColor(Color.lightGray);
      drawLine(g, x, y, xm0, xm0, fm, StyleConstants.ALIGN_LEFT);
    }

    protected void drawLine(Graphics g, int x, int y, int xMax, 
     int xLen, FontMetrics fm, int alignment)
    {
      if (y > getHeight()-3)
        return;
      StringBuffer s = new StringBuffer();
      String str1;
      int xx = x;
      while (true) {
        int m = m_random.nextInt(10)+1;
        str1 = m_dummy.substring(0, m)+" ";
        int len = fm.stringWidth(str1);
        if (xx+len >= xLen)
          break;
        xx += len;
        s.append(str1);
      }
      String str = s.toString();

      switch (alignment) {
        case StyleConstants.ALIGN_LEFT:
          g.drawString(str, x, y);
          break;
        case StyleConstants.ALIGN_CENTER:
          xx = (xMax+x-fm.stringWidth(str))/2;
          g.drawString(str, xx, y);
          break;
        case StyleConstants.ALIGN_RIGHT:
          xx = xMax-fm.stringWidth(str);
          g.drawString(str, xx, y);
          break;
        case StyleConstants.ALIGN_JUSTIFIED:
          while (x+fm.stringWidth(str) < xMax)
            str += "a";
          g.drawString(str, x, y);
          break;
      }
    }
  }
}

Understanding the Code

Class WordProcessor

One new instance variable has been added:

ParagraphDialog m_paragraphDialog: custom dialog used to manage paragraph attributes.

A new menu item titled "Paragraph…" is now added to the "Format" menu. A corresponding ActionListener acts similarly to the listener of "Font..." menu. It repaints the entire application, retrieves a set of attributes corresponding to the character element at the current caret position, and passes this set to m_paragraphDialog. The dialog is then centered with respect to its parent (WordProcessor in our case) and shows itself. When the "OK" or "Cancel" button is pressed, the result returned by the getOption() method is normally checked. If the returned value is equal to JOptionPane.OK_O! PTION (i.e. "OK" was pressed) we retrieve the selected attributes with ParagraphDialog’s getAttributes() method and assign them to the selected text with our setAttributeSet() method. Otherwise we make no changes.

An additional parameter, a boolean, is added to our setAttributeSet() method. This is used to distinguish between setting character attributes and setting paragraph attributes. A value of true indicates that the given attribute set corresponds to paragraph attributes. A value of false indicates character attributes. To preserve the existing code without requiring extensive modification, we keep the old setAttributeSet() method with one parameter, and redirect it to the new method by having it call setAttributeSet(attr, false).

Class ParagraphDialog

This class extends JDialog and acts as a paragraph attributes editor for our word processor application. Several instance variables are declared:

int m_option: indicates how the dialog is closed: by pressing the "OK" button, by pressing the "Cancel" button, or by closing the dialog window directly from the title bar. The constants defined in JOptionPane are reused for this variable.

MutableAttributeSet m_attributes: a collection of paragraph attributes used to preserve the user’s selection.

JTextField m_lineSpacing: used to specify paragraph line spacing.

JTextField m_spaceAbove: used to specify above line spacing.

JTextField m_spaceBelow: used to specify below line spacing.

JTextField m_firstIndent: used to specify left indent of the first paragraph line.

JTextField m_leftIndent: used to specify left indent of all other paragraph lines (other than the first).

JTextField m_rightIndent: used to specify right indent of all paragraph lines.

SmallToggleButton m_btLeft: used to toggle left text alignment.

SmallToggleButton m_btCenter: used to toggle center text alignment.

SmallToggleButton m_btRight: used to toggle right text alignment.

SmallToggleButton m_btJustified: used to toggle justified text alignment.

ParagraphPreview m_preview: custom component for previewing paragraph attribute effects .

The ParagraphDialog constructor first creates a super-class modal dialog titled "Paragraph." This dialog uses a y-oriented BoxLayout to place component groups from top to bottom. Six text fields listed above are placed in two side-by-side panels titled "Space" and "Indent." These controls allow the user to specify spacing attributes. Below these fields is a group of SmallToggleButtons (introduced in chapter 12) used to control text alignment: left, center, right, or justified.

An instance of our custom ParagraphPreview component, m_preview, is used to preview the selected paragraph attributes before applying them to the selected document text. We will discuss how this component works below.

Two buttons labeled "OK" and "Cancel" are placed at the bottom of the dialog and act identically to those at the bottom of our font dialog (see previous exampple). The six text boxes mentioned above receive an identical FocusListener which invokes our updatePreview() method (see below) when a text field loses focus. Similarly, the four toggle buttons receive an identical ActionListener which does the same thing. This provides a dynamic preview of the selected paragraph attributes whenever any attribute is changed.

The setAttributes() method takes an AttributeSet instance as parameter. It copies this attribute set into a SimpleAttributeSet stored as our m_attributes instance variable. Appropriate paragraph attributes are extracted using StyleConstants methods, and used to assign values to the dialog’s controls. Finally the preview component is updated according to these new settings by calling our updatePreview() method. Note that the setAttributes() method is public and is used for data exchange between this dialog and it’s owner (in our case WordProcessor).

The method getAttributes() plays an opposite role with respect to setAttributes(). It retrieves data from the dialog’s controls, packs them into an AttributeSet instance using StyleConstants methods, and return this set to the caller.

 

Note: All spacing variables are of type float, even though they are actually measured in discrete screen pixels.

The getOption() method returns a code indicating how the dialog was closed by the user. This value should be checked prior to retrieving data from the dialog to determine whether or not the user canceled (JOptionPane.CANCEL_OPTION) or ok’d (JOptionPane.OK_OPTION) the changes.

The updatePreview() method is called to update the paragraph preview component whenever a paragraph attribute is changed. It simply forces our m_preview component to repaint itself.

The getAlignment() method checks for the selected toggle button and returns the corresponding alignment attribute.

Class ParagraphDialog.ParagraphPreview

This inner class represents our custom component used to display an imitation paragraph used to preview a set of paragraph attributes. The actual rendering consists of three parts:

  1. A light gray text line representing the end of a preceding paragraph. The indent and spacing of this line is not affected by the current paragraph attribute settings.
  2. Several gray text lines representing a paragraph being modified. Indentations and line spacing depend on the current paragraph attribute settings. The number of these lines is calculated so as to fill the component’s height naturally, and depends on the current line spacing attribute settings.
  3. A light gray text line representing the beginning of a following paragraph. The space between this line and the last paragraph line depends on the current above line spacing attribute. The indentation of this line is not affected by the current paragraph attributes.

Several instance variables are declared in this inner class:

Font m_fn: the font used for preview paragraph rendering. This font is intentionally made small and barely recognizable, because the displayed text itself does not have any meaning; only the paragraph formatting does.

String m_dummy: a dummy string used in random generation of a paragraph.

float m_scaleX: the scaling factor used to re-calculate sizes in the vertical direction.

float m_scaleY: the scaling factor used to re-calculate sizes in the horizontal direction.

Random m_random: random number generator used in piecing together a paragraph of characters.

 

Reminder: java.util.Random provides random number generation capabilities, including seed selection, generation of integers in a given range, etc. In the simplest cases we can use the Math.random() static method instead.

The ParagraphPreview constructor initializes the colors, border, and preferred size for this component.

The most interesting aspect of the preview component’s work is done in the paintComponent() method. First, this method retrieves the paragraph attributes specified by the user. Then we set a hard-coded seed value for our random number generator, m_random, which we use to generate a paragraph of gibberish. The following local variables are used to control the placement of this paragraph’s lines:

int h: the height of the text string determined by the font selected for preview.

int s: spacing between lines in screen pixels.

int s1: actual spacing between the previous paragraph and the first line of this paragraph.

int s2: actual spacing between the following paragraph and the last line of this paragraph.

int y: vertical position of the text being drawn (to be updated during the rendering process).

int xMarg: left and right fixed margins.

int x0: actual left indent for the first line of this paragraph.

int x1: actual left indent for the second and remaining following lines of this paragraph.

int x2: actual right indent for the lines of this paragraph.

int xm0: maximum x-coordinate for the text lines without regard to the specified right indent.

int xm1: maximum x-coordinate for the text lines with regard to the specified right indent.

int n: number of paragraph lines which can fit vertically within this component, taking into account the specified line spacing and both the preceding and following paragraph lines.

Once all these variables are calculated, the rendering can be performed relatively easily. The drawLine() method is used to draw a single line of text. First we draw a line denoting the preceding paragraph, then we draw each line of the current paragraph, and finally, a line denoting the following paragraph. Note that the last line of the current paragraph is intentionally half the length of a normal line to produce a more realistic impression of the text. Also, when justified alignment is specified, it is suppressed for the last line of text, since the last line should not be stretched.

The drawLine() method takes seven parameters:

Graphics g: used for all rendering.

int x, int y: coordinates of the beginning of a text line.

int xMax: maximum x-coordinate a line can occupy.

int xLen: line length plus left margin size.

FontMetrics fm: retrieved from the current Graphics instance.

int alignment: current text alignment.

First this method prepares a line to be drawn by concatenating random pieces of the m_dummy string until the resulting length, plus the left margin size, is greater than or equal to the xLen parameter. (Note that a StringBuffer instance is used here to improve performance.) Then we draw this line depending on the selected alignment. For left alignment we simply start drawing at the left margin. For center and right alignments we calculate the start position by working with the maximum x-coordinate, and the width of that line. For justified alignment we should recalculate space between words so the resulting line will occupy all available width (however, in our case, since the preview text is totally meaningless, we just add some more text at th! e right end).

Running the Code

Open an existing RTF file, select some text and bring up the "Paragraph" dialog. Verify that the initial values of all components correspond to the paragraph attributes at the current caret position. Specify new paragraph attributes and note how the preview component is updated dynamically. Press the "OK" button to apply the specified attributes to the selected paragraphs of document text.

 

Bug Alert! Justified text alignment has not been implemented as of Java 2 FCS.

 

20.8 Word Processor: part VIII - Find and replace

Along with font and paragraph dialogs, find and replace functionality has also become a fairly common tool in GUI-based text editing environments. It is safe to assume that most users would be sadly disappointed if this functionality was not included in a new word processor application. In this section we will show how to add this functionality. Traditionally such tools are represented in an "Edit" menu and can be activated by keyboard accelerators. We will use a dialog containing a single tabbed pane with tabs for finding and replacing a specific region of text. We will also provide several options for searching: match case, search whole words only, and search up or down.

Figure 20.9 WordProcessor with complete find and replace functionality; "Find" tab shown here

<<file figure20-9.gif>>

Figure 20.10 "Replace" tab of our custom find and replace dialog used in a word processor application.

<<file figure20-10.gif>>

The Code: WordProcessor.java

see \Chapter20\8

import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.*;

import javax.swing.*;
import javax.swing.text.*;
import javax.swing.event.*;
import javax.swing.border.*;
import javax.swing.text.rtf.*;
import javax.swing.undo.*;

import dl.*;

public class WordProcessor extends JFrame 
{
  // Unchanged code from section 20.7

  protected FontDialog m_fontDialog;
  protected ParagraphDialog m_paragraphDialog;
  protected FindDialog m_findDialog;

  protected JMenuBar createMenuBar() {
    // Unchanged code from section 20.7

    mEdit.addSeparator();

    Action findAction = new AbstractAction("Find...", new ImageIcon("edit_find.gif")) 
    { 
      public void actionPerformed(ActionEvent e) {
        WordProcessor.this.repaint();
        if (m_findDialog==null)
          m_findDialog = new FindDialog(WordProcessor.this, 0);
        else
          m_findDialog.setSelectedIndex(0);

        Dimension d1 = m_findDialog.getSize();
        Dimension d2 = WordProcessor.this.getSize();
        int x = Math.max((d2.width-d1.width)/2, 0);
        int y = Math.max((d2.height-d1.height)/2, 0);
        m_findDialog.setBounds(x + WordProcessor.this.getX(),
          y + WordProcessor.this.getY(), d1.width, d1.height);

        m_findDialog.show();
      }
    };
    item = mEdit.add(findAction);  
    item.setMnemonic('f');
    item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F, 
      KeyEvent.CTRL_MASK));

    Action replaceAction = new AbstractAction("Replace...") { 
      public void actionPerformed(ActionEvent e) {
        WordProcessor.this.repaint();
        if (m_findDialog==null)
          m_findDialog = new FindDialog(WordProcessor.this, 1);
        else
          m_findDialog.setSelectedIndex(1);

        Dimension d1 = m_findDialog.getSize();
        Dimension d2 = WordProcessor.this.getSize();
        int x = Math.max((d2.width-d1.width)/2, 0);
        int y = Math.max((d2.height-d1.height)/2, 0);
        m_findDialog.setBounds(x + WordProcessor.this.getX(),
          y + WordProcessor.this.getY(), d1.width, d1.height);

        m_findDialog.show();
      }
    };
    item =  mEdit.add(replaceAction);  
    item.setMnemonic('r');
    item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_H, 
      KeyEvent.CTRL_MASK));

    menuBar.add(mEdit);

    // Unchanged code from section 20.7
        
    return menuBar;
  }

  // Unchanged code from section 20.7

  public Document getDocument() { return m_doc; }

  public JTextPane getTextPane() { return m_monitor; }

  public void setSelection(int xStart, int xFinish, boolean moveUp) {
    if (moveUp) {
      m_monitor.setCaretPosition(xFinish);
      m_monitor.moveCaretPosition(xStart);
    }
    else
      m_monitor.select(xStart, xFinish);
    m_xStart = m_monitor.getSelectionStart();
    m_xFinish = m_monitor.getSelectionEnd();
  }

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

  // Unchanged code from section 20.7
}

// Unchanged code from section 20.7

class FindDialog extends JDialog
{
  protected WordProcessor m_owner;
  protected JTabbedPane m_tb;
  protected JTextField m_txtFind1;
  protected JTextField m_txtFind2;
  protected Document m_docFind;
  protected Document m_docReplace;
  protected ButtonModel m_modelWord;
  protected ButtonModel m_modelCase;
  protected ButtonModel m_modelUp;
  protected ButtonModel m_modelDown;

  protected int m_searchIndex = -1;
  protected boolean m_searchUp = false;
  protected String  m_searchData;

  public FindDialog(WordProcessor owner, int index) {
    super(owner, "Find and Replace", false);
    m_owner = owner;

    m_tb = new JTabbedPane();

    // "Find" panel
    JPanel p1 = new JPanel(new BorderLayout());

    JPanel pc1 = new JPanel(new BorderLayout());

    JPanel pf = new JPanel();
    pf.setLayout(new DialogLayout(20, 5));
    pf.setBorder(new EmptyBorder(8, 5, 8, 0));
    pf.add(new JLabel("Find what:"));
        
    m_txtFind1 = new JTextField();
    m_docFind = m_txtFind1.getDocument();
    pf.add(m_txtFind1);
    pc1.add(pf, BorderLayout.CENTER);

    JPanel po = new JPanel(new GridLayout(2, 2, 8, 2));
    po.setBorder(new TitledBorder(new EtchedBorder(), 
      "Options"));

    JCheckBox chkWord = new JCheckBox("Whole words only");
    chkWord.setMnemonic('w');
    m_modelWord = chkWord.getModel();
    po.add(chkWord);

    ButtonGroup bg = new ButtonGroup();
    JRadioButton rdUp = new JRadioButton("Search up");
    rdUp.setMnemonic('u');
    m_modelUp = rdUp.getModel();
    bg.add(rdUp);
    po.add(rdUp);

    JCheckBox chkCase = new JCheckBox("Match case");
    chkCase.setMnemonic('c');
    m_modelCase = chkCase.getModel();
    po.add(chkCase);

    JRadioButton rdDown = new JRadioButton("Search down", true);
    rdDown.setMnemonic('d');
    m_modelDown = rdDown.getModel();
    bg.add(rdDown);
    po.add(rdDown);
    pc1.add(po, BorderLayout.SOUTH);

    p1.add(pc1, BorderLayout.CENTER);

    JPanel p01 = new JPanel(new FlowLayout());
    JPanel p = new JPanel(new GridLayout(2, 1, 2, 8));

    ActionListener findAction = new ActionListener() { 
      public void actionPerformed(ActionEvent e) {
        findNext(false, true);
      }
    };
    JButton btFind = new JButton("Find Next");
    btFind.addActionListener(findAction);
    btFind.setMnemonic('f');
    p.add(btFind);

    ActionListener closeAction = new ActionListener() { 
      public void actionPerformed(ActionEvent e) {
        setVisible(false);
      }
    };
    JButton btClose = new JButton("Close");
    btClose.addActionListener(closeAction);
    btClose.setDefaultCapable(true);
    p.add(btClose);
    p01.add(p);
    p1.add(p01, BorderLayout.EAST);

    m_tb.addTab("Find", p1);

    // "Replace" panel
    JPanel p2 = new JPanel(new BorderLayout());

    JPanel pc2 = new JPanel(new BorderLayout());

    JPanel pc = new JPanel();
    pc.setLayout(new DialogLayout(20, 5));
    pc.setBorder(new EmptyBorder(8, 5, 8, 0));
        
    pc.add(new JLabel("Find what:"));
    m_txtFind2 = new JTextField();
    m_txtFind2.setDocument(m_docFind);
    pc.add(m_txtFind2);

    pc.add(new JLabel("Replace:"));
    JTextField txtReplace = new JTextField();
    m_docReplace = txtReplace.getDocument();
    pc.add(txtReplace);
    pc2.add(pc, BorderLayout.CENTER);

    po = new JPanel(new GridLayout(2, 2, 8, 2));
    po.setBorder(new TitledBorder(new EtchedBorder(), 
      "Options"));

    chkWord = new JCheckBox("Whole words only");
    chkWord.setMnemonic('w');
    chkWord.setModel(m_modelWord);
    po.add(chkWord);

    bg = new ButtonGroup();
    rdUp = new JRadioButton("Search up");
    rdUp.setMnemonic('u');
    rdUp.setModel(m_modelUp);
    bg.add(rdUp);
    po.add(rdUp);

    chkCase = new JCheckBox("Match case");
    chkCase.setMnemonic('c');
    chkCase.setModel(m_modelCase);
    po.add(chkCase);

    rdDown = new JRadioButton("Search down", true);
    rdDown.setMnemonic('d');
    rdDown.setModel(m_modelDown);
    bg.add(rdDown);
    po.add(rdDown);
    pc2.add(po, BorderLayout.SOUTH);

    p2.add(pc2, BorderLayout.CENTER);

    JPanel p02 = new JPanel(new FlowLayout());
    p = new JPanel(new GridLayout(3, 1, 2, 8));

    ActionListener replaceAction = new ActionListener() { 
      public void actionPerformed(ActionEvent e) {
        findNext(true, true);
      }
    };
    JButton btReplace = new JButton("Replace");
    btReplace.addActionListener(replaceAction);
    btReplace.setMnemonic('r');
    p.add(btReplace);

    ActionListener replaceAllAction = new ActionListener() { 
      public void actionPerformed(ActionEvent e) {
        int counter = 0;
        while (true) {
          int result = findNext(true, false);
          if (result < 0)    // error
            return;
          else if (result == 0)    // no more
            break;
          counter++;
        }
        JOptionPane.showMessageDialog(m_owner, 
          counter+" replacement(s) have been done", "Info",
          JOptionPane.INFORMATION_MESSAGE);
      }
    };
    JButton btReplaceAll = new JButton("Replace All");
    btReplaceAll.addActionListener(replaceAllAction);
    btReplaceAll.setMnemonic('a');
    p.add(btReplaceAll);

    btClose = new JButton("Close");
    btClose.addActionListener(closeAction);
    btClose.setDefaultCapable(true);
    p.add(btClose);
    p02.add(p);
    p2.add(p02, BorderLayout.EAST);
        
    // Make button columns the same size
    p01.setPreferredSize(p02.getPreferredSize());

    m_tb.addTab("Replace", p2);

    m_tb.setSelectedIndex(index);

    getContentPane().add(m_tb, BorderLayout.CENTER);

    WindowListener flst = new WindowAdapter() { 
      public void windowActivated(WindowEvent e) {
        m_searchIndex = -1;
        if (m_tb.getSelectedIndex()==0)
          m_txtFind1.grabFocus();
        else
          m_txtFind2.grabFocus();
      }

      public void windowDeactivated(WindowEvent e) {
        m_searchData = null;
      }
    };
    addWindowListener(flst);

    pack();
    setResizable(false);
  }

  public void setSelectedIndex(int index) {
    m_tb.setSelectedIndex(index);
    setVisible(true);
    m_searchIndex = -1;
  }

  public int findNext(boolean doReplace, boolean showWarnings) {
    JTextPane monitor = m_owner.getTextPane();
    int pos = monitor.getCaretPosition();
    if (m_modelUp.isSelected() != m_searchUp) {
      m_searchUp = m_modelUp.isSelected();
      m_searchIndex = -1;
    }

    if (m_searchIndex == -1) {
      try {
        Document doc = m_owner.getDocument();
        if (m_searchUp)
          m_searchData = doc.getText(0, pos);
        else
          m_searchData = doc.getText(pos, doc.getLength()-pos);
        m_searchIndex = pos;
      }
      catch (BadLocationException ex) {
        warning(ex.toString());
        return -1;
      }
    }
        
    String key = "";
    try { key = m_docFind.getText(0, m_docFind.getLength()); }
    catch (BadLocationException ex) {}
    if (key.length()==0) {
      warning("Please enter the target to search");
      return -1;
    }
    if (!m_modelCase.isSelected()) {
      m_searchData = m_searchData.toLowerCase();
      key = key.toLowerCase();
    }
    if (m_modelWord.isSelected()) {
      for (int k=0; k<Utils.WORD_SEPARATORS.length; k++) {
        if (key.indexOf(Utils.WORD_SEPARATORS[k]) >= 0) {
          warning("The text target contains an illegal "+
            "character \'"+Utils.WORD_SEPARATORS[k]+"\'");
          return -1;
        }
      }
    }

    String replacement = "";
    if (doReplace) {
      try {
        replacement = m_docReplace.getText(0, 
          m_docReplace.getLength());
      } catch (BadLocationException ex) {}
    }

    int xStart = -1;
    int xFinish = -1;
    while (true) 
    {
      if (m_searchUp)
        xStart = m_searchData.lastIndexOf(key, pos-1);
      else
        xStart = m_searchData.indexOf(key, pos-m_searchIndex);
      if (xStart < 0) {
        if (showWarnings)
          warning("Text not found");
        return 0;
      }

      xFinish = xStart+key.length();

      if (m_modelWord.isSelected()) {
        boolean s1 = xStart>0;
        boolean b1 = s1 && !Utils.isSeparator(m_searchData.charAt(
          xStart-1));
        boolean s2 = xFinish<m_searchData.length();
        boolean b2 = s2 && !Utils.isSeparator(m_searchData.charAt(
          xFinish));
                    
        if (b1 || b2)    // Not a whole word
        {
          if (m_searchUp && s1)    // Can continue up
          {
            pos = xStart;
            continue;
          }
          if (!m_searchUp && s2)    // Can continue down
          {
            pos = xFinish;
            continue;
          }
          // Found, but not a whole word, and we cannot continue
          if (showWarnings)
            warning("Text not found");
          return 0;
        }
      }
      break;
    }

    if (!m_searchUp) {
      xStart += m_searchIndex;
      xFinish += m_searchIndex;
    }
    if (doReplace) {
      m_owner.setSelection(xStart, xFinish, m_searchUp);
      monitor.replaceSelection(replacement);
      m_owner.setSelection(xStart, xStart+replacement.length(), 
        m_searchUp);
      m_searchIndex = -1;
    }
    else
      m_owner.setSelection(xStart, xFinish, m_searchUp);
    return 1;
  }

  protected void warning(String message) {
    JOptionPane.showMessageDialog(m_owner, 
      message, "Warning", JOptionPane.INFORMATION_MESSAGE);
  }
}

class Utils
{
  public static final char[] WORD_SEPARATORS = {' ', '\t', '\n',
    '\r', '\f', '.', ',', ':', '-', '(', ')', '[', ']', '{',
    '}', '<', '>', '/', '|', '\\', '\'', '\"'};
    
  public static boolean isSeparator(char ch) {
    for (int k=0; k<WORD_SEPARATORS.length; k++)
      if (ch == WORD_SEPARATORS[k])
        return true;
    return false;
  }
}

Understanding the Code

Class WordProcessor

This class now imports the dl package (constructed and discussed in chapter 4) to use our DialogLayout manager. WordProcessor also declares one new instance variable:

FindDialog m_findDialog: custom dialog for finding and replacing a selected region of text.

Two new menu items titled "Find..." and "Replace...," are added to the "Edit" menu. These items are activated with keyboard accelerators Ctrl+F and Ctrl+H respectively. When pressed, both items create an instance of FindDialog (if m_findDialog is null) or activate the existing instance, and the dialog is then displayed. The only difference between the two is that the "Find..." menu item activates the 0-indexed tabbed pane tab, and the "Replace..." menu item activates the tab at index 1.

Three new public methods have been added to this class to make access to our text pane component, and related objects, easier from external classes. The getDocument() method retrieves the text pane’s current document instance, and the getTextPane() method retrieves the text pane itself. The setSelection() method selects a portion of text between given start and end positions, and positions the caret at the beginning or end of the selection, depending on the value of the moveUp boolean parameter. The coordinates of such a selection are then stored in the m_xStart and m_xFinish instance variables (recall that these variables always hold the coordinates of the current text selection and are used to restore this selection when our text pane regains the focus).

Class FindDialog

This class is a modal, JDialog sub-class encapsulating our find and replace functionality. It contains a tabbed pane with two tabs, "Find" and "Replace." Among unique controls, both also contain several common controls: checkboxs for "Whole words only" and "Match case," radio button for "Search up" and "Search down," and a text field for the text to "Find." What we need to is a set of controls which are common across both pages. To simplify this task we create two components and use the same model for each. This guarantees that consistency will be maintained for us, without the need for any maintenance or state checks.

FindDialog maintains the following instance variables:

WordProcessor m_owner: an explicit reference to our WordProcessor parent application frame.

JTabbedPane m_tb: the tabbed pane containing the find and replace pages.

JTextField m_txtFind1: used to enter the string to find.

JTextField m_txtFind2: used to enter the string to replace.

Document m_docFind: a shared data model for the "Find" text fields.

Document m_docReplace: a data model for the "Replace" text field.

ButtonModel m_modelWord: a shared data model for the "Whole words only" checkboxes.

ButtonModel m_modelCase: a shared data model for the "Match case" checkboxes.

ButtonModel m_modelUp: a shared data model for the "Search up" radio buttons.

ButtonModel m_modelDown: a shared data model for the "Search down" radio buttons.

int m_searchIndex: position in the document to start searching from.

boolean m_searchUp: a search direction flag.

String m_searchData: string to search for.

The FindDialog constructor creates a super-class non-modal dialog instance titled "Find and Replace." The main tabbed pane, m_tb, is created, and JPanel p1 (the main container of the "Find" tab) receives the m_txtFind1 text field along with a "Find what:" label. This text field is used to enter the target string to be searched for. Note that the Document instance associated with this textbox is stored in the m_docFind instance variable (which will be used to facilitate sharing b! etween another text field).

 

Note: In a more sophisticated implementation one might use editable comboboxes with memory in place of text fields, similar to those discussed in the final examples of chapter 9.

Two checkboxes titled "Whole words only" and "Match case," and two radio buttons titled "Search up" and "Search down" (initially selected) are placed at the bottom of the p1 panel. These components are surrounded by a titled "Options" border. Two JButtons titled "Find Next" and "Close" are placed at the right side of the panel. The first button calls our findNext() method (see below) when pressed. The second button hides the dialog. Finally the p1 panel is added to m_tb with a tab title of "Find."

JPanel p2 (the main container of the "Replace" tab) receives the m_txtFind2 text field along with a "Find what:" label. It als receives another pair labeled "Replace:". An instance of our custom layout manager, DialogLayout (discussed in chapter 4), is used to lay out these text fields and corresponding labels without involving any intermediate containers. The same layout is used in the "Find" panel. We also synchronize the preferred size of the two panels to avoid movement of the mimicked components when a new page is activated.

Note that the m_docFind data object is set as the document for the m_txtFind2 text field. This ensures consistency between the two different "Find" text fields in the two tabbed panels.

Two checkboxes and two radio buttons are placed at the bottom of the panel to control the replacement options. They have identical meaning and representation as the corresponding four controls in the "Find" panel, and to ensure consistency between them, the data models are shared between each ‘identical’ component.

Three JButtons titled "Replace," "Replace All," and "Close" are placed at the right side of the panel. The "Replace" button makes a single call to our findNext() method (discussed below) when pressed. The "Replace All" button is associated with an actionPerformed() method which repeatedly invokes findNext() to perform replacement until it returns -1 to signal an error, or 0 to signal that no more replacements can be made. If an error occurs this method returns, the actionPerformed() method simple retur! ns (since an error will be properly reported to the user by the findNext() method). Otherwise the number of replacements made is reported to the user in a JOptionPane message dialog. The "Close" button hides the dialog. Finally the p2 panel is added to the m_tb tabbed pane with a tab title of "Replace."

Since this is a non-modal dialog, the user can freely switch to the main application frame and return back to the dialog while each remains visible (a typical find-and-replace feature). Once the user leaves the dialog he/she can modify the document’s content, or move the caret position. To account for this, we add a WindowListener to the dialog whose windowActivated() method sets m_searchIndex to -1. This way, the next time findNext() is called (see below) the search data will be re-initialized, allowing the search to continue as expected, corresponding to the new caret position and document! content.

The setSelectedIndex() method activates a page with the given index and makes this dialog visible. This method is intended mostly for use externally by our app when it wants to display this dialog with a specific tab selected.

The findNext() method is responsible for performing the actual find and replace operations. It takes two arguments:

boolean doReplace: if true, find and replace, otherwise just find.

boolean showWarnings: if true, display a message dialog if target text cannot be found, otherwise do not display a message.

findNext() returns an int result with the following meaning:

-1: an error has occurred.

0: the target text cannot be found.

1: a find or find and replace was completed successfully.

The m_searchIndex == -1 condition specified that the text to be searched through must be re-calculated. In this case we store the portion of text from the beginning of the document to the current caret position if we are searching up, or between the current caret position and the end of the document if we are searching down. This text is stored in the m_searchData instance variable. The current caret position is stored in the m_searchIndex variable.

 

Note: This solution may not be adequate for large documents. However, a more sophisticated solution would take us too far from the primary goal of this example.

Figure 20.11 Usage of instance variables for searching up and down through document text.

<<file figure20-11.gif>>

The text to search for is retrieved from the m_docFind shared Document. If the case-insensitive option is selected, both the m_searchData text and the text to search for are converted into lower case. If the "Whole words only" option is selected, we check whether the text to search for contains any separator characters defined in our Util utilities class (see below).

 

Note: If a given String is already completely in lower or upper case, the toLowerCase() (or toUpperCase()) method returns the original String without creating a new object.

After this, if the doReplace parameter is true, we retrieve the replacement text from our m_docReplace Document. At this point we're ready to actually perform a search. We take advantage of existing String functionality to accomplish this:

   if (m_searchUp)
     xStart = m_searchData.lastIndexOf(key, pos-1);
   else
     xStart = m_searchData.indexOf(key, pos-m_searchIndex);

If we are seaching up, we search for the last occurrence of the target string from the current caret position. Otherwise we search for the first occurrence of the target string from the current caret position. If the target string is not found, we cannot continue the search, and a warning is displayed if the showWarnings parameter is true.

This simple scheme is complicated considerably if the "Whole words only" option is selected. In this case we need to verify whether symbols on the left and on the right of a matching region pf text are either word separators defined in our Utils class, or the string lies at the end of the data being searched. If these conditions are not satisfied, we attempt to continue searching, unless the end of the search data is reached.

In any case, if we locate an acceptable match, we select the located text. If the replace option is selected, we replace this selected region with the specified replacement text and then select the new replacement text. In this latter case we also set m_searchIndex to -1 to force the m_searchData variable to be updated. This is necessary for continued searching because the data being searched most likely changes after each replace. The location of the caret also usually changes.

Class Utils

This class provides a simple static utility method and an array of chars representing word separator characters. The isSeparator() method simply checks whether a given character belongs to the static WORD_SEPARATORS char array.

Running the Code

Open an existing RTF file and use the "Edit" menu, or the appropriate keyboard accelerator, to bring up the "Find and Replace" dialog with the "Find" tab selected. Enter some text to search for, select some search options, and press the "Find Next" button. If your target text is found, the matching region will be highlighted in the base document. Press this button again to find subsequent entries (if any). Verify that the "Whole words only" and "Match case" options function as discussed above. Change focus to the main application window and modify the document and/or change the caret position. Return to the "Find and Replace" dialog and note that the search continues as expected.

Select the "Replace" tab and verify that the state of all search options, including the search target string, are preserved from the "Find" tab (and vice versa when switching back). Enter a replacement string and verify that the "Replace" and "Replace All" buttons work as expected.

 

20.9 Word Processor: part IX - Spell checker [using JDBC and SQL]

All modern word processor applications worth mentioning offer tools and utilities which help the user in finding grammatical and spelling mistakes in a document. In this section we will add spell-checking to our word processor application. To do this we will need to perform some of our own multithreading, and communicate with JDBC. We will use a simple database with one table, Data, which has the following structure:

Name Type Description

word String A single English word

soundex String A 4-letter SOUNDEX code

An example of this database, populated with words from several Shakespeare comedies and tragedies, is provided in this example’s directory: Shakespeare.mdb. (This database must be a registered database in your database manager prior to using it. This is not a JDBC tutorial, so we’ll skip the details.)

Figure 20.12 WordProcessor with spell checking functionality.

<<file figure20-12.gif>>

 

Note: The custom SOUNDEX algorithm used in this example hashes words for efficiency by using a simple model which approximates the sound of the word when spoken. Each word is reduced to a four character string, the first character being an upper case letter and the remaining three being digits.

The Code: WordProcessor.java

see \Chapter20\9

import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.*;
import java.sql.*;

import javax.swing.*;
import javax.swing.text.*;
import javax.swing.event.*;
import javax.swing.border.*;
import javax.swing.text.rtf.*;
import javax.swing.undo.*;

import dl.*;

public class WordProcessor extends JFrame 
{
  // Unchanged code from section 20.8

  protected JMenuBar createMenuBar() {

    // Unchanged code from section 20.8

    JMenu mTools = new JMenu("Tools");
    mTools.setMnemonic('t');

    Action spellAction = new AbstractAction("Spelling...", new ImageIcon("tools_abc.gif")) 
    { 
      public void actionPerformed(ActionEvent e) {
        SpellChecker checker = new SpellChecker(WordProcessor.this);
        WordProcessor.this.setCursor(Cursor.getPredefinedCursor(
          Cursor.WAIT_CURSOR));
        checker.start();
      }
    };
    item =  mTools.add(spellAction);  
    item.setMnemonic('s');
    item.setAccelerator(KeyStroke.getKeyStroke(
      KeyEvent.VK_F7, 0));
    menuBar.add(mTools);

    m_toolBar.addSeparator();
    m_toolBar.add(new SmallButton(spellAction, 
      "Spell checker"));

    getContentPane().add(m_toolBar, BorderLayout.NORTH);
        
    return menuBar;
  }

  // Unchanged code from section 20.8
}

class OpenList extends JPanel
 implements ListSelectionListener, ActionListener
{
  // Unchanged code from section 20.6

  public OpenList(String title, int numCols) {
    setLayout(null);
    m_title = new JLabel(title, JLabel.LEFT);
    add(m_title);
    m_text = new JTextField(numCols);
    m_text.addActionListener(this);
    add(m_text);
    m_list = new JList();
    m_list.setVisibleRowCount(4);
    m_list.addListSelectionListener(this);
    m_scroll = new JScrollPane(m_list);
    add(m_scroll);
  }

  public void appendResultSet(ResultSet results, int index, 
   boolean toTitleCase)
  {
    m_text.setText("");
    DefaultListModel model = new DefaultListModel();
    try {
      while (results.next()) {
        String str = results.getString(index);
        if (toTitleCase)
          str = Utils.titleCase(str); 
        model.addElement(str);
      }
    }
    catch (SQLException ex) {
      System.err.println("appendResultSet: "+ex.toString());
    }
    m_list.setModel(model);
    if (model.getSize() > 0)
      m_list.setSelectedIndex(0);
  }

  // Unchanged code from section 20.6
}

// Unchanged code from section 20.6

class SpellChecker extends Thread
{
  protected static String SELECT_QUERY = 
    "SELECT Data.word FROM Data WHERE Data.word = ";
  protected static String SOUNDEX_QUERY = 
    "SELECT Data.word FROM Data WHERE Data.soundex = ";

  protected WordProcessor m_owner;
  protected Connection m_conn;
  protected DocumentTokenizer m_tokenizer;
  protected Hashtable  m_ignoreAll;
  protected SpellingDialog m_dlg;

  public SpellChecker(WordProcessor owner) {
    m_owner = owner;
  }

  public void run() {
    JTextPane monitor = m_owner.getTextPane();
    m_owner.setEnabled(false);
    monitor.setEnabled(false);

    m_dlg = new SpellingDialog(m_owner);
    m_ignoreAll = new Hashtable();

    try {
      // Load the JDBC-ODBC bridge driver
      Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");
      m_conn = DriverManager.getConnection(
        "jdbc:odbc:Shakespeare", "admin", "");
      Statement selStmt = m_conn.createStatement();

      Document doc = m_owner.getDocument();
      int pos = monitor.getCaretPosition();
      m_tokenizer = new DocumentTokenizer(doc, pos);
      String word, wordLowCase;

      while (m_tokenizer.hasMoreTokens()) {
        word = m_tokenizer.nextToken();
        if (word.equals(word.toUpperCase()))
          continue;
        if (word.length()<=1)
          continue;
        if (Utils.hasDigits(word))
          continue;
        wordLowCase = word.toLowerCase();
        if (m_ignoreAll.get(wordLowCase) != null)
          continue;
                
        ResultSet results = selStmt.executeQuery(
          SELECT_QUERY+"'"+wordLowCase+"'");
        if (results.next())
          continue;

        results = selStmt.executeQuery(SOUNDEX_QUERY+
          "'"+Utils.soundex(wordLowCase)+"'");
        m_owner.setSelection(m_tokenizer.getStartPos(), 
          m_tokenizer.getEndPos(), false);
        if (!m_dlg.suggest(word, results))
          break;
      }

      m_conn.close();
      System.gc();
      monitor.setCaretPosition(pos);
    }
    catch (Exception ex) {
      ex.printStackTrace();
      System.err.println("SpellChecker error: "+ex.toString());
    }
        
    monitor.setEnabled(true);
    m_owner.setEnabled(true);
    m_owner.setCursor(Cursor.getPredefinedCursor(
      Cursor.DEFAULT_CURSOR));
  }

  protected void replaceSelection(String replacement) {
    int xStart = m_tokenizer.getStartPos();
    int xFinish = m_tokenizer.getEndPos();
    m_owner.setSelection(xStart, xFinish, false);
    m_owner.getTextPane().replaceSelection(replacement);
    xFinish = xStart+replacement.length();
    m_owner.setSelection(xStart, xFinish, false);
    m_tokenizer.setPosition(xFinish);
  }

  protected void addToDB(String word) {
    String sdx = Utils.soundex(word);
    try {
      Statement stmt = m_conn.createStatement();
      stmt.executeUpdate(
        "INSERT INTO DATA (Word, Soundex) VALUES ('"+
        word+"', '"+sdx+"')");
    }
    catch (Exception ex) {
      ex.printStackTrace();
      System.err.println("SpellChecker error: "+ex.toString());
    }
  }

  class SpellingDialog extends JDialog
  {
    protected JTextField  m_txtNotFound;
    protected OpenList    m_suggestions;

    protected String   m_word;
    protected boolean  m_continue;

    public SpellingDialog(WordProcessor owner) {
      super(owner, "Spelling", true);

      JPanel p = new JPanel();
      p.setBorder(new EmptyBorder(5, 5, 5, 5));
      p.setLayout(new BoxLayout(p, BoxLayout.X_AXIS));
      p.add(new JLabel("Not in dictionary:"));
      p.add(Box.createHorizontalStrut(10));
      m_txtNotFound = new JTextField();
      m_txtNotFound.setEditable(false);
      p.add(m_txtNotFound);
      getContentPane().add(p, BorderLayout.NORTH);
            
      m_suggestions = new OpenList("Change to:", 12);
      m_suggestions.setBorder(new EmptyBorder(0, 5, 5, 5));
      getContentPane().add(m_suggestions, BorderLayout.CENTER);

      JPanel p1 = new JPanel();
      p1.setBorder(new EmptyBorder(20, 0, 5, 5));
      p1.setLayout(new FlowLayout());
      p = new JPanel(new GridLayout(3, 2, 8, 2));
            
      JButton bt = new JButton("Change");
      ActionListener lst = new ActionListener() { 
        public void actionPerformed(ActionEvent e) {
          replaceSelection(m_suggestions.getSelected());
          m_continue = true;
          setVisible(false);
        }
      };
      bt.addActionListener(lst);
      bt.setMnemonic('c');
      p.add(bt);

      bt = new JButton("Add");
      lst = new ActionListener() { 
        public void actionPerformed(ActionEvent e) {
          addToDB(m_word.toLowerCase());
          m_continue = true;
          setVisible(false);
        }
      };
      bt.addActionListener(lst);
      bt.setMnemonic('a');
      p.add(bt);

      bt = new JButton("Ignore");
      lst = new ActionListener() { 
        public void actionPerformed(ActionEvent e) {
          m_continue = true;
          setVisible(false);
        }
      };
      bt.addActionListener(lst);
      bt.setMnemonic('i');
      p.add(bt);

      bt = new JButton("Suggest");
      lst = new ActionListener() { 
        public void actionPerformed(ActionEvent e) {
          try {
            m_word = m_suggestions.getSelected();
            Statement selStmt = m_conn.createStatement();
            ResultSet results = selStmt.executeQuery(
              SELECT_QUERY+"'"+m_word.toLowerCase()+"'");
            boolean toTitleCase = Character.isUpperCase(
              m_word.charAt(0));
            m_suggestions.appendResultSet(results, 1, 
              toTitleCase);
          }
          catch (Exception ex) {
            ex.printStackTrace();
            System.err.println("SpellChecker error: "+
              ex.toString());
          }
        }
      };
      bt.addActionListener(lst);
      bt.setMnemonic('s');
      p.add(bt);

      bt = new JButton("Ignore All");
      lst = new ActionListener() { 
        public void actionPerformed(ActionEvent e) {
          m_ignoreAll.put(m_word.toLowerCase(), m_word);
          m_continue = true;
          setVisible(false);
        }
      };
      bt.addActionListener(lst);
      bt.setMnemonic('g');
      p.add(bt);

      bt = new JButton("Close");
      lst = new ActionListener() { 
        public void actionPerformed(ActionEvent e) {
          m_continue = false;
          setVisible(false);
        }
      };
      bt.addActionListener(lst);
      bt.setDefaultCapable(true);
      p.add(bt);
      p1.add(p);
      getContentPane().add(p1, BorderLayout.EAST);

      pack();
      setResizable(false);
      Dimension d1 = getSize();
      Dimension d2 = owner.getSize();
      int x = Math.max((d2.width-d1.width)/2, 0);
      int y = Math.max((d2.height-d1.height)/2, 0);
      setBounds(x + owner.getX(), 
        y + owner.getY(), d1.width, d1.height);
    }

    public boolean suggest(String word, ResultSet results) {
      m_continue = false;
      m_word = word;
      m_txtNotFound.setText(word);
      boolean toTitleCase = Character.isUpperCase(
        word.charAt(0));
      m_suggestions.appendResultSet(results, 1, toTitleCase);
      show();
      return m_continue;
    }
  }
}

class DocumentTokenizer
{
  protected Document m_doc;
  protected Segment  m_seg;
  protected int m_startPos;
  protected int m_endPos;
  protected int m_currentPos;

  public DocumentTokenizer(Document doc, int offset) {
    m_doc = doc;
    m_seg = new Segment();
    setPosition(offset);
  }

  public boolean hasMoreTokens() {
    return (m_currentPos < m_doc.getLength());
  }

  public String nextToken() {
    StringBuffer s = new StringBuffer();
    try {
      // Trim leading seperators
      while (hasMoreTokens()) {
        m_doc.getText(m_currentPos, 1, m_seg);
        char ch = m_seg.array[m_seg.offset];
        if (!Utils.isSeparator(ch)) {
          m_startPos = m_currentPos;
          break;
        }
        m_currentPos++;
      }
            
      // Append characters
      while (hasMoreTokens()) {
        m_doc.getText(m_currentPos, 1, m_seg);
        char ch = m_seg.array[m_seg.offset];
        if (Utils.isSeparator(ch)) {
          m_endPos = m_currentPos;
          break;
        }
        s.append(ch);
        m_currentPos++;
      }
    }
    catch (BadLocationException ex) {
      System.err.println("nextToken: "+ex.toString());
      m_currentPos = m_doc.getLength();
    }
    return s.toString();
  }

  public int getStartPos() { return m_startPos; }

  public int getEndPos() { return m_endPos; }
    
  public void setPosition(int pos) { 
    m_startPos = pos;
    m_endPos = pos;
    m_currentPos = pos;
  }
}

class Utils
{
  // Unchanged code from section 20.8

  public static String soundex(String word) {
    char[] result = new char[4];
    result[0] = word.charAt(0);
    result[1] = result[2] = result[3] = '0';
    int index = 1;
        
    char codeLast = '*';
    for (int k=1; k<word.length(); k++) {
      char ch = word.charAt(k);
      char code = ' ';
      switch (ch) {
        case 'b': case 'f': case 'p': case 'v': 
          code = '1';
          break;
        case 'c': case 'g': case 'j': case 'k': 
        case 'q': case 's': case 'x': case 'z': 
          code = '2';
          break;
        case 'd': case 't': 
          code = '3';
          break;
        case 'l': 
          code = '4';
          break;
        case 'm': case 'n': 
          code = '5';
          break;
        case 'r': 
          code = '6';
          break;
        default:
          code = '*';
          break;
      }
      if (code == codeLast)
        code = '*';
      codeLast = code;
      if (code != '*') {
        result[index] = code;
        index++;
        if (index > 3)
          break;
      }
    }
    return new String(result);
  }

  public static boolean hasDigits(String word) {
    for (int k=1; k<word.length(); k++) {
      char ch = word.charAt(k);
      if (Character.isDigit(ch))
        return true;
    }
    return false;
  }

  public static String titleCase(String source) {
    return Character.toUpperCase(source.charAt(0)) + 
      source.substring(1);
  }
}

Understanding the Code

Class WordProcessor

This class now imports the java.sql package to make use of JDBC functionality. The createMenuBar() method now creates a new menu titled "Tools," which contains one menu item titled "Spelling..." This menu item can be invoked with keyboard accelerator F7, or by pressing the coresponding toolbar button. When selected, this new menu item creates and starts the SpellChecker thread (see below), passing a reference to the main application frame as parameter.

Class OpenList

This custom component receives new functionality for use in our new spell checker dialog. First, we add a new constructor which assigns a given number of columns to the text field, and does not initialize the list component.

Second, we add the appendResultSet() method which populates the list component with the data supplied in the given ResultSet instance at the given position. If the third parameter is set to true, this tells the method to convert all string data to the ‘title case’ (which means that the first letter is in upper case, and the rest of the string is unchanged). This is accomplished through use of our new titleCase() method in our Utils class (see below).

Class SpellChecker

This class extends Thread to perform spell checking of the current document from the current caret position moving downward. Two class variables are declared (their use will become more clear below):

String SELECT_QUERY: SQL query text used to select a word equal to a given string.

String SOUNDEX_QUERY: SQL query text used to select a word matching a given SOUNDEX value.

Five instance variables are declared:

WordProcessor m_owner: a reference to the main application frame.

Connection m_conn: JDBC connection to a database.

DocumentTokenizer m_tokenizer: a custom object used to retrieve each word in a document.

Hashtable m_ignoreAll: a collection of words to ignore in a search, added to with the "Ignore All" button.

SpellingDialog m_dlg: our custom dialog used for processing spelling mistakes.

The SpellChecker constructor takes a reference to the application’s frame as a parameter and stores it in the m_owner instance variable.

The run() method is responsible for the most significant activity of this thread. To prevent the user from modifying the document during spell checking we first disable the main application frame and our text pane contained within it.

 

Note: Unlike AWT, Swing containers do not disable their child components when they themselves are disabled. It is not clear whether this is a bug or an intended feature.

Then we create a new SpellingDialog instance to provide the user interface, and instantiate the m_ignoreAll collection. In a try/catch block we process all JDBC interactions to allow proper handling of any potential errors. This code creates a JDBC connection to our Shakespeare database, retrieves the current caret position, and creates an instance of DocumentTokenizer to parse the document from the current caret position. In a while loop we perform spell checking on each word fetched until there are no more tokens. Words in all upper case, containing only one letter, or containing digits, are skipped (this behavior can easily be customized). Then we convert the word under examination to lower case and search for it in the m_ignoreAll collection. If it is not found, we try to find it in the database. If the SQL query does not return any results, we try to locate a similar word in the database with the same SOUNDEX value to suggest to the user in the dialog. The word in question is then selected in our text pane to show the user which word is currently under examination. Finally we call our SpellingDialog’s suggest() method to request that the user make a decision about what to do with this word. If the suggest() method returns false, the user has chosen to terminate the spell checking process, so we exit the loop. Once outside the loop w! e close the JDBC connection, restore the original caret posit! io! n, explicitly call the garbage collector, and reenable the main application frame and our text pane editor contained within it.

The following two methods are invoked by the SpellingDialog instance associated with this SpellChecker:

replaceSelection() is used to replace the most recently parsed word by the DocumentTokenizer instance, with the given replacement string.

addToDB() adds a given word, and it’s SOUNDEX value, to the database by executing an insert query.

Class SpellChecker.SpellingDialog

This inner class represents a dialog which prompts the user to verify or correct a certain word if it is not found in the database. The user can select one of several actions in response: ignore the given word, ignore all occurrences of that word in the document, replace that word with another word, add this word to the database and consider it correct in any future matches, or cancel the spell check. Four instance variables are declared:

JTextField m_txtNotFound: used to display the word under investigation.

OpenList m_suggestions: editable list component to select or enter a replacement word.

String m_word: the word under investigation.

boolean m_continue: a flag indicating that spell checking should continue.

The SpellingDialog constructor places the m_txtNotFound component and corresponding label at the top of the dialog window. The m_suggestions OpenList is placed in the center, and six buttons discussed below, are grouped to the right.

The button titled "Change" replaces the word under investigation with the word currently selected in the list or entered by the user. Then it stores true in the m_continue flag and hides the dialog window. This terminates the modal state of the dialog and makes the show() method return, which in turn allows the program’s execution to continue (recall that modal dialogs block the calling thread until they are dismissed).

The button titled "Add" adds the word in question to the spelling database. This word will then be considered correct in future queries. In this way we allow the spell checker to "learn" new words (i.e. add them to the dictionary).

The button titled "Suggest" populates the m_suggestions list with all SOUNDEX matches to the word under investigation. This button is intended for use in situations where the user is not satisfied with the initial suggestions.

The button titled "Ignore" simply skips the current word and continues spell checking the remaining text.

The button titled "Ignore All" does the same as the "Ignore" button, but also stores the word in question in the collection of words to ignore, so the next time the spell checker finds this word it will be deemed correct. The difference between "Ignore All" and "Add" is that ignored words will only be ignored during a single spell check, whereas words added to the database will persist as long as the database data does.

The button titled "Close" stores false in the m_continue flag and hides the dialog window. This results in the termination of the spell checking process (see the suggest() method).

The suggest() method is used to display this SpellingDialog each time a questionable word is located during the spell checking process. It takes a String and a ResultSet containing suggested substitutions as parameters. It sets the text of the m_txtNotFound component to the String passed in, and calls appendResultSet() on the OpenList to display an array of suggested corrections. Note that t! he first character of these suggestions will be converted to upper case if the word in question starts with a capital letter. Finally, the show() method displays this dialog in the modal state. As soon as this state is terminated by one of the push buttons, or by directly closing the dialog, the suggest() method returns the m_continue flag. If this flag is set to false, this indicates that the calling program should terminate the spell checking cycle.

Class DocumentTokenizer

This helper class was built to parse the current text pane document. Unfortunately we cannot use the standard StreamTokenizer class for this purpose, because it provides no way of querying the position of a token within the document (we need this information to allow word replacement). Several instance variables are declared:

Document m_doc: a reference to the document to be parsed.

Segment m_seg: used for quick delivery of characters from the document being parsed.

int m_startPos: the start position of the current word from the beginning of the document.

int m_endPos: the end position of the current word from the beginning of the document.

int m_currentPos: the current position of the parser from the beginning of the document.

The DocumentTokenizer constructor takes a reference to the document to be parsed and the offset to start at as parameters. It initializes the instance variables described above.

The hasMoreTokens() method returns true if the current parsing position lies within the document.

The nextToken() method extracts the next token (a group of characters separated by one or more characters defined in the WORD_SEPARATORS array from our Utils class) and returns it as a String. The positions of the beginning and the end of the token are stored in the m_startPos and m_endPos instance variables respectively. To access a portion of document text with the least possible overhead we use the Document.getText() method which takes three parameters: offse! t from the beginning of the document, length of the text fragment, and a reference to an instance of the Segment class (recall from chapter 19 that the Segment class provides an efficient means of directly accessing an array of document characters).

We look at each character in turn, passing over separator characters until the first non-separator character is reached. This position is marked as the beginning of a new word. Then a StringBuffer is used to accumulate characters until a separator character, or the end of document, is reached. The resulting characters are returned as a String.

 

Note: This variant of the getText() method gives us direct access to the characters contained in the document through a Segment instance. These characters should not be modified.

 

Class Utils

Three new static methods are added to this class. The soundex() method calculates and returns the SOUNDEX code of the given word. To calculate that code we use the first character of the given word and add a three-digit code that represents the first three remaining consonants. The conversion is made according to the following table:

Code Letters

1 B,P,F,V

2 C,S,G,J,K,Q,X,Z

3 D,T

4 L

5 M,N

6 R

* (all others)

The hasDigits() method returns true if a given string contains digits, and the titleCase() method converts the first character of a given string to upper case.

Running the Code

Open an existing RTF file and try running a complete spell check. Try adding some words to the dictionary and use the "Ignore All" button to avoid questioning a word again during that spell check. Try using the "Suggest" button to query the database for more suggestions based on our SOUNDEX algorithm. Click the "Change" button to accept a suggestion or a change typed into the text field. Click the "Ignore" button to ignore the current word being questioned.

 

Note: The Shakespeare vocabulary database supplied for this example is neither complete nor contemporary. It does not include such words as "software" or "Internet." However, you can easily add them, when encountered during a spell check, by clicking the "Add" button.