The Swing Text Components

Java > Core SWING advanced programming > 1. THE SWING TEXT COMPONENTS: CREATING CUSTOMIZED INPUT FIELDS > Adding Functionality to the Basic Text Components

 

Adding Functionality to the Basic Text Components

The Swing text components provide a lot of functionality. However, if you understand the way in which they are built, you can get much more out of them either by subclassing the components themselves or by reimplementing some of the pieces that make them up. This section looks first at the architecture of the text components. When you've seen the overall picture, you'll then see how to create extended text components that, for example, validate input as it is being typed by the user.

Text Component Architecture

Figure 1-10 shows the major pieces that make up a text component. This view is a logical one, rather than a class-based one. The diagram shows an imaginary document containing three paragraphs held in the Document object, represented lower left. The paragraphs are delimited by newline characters. The actual layout of this text on the screen is shown in the top left. Here, the text is split into three paragraphs aligned vertically above each other and the content of the first paragraph has been selected by dragging the mouse over it. The cursor is assumed to be positioned after the word "paragraph."

Figure 1-10. Text component architecture.
graphics/01fig10.gif

The selected text is highlighted using an object called the highlighter, which draws directly onto the component's drawing surface. The highlighter is represented by the shaded area over the text of the first paragraph. In front of the component surface is a logical representation of the views that actually render the text onto the screen. Here, there is a view for each paragraph. As you'll see in Chapter 3, there are more view objects in a real text component; the figure shows only the paragraph views for the sake of simplicity. Finally, the cursor is drawn using an object called the caret. Like the highlighter, the caret draws directly on the component's drawing surface.

The Document

The document stores the data that the text component displays and the attributes that are associated with it or with other objects within the document. While JTextField, JPasswordField, and JTextArea can only display text and are limited to a single font and two colors (one for the background and another for the foreground), both JEditorPane and JTextPane support multiple fonts and colors and can host more interesting content, such as icons and components in addition to text. The document holds all the information that describes the content of the component, including the fonts and colors.

The methods that maintain the document contents are defined by the Document interface. The Swing package includes two concrete implementations of this interface that are used by the standard text components. PlainDocument is used to hold text only. It has no methods for supporting the more complex formatting possible with the JEditorPane and JTextPane components, which use a document class called DefaultstyledDocument. In addition to these basic document classes, the HTML support provides a third one, HTMLDocument, which is derived from DefaultstyledDocument. The standard document types and the Document interface are discussed in "Storing Document Content".

The Highlighter

Every text component has an associated highlighter, which is used to show the portion of the text that is currently selected. A highlighter is an implementation of the Highlighter interface, of which the DefaultHighlighter class in the Swing text package is an example. When the component is created, an instance of the class DefaultHighlighter is initialized and associated with it. DefaultHighlighter is a simple implementation that highlights selected text by changing its background color to the selection color of the text component that it is associated with, which is usually extracted from the UIDefaults object of the installed look-and-feel.

Although there is at most one selection in a text component at any given time, and hence at most one area highlighted to show the selection, a highlighter can actually be used to color an arbitrary set of ranges within a document. Each range is controlled by a class implementing the interface Highlighter HighlightPainter. When a Highlighter needs to draw its associated highlights, it uses the corresponding HighlightPainter to do so. The DefaultHighlighter class uses the DefaultHighligher.DefaultHighlightPainter, which fills a rectangular area with a solid color, to do its painting. Highlighters are discussed in more detail in Chapter 3.

The Caret

The caret (otherwise known as the cursor) is principally concerned with displaying the point where text will be inserted in response to keystrokes. By default, the cursor is drawn as a thin vertical line. However, when bi-directional text is in use (see Chapter 5), it is augmented by a small black blob that shows which way the current text region flows.

The caret can be provided by any class that implements the Caret interface. There is a default implementation called DefaultCaret that provides all the functionality required by the standard text components. The caret is not usually look-and-feel dependent, but its color and the rate at which it blinks when its associated component has the keyboard focus are configured in the selected look-and-feels UIDefaults table.

The caret listens to property change, focus, mouse, and mouse motion events that occur within its text component and DocumentEvents from the components Document. By responding to focus and mouse events, the caret is fulfilling part of the controller role of the MVC model. The other part of the controller, the part that handles keyboard input, is implemented by JTextComponent, although much of the work is delegated to the text component's installed editor kit.

Property change events are used to detect a change of Document within the text component so that the caret can register itself as a DocumentListener on whatever is the current Document. DocumentEvents are generated when the content of the text component's model changes; a change in document content can affect the visual position of the caret if text is added or removed before the location of the caret in the document.

Focus events are used to switch the caret on and off. In the default implementation, the caret is visible only when the text component has the focus.

When the component gains the focus, the caret paints itself at its current location; when the focus is lost, the caret is removed.

Finally, mouse events are used to control the caret's position and to manage the selection of text. The caret maintains two distinguished positions within the document referred to as the mark and the dot. The dot always corresponds to the caret's current position, while the mark is the position to which the caret was last moved by clicking the mouse. The range of the document between the mark and the dot is the selection. Suppose that the user clicks the mouse somewhere inside a text component. This position becomes both the mark and the dot. Because the mark and the dot now occupy the same place in the document, there will be no selection. If the user now drags the mouse, the caret follows the mouse by moving the dot to the position reported by the last mouse drag event. The mark, however, is left behind. Because dragging the mouse causes a selection to be created, the caret is responsible for making the selection visible by arranging for the component's highlighter to repaint the background of the selected text. A custom highlighter may, of course, use other means to display the selection, such as underlining the affected range of text. Methods that allow the programmer to control the selection (such as select, selectAll, etc.) also control the caret by setting the dot and mark directly.

If you want, you can change the appearance of the caret by substituting your own implementation of the Caret interface using the JTextComponent setcaret method. There is an example that shows how this is done later in this chapter.

The Editor Kit

Although not shown in Figure 1-10, the editor kit provides much of the functionality of the text components. There are four editor kits provided with the Swing text packages:

  • DefaultEditorKit

  • StyledEditorKit

  • HTMLEditorKit

  • RTFEditorKit

Broadly speaking, an editor kit knows how to handle a document with a particular content type. DefaultEditorKit is used with simple text components (JTextField, JPasswordField, and JTextArea) that use a single font and a single foreground color. StyledEditorKit is more complex and handles arbitrary attributes that can be applied to the whole document, to paragraphs, or to ranges of characters. This editor kit is used by JTextPane and is the superclass of both HTMLEditorKit and RTFEditorKit, which use the text and attribute handling inherited from styledEditorKit to read and display HTML and RTF documents respectively. These two editor kits are typically plugged into a JEditorPane.

The editor kit is responsible for the following:

  • Creating the appropriate type of Document to support the same facilities as the editor kit. DefaultEditorKit creates a PlainDocument; StyledEditorKit and RTFEditorKit both use a DefaultStyledDocument; and HTMLEditorKit uses an HTMLDocument, which is derived from DefaultStyledDocument. The editor kit creates the Document when the text component that it is hosted by is created, unless the programmer has installed a specific Document type by passing it to the component's constructor.

  • Loading document content from an input stream (or reader) or writing content to an output stream (or writer). The editor kit read and write methods are usually invoked indirectly via the same methods of JTextComponent. There is a connection between the editor kit and the format of the data in the input stream. For example, the HTMLEditorKit expects to read an input stream that contains text and HTML tags and writes output in the same format. DefaultEditorKit and StyledEditorKit handle only plain text, so you can't save the formatting applied programmatically to a JTextPane in a file and recover it later, unless you override the StyledEditorKit write method to store the attributes in a private format and implement the read method so that you can restore them.

  • Creating the View objects needed to draw the content of a text component on the screen. View objects are described in "Views" below, and in more detail in Chapter 3.

  • Providing a set of editing actions that can be connected to keyboard actions for use by the end-user. This aspect of the editor kit is described in "Keymaps and Key Bindings".

Views

Text components are drawn by objects derived from the abstract View class. There are several standard subclasses of View that handle various aspects of the drawing process. The views that will be used to draw a particular component depend on the component type and on its actual content. Classes called PlainView and WrappedPlainView are used by JTextArea, while two Others called FieldView and PasswordView are used by JTextField and JpasswordField, respectively. For the simple text components, only one View type is required to manage the whole document and the component itself knows how to create its own specific kind of View.

Core Note

This is not strictly true, because there is an inner class of WrappedPlainView that manages line wrapping. For now, we'll sacrifice strict accuracy in the interests of making it easier to understand exactly how Views are used. You'll see the complete truth in Chapter 3.



Other components have a hierarchy of Views, each of which knows how to display part of the control's content. The most important View objects are listed in Table 1-2.

Table 1-2. Swing View Classes
FieldView and PasswordView These views are used by JTextField and JPasswordField respectively. They draw text in a single-line display area. FieldView is derived from PlainView. PasswordView is a subclass of FieldView that differs only in that it renders each character in the model using the configured echo character instead of the corresponding characters from the text component's selected font.
PlainView and WrappedPlainView PlainView and WrappedPlainView are used by JTextArea and draw two-dimensional text. The PlainView does not wrap lines that are too long to fit within the visible area allocated to them it simply truncates them. WrappedPlainView is a subclass of PlainView that wraps by breaking the text at white-space boundaries when word wrapping style is in use, or when the line is full if it is not.
BoxView BoxView is a "container" view that acts a little like the Swing Box component. Its function is to manage child views by arranging them horizontally or vertically. In practice, this view is used as the main view of a JTextPane or a JEditorPane and manages the entire document, laying out ParagraphViews vertically, one above the other.
ParagraphView As its name implies, this is used to manage the layout of the contents of a paragraph from a JEditor Pane or JTextPane. A ParagraphView creates paragraphs by building rows consisting of LabelViews, IconViews, and ComponentViews as appropriate.
LabelView LabelView is the basic view that actually renders text from the underlying document. A LabelView may or may not map onto a single line of the view area of a text component. However, a LabelView is never more than one row long.
IconView An IconView takes care of displaying inline images stored as instances of the Swing Icon interface.
ComponentView Similarly, ComponentView hosts a Component and places it inline in the document flow. Although you can place an AWT Component in a document, you are more likely to use a lightweight component derived from JComponent to avoid the usual problems inherent in mixing AWT and Swing components.

In the case of JTextPane and JEditorPane, different elements of the document are managed by different View objects. To avoid hard-coding this relationship into the components themselves, the editor kit is used as a factory that creates the appropriate View objects based on the nature of the piece of the document being rendered. For example, in the case of JText-Pane, for each paragraph in the document, styledEditorKit would create a ParagraphView to represent it and this ParagraphView would be added to another View called BoxView that manages the vertical layout of the document. Each piece of the paragraph would, in turn, be managed by a LabelView if it consists of plain text. JTextPane and JEditorPane can also include Icons and arbitrary Components inline with the text. When a StyledEditorKit is installed in one of these components, these objects are drawn by Views of type IconView or a ComponentView. A custom editor kit might have private View objects that render text, Icons, or Components differently and would return instances of its own customized objects from its factory method. You'll see an example of a text component that uses a custom View component, along with a more detailed discussion of the way in which Views work, in Chapter 3.

Storing Document Content

Now that you've seen the overall architecture of the text components, it's time to write some real code. The simplest aspect of all the text components is the underlying document model. Even though there are three different document models in the Swing text package, they are all derived from a common base class (called AbstractDocument) and they all fundamentally do the same thing. Basically, the job of the data model is to store the text that will ultimately be displayed on the screen and information that describes (logically, not physically) how it should be formatted. These two aspects are actually neatly separated from each other. In this section, you'll see how the text is stored and how it can be manipulated without regard to the formatting information. Knowing how this works, you'll be able to create, among other things, useful subclasses of JTextField that perform various kinds of input validation, or help users by trying to guess what they want to type by comparing the input keystrokes to a predefined list of possible values. Later, you'll see how the text can be enhanced with attributes such as color and font changes and how paragraph structure can be imposed on top of what would otherwise be linear text.

The Document Interface

The Document interface controls how data that is stored within the text component's model is accessed and updated. In short, it is the interface to the data model. As noted in the previous paragraph, the data model stores both text and attributes and Document provides the hooks needed to retrieve and store both types of information. It does not, however, dictate how that information should be stored that's left to concrete implementations to determine. The methods defined by the Document interface are listed in Table 1-3.

Insertion and Removal of Text

The first five methods in this interface are by far the most important as far as this section is concerned. The insertstring and remove methods respectively allow content to be added to or removed from the data model. At this level, the document is considered to be composed of a sequence of Unicode characters; for convenience, insertstring allows you to add a group of characters together by taking its data in the form of a String rather than as individual characters.

Table 1-3. The Document Interface
 public int getLength(); public void insertString (int offset, String str,     AttributeSet a) throws BadLocationException; public void remove (int offs, int len) throws     BadLocationException; public String getText(int offset, int length) throws     BadLocationException; public void getText(int offset, int length, Segment txt)     throws BadLocationException; public Position getStartPosition(); public Position getEndPosition(); public Position createPosition (int offs) throws     BadLocationException; public Object getProperty (Object key); public void putProperty (Object key, Object value); public void addDocumentListener (DocumentListener listener); public void removeDocumentListener (DocumentListener     listener); public void addUndoableEditListener (UndoableEditListener     listener); public void removeUndoableEditListener (UndoableEditListener     listener); public Element [] getRootElements(); public Element getDefaultRootElement(); public void render (Runnable r); 

Positioning within the data model is specified by using an offset from the beginning of the document. These offsets refer not to the characters themselves, but to the "gaps" between successive characters. Offset 0, for example, refers to the location just before the first character of the document and, if the document contains n characters in all, the position before the last character has offset (n - 1), while the position after the last character is at offset n.

The insertstring method takes an offset as its first parameter. When this method is called, the implementation is expected to place the data in the string passed as the second argument into the data model at the location directly following the given offset. Any data originally at that offset is moved down to offset (offs + length), where length is the number of characters in the supplied String. When inserting content, you can also specify an AttributeSet. As its name suggests, an AttributeSet stores attributes, such as color or font information, that will be associated with the data in the model. Attributes are generally only meaningful for text components derived from JEditorPane; they will be described in the next chapter. When there are no attributes to store, as is the case for JTextField, JPasswordField, and JTextArea, this argument has the value null.

The remove method deletes content from the model, moving whatever follows the removed text up to occupy its original location. Because of this, the model always appears to be compact there are never any unoccupied offsets anywhere in the data.

Both insertstring and remove throw a BadLocationException if you try to address a range of offsets that is not entirely within the model. This means that the starting offset may not be negative and must be less than the number of characters in the model. The length argument must similarly be zero or positive and, in the case of the remove method, it must be such that the value of (offset + length) is less than the length of the model. If you supply a range of offsets that is invalid, the resulting BadLocationException contains the value of the first illegal offset in the range, which can be obtained using the offsetRequested method.

Core Note

As you'll see in later chapters, inserting and removing text has a direct effect on other parts of the document model and on the View components used to render the model onto the screen. At the moment, however, you can forget about those secondary effects, because they are dealt with separately, Views, for example, are updated as a result of events sent from the model when its content changes.



Retrieving Model Content

The two getText methods allow you to inspect the content of the model. The first variant copies the data from the specified range into a newly created String object. While this may be convenient for some purposes, it is not the most efficient way to access the model. The second getText method accepts a Segment object, defined as follows:

 public class Segment {    public char[] array;    public int offset;    public int count;    public Segment();    public Segment(char[] array, int offset, int count);    public String to String(); } 

The second variant of getText is usually a more efficient way to inspect the model, because it often avoids the need to copy the data. To use it, you create a Segment object using the first constructor shown above and simply pass it to getText along with the offset and length of the part of the document that you want access to. The getText method then sets the array, offset, and count members to point to a character array that contains the data in the specified range. The character at the first offset in the specified range can be obtained as follows:

 try {    Segment seg = new Segment();    model.getText(offset, length, seg);    char firstchar = seg.array[seg.offset]; } catch (BadLocationException e) {    // Handle illegal access } 

In some cases, the array member will actually point to the real document content, thus avoiding the need to copy the data out of the model. For this reason, the data returned by this method must not be modified. In other cases, however, a copy of the model content will be returned. Callers of getText should not make any assumptions about whether the data returned is a copy or the real data, because this depends on the implementation of the underlying data storage mechanism.

Positions

Although the insertstring, remove, and getText methods use absolute offsets to specify locations within the model, this is often not a convenient way to manipulate text, mainly because offsets can become invalid when text is added to or removed from the model. If you have a pointer to the start of a paragraph in the form of an offset, you can continue to access the start of the paragraph using that offset provided nothing inserts or removes data in the area that precedes the paragraph. If this does happen, there is no easy way to find the start of the paragraph again.

To make it possible to track logical locations within a document, the model provides Position objects. A Position object is tied to a particular offset in the model that is specified when the createPosition method is called. Once you've got a Position object for a given location, it remains attached to that location even if its offset changes later. To obtain the offset that corresponds to a Position at any given time, you can use the getoffset method. For example, suppose the model contains 100 characters and you want to keep track of the character that is initially located at offset 50. Consider the following code sequence:

 Position paraPos = model.createPosition(50); System.out.println("Initial offset = " + paraPos.getoffset()); model.remove(10, 20); System.out.println("Final offset = " + paraPos.getoffset()); 

The first call to println would show that the Position created by the first line has associated offset 50. After the remove method completes, 20 characters from offsets 10 through 29 will have been removed, so everything that was at offset 30 and higher will have moved down to fill up the gap. In particular, the character initially at offset 50 will have moved to offset 30 and the second call to println would show that the offset in the Position object is now 30. The Position object continues to track the point it was initially associated with regardless of what changes are made to the model.

The getstartPosition and getEndPosition methods create Position objects that always correspond to the start and end of the document respectively. In this respect, they don't actually track a given offset in the document, but rather always stay attached to its start and end, even if text is added at the front of the document or at the end.

Properties

The getProperty and putProperty methods allow arbitrary values addressed by arbitrary keys to be stored in the document model. The various Document implementations use this feature to store information that could be used during rendering or for some other type-specific purpose. For example, PlainDocument holds the default tab spacing that will be used to decide where to move the caret when the TAB key is pressed under the property name tabsize; the associated value being an Integer. More interestingly, the HTMLDocument type uses a common property defined by Document called title to store the value associated with the HTML TITLE tag.

When building custom text components or custom document models, you can create and store your own properties using this mechanism.

Event Handling

The four methods addDocumentListener, removeDocumentListener, addUndoableEditListener, and removeUndoableEditListener are used to allow events within the text model to be monitored. The latter two are part of the undo/redo mechanism that will be covered in Chapter 9. The other two methods allow listeners to receive notification of content changes within the document model, which cause DocumentEvents to be generated.

Core Note

If you're familiar with the AWTtext components, a DocumentEvent is the Swing equivalent of the AWT TextEvent. Unlike TextEvent, which is a class, DocumentEvent is actually an interface. Real DocumentEvents in Swing are instances of the class DefaultDocumentEvent, which implements the DocumentEvent interface. DefaultDocumentEvent is an inner class of AbstractDocument. You'll find more on this in Chapter 9.



A DocumentEvent is created when any of the following occurs:

  • Something is inserted into the model.

  • Something is deleted from the model.

  • The attributes associated with some part of the model are changed.

DocumentEvent has methods that allow you to extract the offset of the first affected character, the length of the region affected, the Document in which the change was made, and the event type, which will be one of DocumentEvent .INSERT, DocumentEvent.REMOVE, and DocumentEvent.CHANGE. These events are used internally by the text component's View objects to update its on-screen representation. The offset and length values can be used to work out exactly which Views are affected by whatever change has been made, which makes it possible to optimize the screen update process.

A typical application might use a DocumentEvent to be notified that some change has occurred in a text component that may require state elsewhere in the application to be updated. For example, suppose the application presents a dialog box consisting of a JTextField and an associated OK button that would dismiss the dialog and cause the content of the text field to be processed, perhaps as a persons name or e-mail address. When the text field is empty, it would not be useful for the button to be active, so a well-written application would create the dialog with the button disabled. As soon as a single character has been typed, however, the content of the text field might be valid, so the button should be enabled. If the user types some characters and then deletes them, leaving the text field empty, the button should be disabled again. The application can implement these semantics by receiving DocumentEvents from the JTextField. Here is how a suitable listener might be registered:

 JTextField textField = new JTextField(20); DocumentListener 1 = new DocumentListener() {    // Implementation not shown }; textField.getDocument().addDocumentListener(1); 

The important thing to note about this code is that the listener has to be registered with the Document, not the text field, because JTextComponent does not have convenience methods to delegate registration to the Document.

The DocumentListener interface is defined as follows:

 public interface DocumentListener extends EventListener {    public void insertUpdate(DocumentEvent e);    public void removeUpdate(DocumentEvent e);    public void changedupdate(DocumentEvent e); } 

In this example, the insertUpdate method would be called when content has been added to the JTextField and it would respond by enabling the OK button. Similarly, removeUpdate is entered when something is deleted from the text field. It would probably get the length of the document content and disable the OK button if the text field were now empty.

Because a DocumentEvent is generated when any change of any kind is made to the content of a text component, you might think that you could listen to DocumentEvents as a way of monitoring characters typed into the text field with a view to checking if they are valid. For example, you might try to implement a text field that only allows uppercase characters to be typed by attaching a listener to the text field and either removing lowercase characters or forcing them to uppercase within the listener code. However, this technique does not work, because the Swing Document implementations do not allow you to change their content from with a DocumentListener. The reason for this is clear if it were allowed, changing the document content from within the DocumentListener would cause another DocumentEvent, which could result in your DocumentListener methods being invoked recursively. As you'll see in the next section, there are better and more direct ways to create text fields that verify or act immediately upon input passed to them, without using DocumentEvents. It is, however, perfectly practical to use these events when the action you take in response does not affect the text field, as is the case with the example of the ok button that you have just seen.

Elements and Rendering

The getRootElements and getDefaultRootElement methods of the Document interface are concerned with maintaining the superstructure that sits over the raw text that allows the more complex text components to display formatted text and mix them with images and components. This aspect of the data model will be covered in Chapter 3.

The render method is a convenience that makes it possible for the text component to be safely drawn onto the screen without the potentially disruptive effects of document updates during the drawing process. This is achieved by locking the document to prevent other threads attempting to change its content or the associated attributes, and then invoking the run method of the Runnable passed to the render method. The Document methods that update the document content are expected to synchronize with the rendering process by obtaining a write lock before making any changes. The locking is implemented within the standard Document implementations, so there is no need to be concerned with the details unless you plan to override the insertString or replace methods as implemented in AbstractDocument.

Implementing Text Fields with Input Validation

JTextField is a perfectly usable general purpose input field, but it doesn't have any means of validating that the user is typing characters that are acceptable when taking into account the way in which the content of the field will actually be used by an application. This is not surprising, of course, because there arc countless ways to use a text field, all of which require different validity checks to be made. Nevertheless, it is possible to adopt a general strategy for the implementation of a text field that provides some kind of input validation, and in this section, you'll see two examples that illustrate how to use the architecture of the Swing text components to get control at the appropriate points to make whatever checks are required.

A Text Field with Limited Input Capacity

For our first example, let's create a text field that imposes an upper limit on the number of characters that can be typed into it. This is a very common requirement in applications that interface with databases with fixed-length fields, because it would not be acceptable to allow the user to try to enter a value whose size exceeds that of the corresponding field in the database. Although the code to check the length could be added to the application, having an off-the-shelf component that can do the job is better from the point of view of code reuse and also creates a better user interface, because it is possible to stop accepting keystrokes when the maximum size of the input field has been reached, rather than allowing the user to type something unacceptable and providing feedback later.

How would you implement such a text field? There are several possible ways to do this. Let's look at three of them.

Limiting Input using a DocumentListener

You've already seen one possible solution earlier in this chapter in connection with DocumentEvents. Perhaps the most obvious thing to do is to create a subclass of JTextField that can be given the maximum field size as an argument to its constructor, which then registers a DocumentListener on its own document. With this approach, each time the user added a character to the text field or removed characters, the DocumentListener would be informed and its insertUpdate method would check how many characters had been typed so far. If the number of characters exceeded the configured limit, the content would be truncated to the maximum size and written back to the model. Unfortunately, we know that this cannot be done because you cannot change the content of the document from within the DocumentListener implementation. Perhaps you could get around this restriction by using the SwingUtilities invokeLater method to defer the modification so that it would be made outside the critical phase of the document update. One problem with this technique is that, as viewed from outside the component, some number of characters would be added to the text component and then taken away again. This is a confusing state of affairs only real document updates should be seen by the user and reflected to any other DocumentListeners that might be registered on the component.

Furthermore, making changes and immediately removing them also needlessly complicates any possible support for undo/redo that you might want to provide for your enhanced text field, because the internal changes will appear as distinct actions from the point of view of the undo mechanism. At worst, this might mean that users will see these phantom changes if they want to make use of the redo mechanism to reverse actual edits on the text field. For example, suppose you implement a bounded-length text field in this way and then add the ability for the user to use CTRL-Z to undo the last edit and CTRL-Y to redo the last undone change. Suppose also that there is a maximum of 10 characters allowed in the input field and that the user inadvertently typed an 11-character string. At this point, the text component intervenes and removes the llth character, leaving 10 characters in the data model. Now suppose the user types CTRL-Z. What should this do? Arguably, it should remove the 10th character typed, because the insertion of the llth character never really happened. However, the sequence of events in the model has actually been:

  1. Type the 10th character.

  2. Type the llth character.

  3. Delete the llth character.

Now, unless the undo mechanism is aware of what is happening under the covers in the enhanced text field, it will respond to CTRL-Z by undoing the deletion of the llth character, creating an 11-character text field again. At this point, the text component's internal DocumentListener will notice that this is illegal and arrange to remove it, leaving the original 10 characters. In other words, CTRL-Z has actually done something within the component, but left its content and appearance unchanged. This is not correct behavior. While it might be possible to devise some way to work around this, it would almost certainly create a complex implementation that is difficult to maintain. Not surprisingly, there is a better way.

Listening to Keyboard Input

If trying to change the content of the model after it has been updated is not an acceptable solution, how about stopping the excess characters getting into the model in the first place? This has got to be a better solution, because it immediately avoids creating document events and undoable edit events for state changes that shouldn't really have happened. One possible way to do this would be to capture the keyboard events that are generated as the user types into the text component. Each time a new character is typed, the keyboard event would be intercepted and the number of characters in the model would be checked. If the model is already full, the newly typed key would be rejected by consuming the event, so that the logic within the text component that would insert the new key into the model would never see it. It is certainly possible to catch and suppress key events in this way, but this approach is also not without problems.

Consider, for example, what your key event handler would actually need to do. In principle, when each key is typed, it would get the length of the model and compare it to the maximum bound of the input field. If they match, the key must be rejected. Well, not quite. What if the user presses the backspace key? This is a special case that should not be blocked, regardless of the current length of the data model. Of course, it is easy to write code that recognizes this particular key and suppresses the usual check. Unfortunately, this is not the only special case. Suppose the programmer chooses to bind some other key or keys to actions that affect the content of the text field? How about if the programmer binds CTRL-U so that it clears the text field? In this particular case, if the user typed 10 characters into a field that accepts 10 characters and then pressed CTRL-U, this would be seen as an illegal llth character and suppressed before it can clear the input field. Should you look for this key combination and write special code for that, too? Because the programmer can, in fact, bind arbitrary keys to arbitrary actions (as you'll see in "Keymaps and Key Bindings" ), it is impossible to hard-wire all of the exceptions into the key event code.

Even if you feel comfortable with creating a bounded text field that doesn't allow custom key bindings that could cause you trouble, there are still other problems to deal with. What happens if the programmer decides to initialize the field by calling the setText method? Suppose the initial value is more than 10 characters long. Should this be allowed? Clearly, it should not. However, this operation does not involve key events, so the key listener, no matter how carefully it is coded, cannot intervene to prevent it. The only possible recourse is to override the JTextField setText method and add the check there, adding a little more code to plug another leak. This is surely not a good design principle.

Unfortunately, even with all of this sticking plaster there is still another problem. As you know, all the Swing text components allow you to paste text from the system clipboard. Suppose the clipboard contains a 100-character string and the user clicks in your text field, and then presses CTRL-V, or whichever key sequence corresponds to the paste action in the selected look-and-feel. What will happen is that all 100 characters will be pasted into the text component's data model via its insertString method, bypassing the key event handler and the (presumably overridden) setText method. In fact, you can't really get around this without overriding the paste method of JTextComponent. Again, this is possible, but the complexity is now surely becoming unacceptable for such a simple feature.

Using the Document Model

The most elegant way to validate the proposed content of a text field was actually touched upon in the previous paragraph when data is pasted into a text component, it is copied directly to the data model. In fact, anything being entered into the text field is also ultimately written to the data model, whether it comes from the keyboard, the clipboard, or via setText. In implementation terms, any text insertion is performed in the Document insertString method, so this is the obvious common place to check what is being placed in the model and to take the appropriate action. Furthermore, this approach also ensures that only appropriate document and undoable edit events are generated, because these events reflect exactly what happens to the model.

Core Note

I am not trying to imply that all custom text components should be implemented by subclassing PlainDocument or some other Document class. For the examples in this section, this is, indeed, the simplest thing to do. However, there are cases in which you can't add all the extra functionality by modifying the model alone. When this is true, despite the complications outlined previously, you will have to do more work in the text component itself. For an example of this, see "A Text Field with Look-Ahead".



Let's see how this might work in practice by looking at the implementation of a subclass of JTextField that allows only a fixed number of characters to be typed into it. In terms of the data model, this translates to there being an upper limit on the number of characters in the model. If anything that gets into the model goes via insertString, it is obviously possible to check that the allowed capacity won't be exceeded by the String passed to insertString by adding appropriate code to that method. Listing 1-5 shows a subclass of PlainDocument with an overridden insertString method that implements this policy.

Because the aim is to create a model that can be used in conjunction with a JTextField, this model is derived from PlainDocument, the document type used by JTextField (and, incidentally by JPasswordField and JTextArea). The first two constructors allow you to create a BoundedPlainDocument with a specific limit, or to set the limit later using the setMaxLength method. The third constructor accepts a maximum length and some initial data in the form of an object of type AbstractDocument.Content. This is, in fact, an interface rather than a class. Up to now, we have assumed that the Document implementation actually stores the data, but this is not quite true. The responsibility for storing the data is delegated to a separate class that implements the Content interface. Swing has two such classes, StringContent and GapContent.

Listing 1-5 A Text Component Data Model with Limited Capacity
 package AdvancedSwing.Chapterl; import javax.swing.text.*; public class BoundedPlainDocument extends PlainDocument {    public BoundedPlainDocument() {       // Default constructor - must use setMaxLength later       this.maxLength = 0;    }    public BoundedPlainDocument(int maxLength) {       this.maxLength = maxLength;    }    public BoundedPlainDocument (          AbstractDocument.Content content,int maxLength) {       super(content);       if (content.length() > maxLength) {          throw new IllegalArgumentException(             "Initial content larger than maximum size");       }       this.maxLength = maxLength;    }    public void setMaxLength(int maxLength) {       if (getLength() > maxLength) {          throw new IllegalArgumentException(             "Current content larger than new maximum size");       }       this.maxLength = maxLength;    }    public int getMaxLength() {       return maxLength;    }    public void insertString(int offset, String str,       AttributeSet a)          throws BadLocationException {       if (str == null) {          return;       }       // Note: be careful here - the content always has a       // trailing newline, which should not be counted!       int capacity = maxLength + 1 - getContent().length();       if (capacity >= str.length()) {          // It all fits          super.insertString(offset, str, a);       } else {          // It doesn't all fit. Add as much as we can.          if (capacity > 0) {             super.insertString(offset, str.substring(0,                                capacity), a);          }          // Finally, signal an error.          if (errorListener != null) {             errorListener.insertFailed(this, offset,                                        str, a);          }       }    }    public void addInsertErrorListener(                                  InsertErrorListener l) {       if (errorListener == null) {          errorListener = l;          return;       }       throw new IllegalArgumentException(             "InsertErrorListener already registered");    }    public void removeInsertErrorListener             (InsertErrorListener l) {       if (errorListener == l) {          errorListener = null;       }    }    public interface InsertErrorListener {       public abstract void insertFailed          (BoundedPlainDocument doc,          int offset, String str, AttributeSet a);    }    protected InsertErrorListener errorListener;                   // Unicast listener    protected int maxLength; } 

The first of these classes stores the content as a character array that grows as the contents size increases. The character array always maps exactly the model's content, so that when data is inserted in the middle the characters at and above the insertion point need to be copied to their new locations to allow new data to be added. Similarly, when anything is deleted, the characters above the deleted region need to be copied down to occupy the newly freed space. GapContent is a more sophisticated class that tries to minimize data copying when content is added or removed by allowing a single gap to exist in the data. The gap is moved around as necessary to preserve the illusion of a contiguous span of characters. Unless you intend to implement your own Content class, you won't need to delve into the details of the implementation of these classes. By default, the Swing Document classes use GapContent to store their data.

Core Note

In Swing 1.0, StringContent was the only Content implementation available.



Most of the code in Listing 1-5 is straightforward and should be readily understandable. When new content is passed to the insertString method, the available space in the model is computed by subtracting the number of characters already in the model from the maximum capacity. Note that the model always includes a trailing newline, which is not counted in our maximum length, so the calculation takes this into account. If the available capacity exceeds the length of the String to be inserted, the insertion is legal and the superclass insertString method is called to carry it out. BoundedPlainDocument does not, of course, need to care about how the data is actually stored because it can simply delegate everything apart from checking the total length to PlainDocument. Removing data from the model is also not a particular concern for this class, because this operation can never cause the maximum size to be exceeded. Therefore, the remove method of PlainDocument doesn't need to be overridden here.

The only design issue of any interest is what to do when the insertion operation is not legal. When the user is typing into a text field that uses this model, each call to insertString will be passed a String containing exactly one character. Obviously, this character will not be added to the model. There are, however, two other things to consider:

  • What should happen if the String has more than one character and there is room for some, but not all, of its content? This might happen if the data is coming from the clipboard or the setText method.

  • Should there be any direct feedback to the user when insertion fails and how can this feedback be generated?

As far as the first point is concerned, there are two possible approaches: either that part of the data that could fit in the model should be added and the rest discarded, or the entire operation should be ignored. Which you choose is largely a matter of taste or the requirements of your particular application. For complete flexibility, you could make this behavior a configurable property of the control and make the decision when the issue arises in the insertString method. The implementation shown in Listing 1-5 chooses to add as much data as will fit and discard the rest. It is, of course, a simple matter to change this to adopt the alternative policy.

The second issue is slightly more difficult to deal with. Looking at this from the user's point of view, there must be some feedback when the text field fills up, otherwise if users are typing without looking at the screen, they may not be aware that not all the characters being typed are actually getting into the text field. The best thing to do is probably to generate a short beep to alert the user to the problem. This could be done directly from the insertString method, because the AWT Toolkit class includes a method that allows you to cause a short beep. However, in design terms this is not very desirable. After all, BoundedPlainDocument is concerned only with storing data it is an MVC model class. User feedback is not a model issue it is the view's job to let the user know what is happening; the model's duty is only to make sure that the view has the information needed to keep the user up-to-date.

Instead of hard-coding a beeping action into the model, this example defines an interface called InsertErrorListener with a single method (insertFailed) that will be called when some (or all) of the characters passed to insertString did not fit in the model. The idea is that the view will use the addInsertErrorListener method to connect an implementation of BoundedPlainDocument.InsertErrorListener to the model and provide whatever feedback is appropriate to the user when its insertFailed method is called. In practice, only the text field that hosts this model is likely to need to be notified of insertion problems, so only one listener is allowed. You can, of course, change this if your requirements turn out to be different. As you can see, if the insertString method cannot add all the data it is given to the model, it checks whether there is a listener registered and calls its insertFailed method if there is one.

That's all there is to the model itself. Listing 1-6 shows a subclass of JTextField that uses BoundedPlainDocument to create a text field with limited capacity.

Listing 1-6 A Fixed-Length Text Input Field
 package AdvancedSwing.Chapter1; import javax.swing.*; import javax.swing.text.*; import java.awt.Toolkit; public class BoundedTextField extends JTextField       implements BoundedPlainDocument.InsertErrorListener {    public BoundedTextField() {       this(null, 0, 0);    }    public BoundedTextField(String text, int columns,                            int maxLength) {        super(null, text, columns);        if (text != null && maxLength == 0) {           maxLength = text.length();        }        BoundedPlainDocument plainDoc =                    (BoundedPlainDocument)getDocument();        plainDoc.setMaxLength(maxLength);        plainDoc.addInsertErrorListener(this);    }    public BoundedTextField(int columns, int maxLength) {       this(null, columns, maxLength);    }    public BoundedTextField(String text, int maxLength) {       this(text, 0, maxLength);    }    public void setMaxLength(int maxLength) {       ((BoundedPlainDocument)getDocument()).setMaxLength(                   maxLength);    }    public int getMaxLength() {       return ((BoundedPlainDocument)                   getDocument()).getMaxLength();    }    // Override to handle insertion error    public void insertFailed(BoundedPlainDocument doc,                   int offset, String str, AttributeSet a) {       // By default, just beep       Toolkit.getDefaultToolkit().beep();    }    // Method to create default model    protected Document createDefaultModel() {       return new BoundedPlainDocument();    }    // Test code    public static void main(String[] args) {       JFrame f = new JFrame("Bounded Text Field Example");       BoundedTextField tf = new BoundedTextField(10, 32);       JLabel l = new JLabel("Type up to 32 characters: ");       f.getContentPane().add(tf, "East");       f.getContentPane().add(l, "West");       f.pack();       f.setVisible(true);    } } 

Again, this code is very easy to understand. BoundedTextField extends JTextField, so it inherits most of its functionality from JTextField. There is a choice of constructors that allow you to specify or default various parameters, the most important of which is the text field's maximum capacity, which can be given to a constructor or set later using the setMaxLength method. However it is set, this value is passed directly to the data model.

For this component to work properly, it must have BoundedPlainDocument as its data model. All the constructors end up calling the second constructor in Listing 1-6, which first invokes the JTextField constructor. One of the things that this constructor does is to create the data model by calling the createDefaultModel method. In JTextField, this returns an instance of PlainDocument. Here, it is overridden to create a BoundedPlainDocument instead. Thus, when the JTextField constructor returns, the BoundedTextField has a BoundedPlainDocument as its model and it can then proceed to set the maximum length of the model from the value passed to whichever constructor was used. If no value was given, the setMaxLength method will need to be called later.

The only other special behavior required of BoundedTextField is to connect some code that handles the failure to insert data. As you can see, BoundedTextField directly implements the BoundedPlainDocument .InsertErrorListener interface, so it simply registers itself to be notified of any errors. If the user tries to insert too much text into the text field, its insertFailed method will be called and the user will get audible feedback. If you want to do something other than this, you can change this behavior by subclassing BoundedTextField and overriding this method.

The BoundedTextField class includes a main method that allows us to see how it works in practice. If you type the command

 java AdvancedSwing.Chapter1.BoundedTextField 

a window containing a text field that will accept up to 32 characters will appear, as shown in Figure 1-11.

Figure 1-11. An input field with limited capacity.
graphics/01fig11.gif

Click in the input field and type as usual until you've typed 32 characters. Now try to add an extra character. You'll find that the character you type does not appear in the text field and you should also hear a beep. You don't have to try to add the character at the end of the field for this to work, of course. To demonstrate this, move the input cursor to the left and try again to type something. Once more, you'll hear a beep and what you typed will not be added to the text field. If you delete some characters from anywhere in the field, you'll find that you can now type again, but only until you reach the 32-character limit.

A more sophisticated test is to fill the text field as before, then select some of the text and type over it, replacing the selection with new characters. You'll find that you can insert as many characters as you selected and no more. Say you select four characters; you'll be able to replace them with four others, but not five. Also, if you substitute three characters for the original four, you'll find that you can add one more anywhere in the text field.

You can verify that pasting data from the clipboard works by selecting text from another window and pasting it into the BoundedTextField. Provided its capacity is not reached, whatever you paste will appear in the text field. However, if you try to paste in too much, the part that doesn't fit will be discarded and you'll hear a beep.

Creating a Numeric Input Control

Almost every application needs an input field that handles numbers. Creating such a field is simple, if you don't want too many frills. The simplest approach is just to use a JTextField and attach an ActionListener. When the user presses return, the ActionListener retrieves the content of the text field and tries to convert it to a number. If this fails, there would usually be some feedback to the user, typically a beep. These days, however, input fields are expected to be a good deal smarter than this. A properly implemented numeric input field would check each character as the user typed it, making sure that it was valid. If the user attempts to type something that would not be acceptable in its current context, it should be rejected immediately, with the usual feedback.

Checking for Valid Numbers

Implementing a smart numeric text field is very similar to the example shown in Listings 1-6 and 1-7. Each character entering the model needs to be checked. In this case, however, the checks are a little more sophisticated than simply ensuring that there is room in the model for the next character. Each time the user types something, the whole content of the input field needs to be looked at, to see if it still constitutes a valid number. This is no simple task it isn't as easy as ensuring that each character is in the range 0 through 9. Consider the following points.

  • Some input fields might be restricted to integers, while others will need to deal with floating point numbers. In the latter case, it would be legal to type a decimal point, whereas this would not be acceptable in an integer field. However, only one decimal point should be allowed, so it is necessary to check not only what is being typed, but also the rest of the input field to see if there is already a decimal point.

  • In some contexts, negative numbers are allowed. A simple numeric input field would implement this by allowing the user to type a minus sign as the first character of the input field and reject this key if it were typed anywhere else. However, as you'll see, this is not always sufficient.

  • The issue of presentation is important for many applications. For example, it is often useful to be able to use group separators so that it is easier to see the exact magnitude of a number. When group separators are in use, an input field containing the value one million might show the string 1,000,000. An intelligent numeric input field would allow (but not require) the user to type the separators and would be able to insert them as necessary when the user finished typing.

In fact, the representation of a number is very locale-dependent and application-dependent. In some places, the decimal separator is a period while in others it is a comma. Some applications use the traditional minus sign to indicate a negative number, while others use a more complex representation. For example, in a financial application, the following might be legal inputs:

 $1,345,623.89 ($1,345,623.89) 

The first number represents a positive amount, the dollar sign being merely decorative as far as the application is concerned, while the second is negative, indicated by the surrounding parentheses. From these two examples, you can see that detecting a negative number is not as simple as looking for a minus sign as the first character. Here, for example, you need to ensure that the number starts with ($ and ends with ), while a positive number must start with $. More complex conventions are, of course, possible.

Fortunately, you don't have to write code that parses input strings like the ones shown above because the code already exists in the DecimalFormat class in the java, text package. To use this class, you create a DecimalFormat object that describes how you want your numbers to look. You can then use the object to create a string representation of the number that adheres to your specification, or you can supply a string that contains a number in that format and have it translated into the equivalent Long or a Double. When parsing a string into a number, DecimalFormat uses some of the information in the formatting string, but doesn't insist that the string strictly conforms to it. As an example, suppose you want the user to type a number that uses group separators around each block of three numbers and displays two places after the decimal point. To do this, you pass the following format string to DecimalFormat:

 #,###.## 

Each # represents an optional decimal digit, the comma represents the group separator, and the period represents the decimal point. In any given locale, the group separator may or may not be a comma, the decimal point need not be a period, and the numeric digits need not be 0 through 9. When using a DecimalFormat configured like this, if you supply it a Double with value 12345678.978, it would produce the string:

 12,345,678.98 

As you can see, the group separators have been added in the appropriate places and the decimal part has been rounded to two places, as required by the format. However, when parsing an input string, more leniency is shown. The group separators, for example, don't need to be supplied and, even if they are present, they don't need to be in the correct places. The user could type any of the following to get the same value:

 1234,56,78.98 1,234,5678.98 12,345,678.98 12345678.98 
Implementing the Model Outline

The simplest way to create a numeric text field with the capabilities that you saw in the previous paragraph is to implement a model that uses the DecimalFormat class to specify what constitutes a valid number. Whenever new content is added to the model, from the keyboard or elsewhere, the model content would be parsed by the DecimalFormat object to ensure that it is valid. This would, of course, allow the programmer to specify what constitutes a valid number by creating a suitable DecimalFormat object, instead of hard-coding the rules into the model. The resulting numeric text field, which will use the model, would be extremely flexible and portable from locale to locale as well as being usable in various different types of application.

Well, that's the theory at least. The implementation is actually a little more complex than that. Ideally, you would want to implement the model's insertString method along these lines:

  • Merge the text being inserted with that already in the model.

  • Parse the resulting text using the model's DecimalFormat object.

  • If it is not valid, reject it.

  • If it is valid, insert it in the model.

Lets look at why this might not work. Suppose the model is configured with a DecimalFormat that requires the use of a - sign as the first character to indicate a negative number and a period as the decimal point, and that the user wants to type the value -1.3 into the control. When the - has been typed, the insertString method will be called with this single character. Because there is nothing in the model yet, a string consisting of just the - sign is passed to the DecimalFormat parse method. Unfortunately, this method accepts only complete numbers, so it rejects the - because it isn't valid. According to the algorithm shown previously, the user's keystroke will not be added to the model. In other words, there is no way, under these conditions, for the user to type a negative number. This isn't the only problem, however. It turns out that even positive numbers don't always work either. If the user tries to type 1.3, the 1 will be accepted (because it is a valid number), but the will not be. This is because the DecimalFormat will be asked to parse 1., which it considers to be illegal.

How can this problem be solved? One possibility is to abandon the idea and write all the parsing code in the insertString method. This is not a very appealing possibility, though, because of the duplication of code between the java.text package and our input control. It also means that enhancements to the DecimalFormat class will not be picked up automatically. The alternative is to catch all of the cases that won't work and ignore the parsing errors that result when the input control is in one of these states. In the two cases shown before, the content would be parsed. When the error occurs, the insertString method would check to see whether it was a real error or a transient one that will be corrected when the user types more into the control. The details of this are a little complicated, but the task is manageable as you can see from the implementation in Listing 1-7.

Listing 1-7 A Document that Accepts Numeric Input
 package AdvancedSwing.Chapter1; import javax.swing.text.*; import java.text.*; public class NumericPlainDocument extends PlainDocument {    public NumericPlainDocument() {       setFormat(null);    }    public NumericPlainDocument(DecimalFormat format) {      setFormat(format);    }    public NumericPlainDocument(AbstractDocument.Content             content, DecimalFormat format) {    super(content);    setFormat(format) ;    try {       format.parseObject(content.getString(0,                          content.length()), parsePos);    } catch (Exception e) {       throw new IllegalArgumentException(                "Initial content not a valid number");    }    if (parsePos.getIndex() != content.length() - 1) {       throw new IllegalArgumentException(                "Initial content not a valid number");       }    }    public void setFormat(DecimalFormat fmt) {       this.format = fmt != null ? fmt :             (DecimalFormat)defaultFormat.clone();       decimalSeparator =                    format.getDecimalFormatSymbols().                    getDecimalSeparator();       groupingSeparator =                    format.getDecimalFormatSymbols().                    getGroupingSeparator();       positivePrefix = format.getPositivePrefix();       positivePrefixLen = positivePrefix.length();       negativePrefix = format.getNegativePrefix();       negativePrefixLen = negativePrefix.length();       positiveSuffix = format.getPositiveSuffix();       positiveSuffixLen = positiveSuffix.length();       negativeSuffix = format.getNegativeSuffix();       negativeSuffixLen = negativeSuffix.length();    }    public DecimalFormat getFormat() {       return format;    }    public Number getNumberValue() throws ParseException {       try {          String content = getText(0, getLength());          parsePos.setlndex(0);          Number result = format.parse(content, parsePos);          if (parsePos.getlndex() != getLength()) {             throw new ParseException(                   "Not a valid number: " + content, 0) ;          }          return result;       } catch (BadLocationException e) {           throw new ParseException("Not a valid number", 0);        }    }    public Long getLongValue() throws ParseException {       Number result = getNumberValue();       if ((result instanceof Long) == false) {          throw new ParseException("Not a valid long", 0);       }       return (Long)result;    }    public Double getDoubleValue() throws ParseException {       Number result = getNumberValue();       if ((result instanceof Long) == false &&            (result instanceof Double) == false) {          throw new ParseException("Not a valid double", 0);       }       if (result instanceof Long) {          result = new Double(result.doubleValue());       }       return (Double)result;    }    public void insertString(int offset, String str,                             AttributeSet a)                   throws BadLocationException {       if (str == null || str.length() ==0) {          return;       }       Content content = getContent();       int length = content.length();       int originalLength = length;       parsePos.setlndex(0);       String targetString = content.getString(0, offset) +             str + content.getString(offset, length -             offset - 1);       // Create the result of inserting the new data,       // but ignore the trailing newline       // Parse the input string and check for errors       do {          boolean gotPositive =                targetString.startsWith(positivePrefix);          boolean gotNegative =                targetString.startsWith(negativePrefix);          length = targetString.length();          // If we have a valid prefix, the parse fails if          // the suffix is not present and the error is          // reported at index 0. So, we need to add the          // appropriate suffix if it is not present at this          // point.          if (gotPositive == true || gotNegative == true) {             String suffix;             int suffixLength;             int prefixLength;             if (gotPositive == true && gotNegative == true) {                // This happens if one is the leading part                // of the other - e.g. if one is "(" and the                // other "(("                if (positivePrefixLen > negativePrefixLen) {                   gotNegative = false;                } else {                   gotPositive = false;                }             }             if (gotPositive == true) {                suffix = positiveSuffix;                suffixLength = positiveSuffixLen;                prefixLength = positivePrefixLen;             } else {                // Must have the negative prefix                suffix = negativeSuffix;                suffixLength = negativeSuffixLen;                prefixLength = negativePrefixLen;             }       //If the string consists of the prefix alone,       //do nothing, or the result won't parse.       if (length == prefixLength) {          break;       }       //We can't just add the suffix, because part of       // it may already be there. For example,       // suppose the negative prefix is "(" and the       // negative suffix is "$)". If the user has       // typed "(345$", then it is not correct to add       // "$)". Instead, only the missing part       // should be added, in this case ")".       if (targetString.endsWith(suffix) == false) {          int i ;          for (i = suffixLength -1; i > 0 ; i--) {             if (targetString.regionMatches(length - i,                      suffix, 0, i)) {                 targetString += suffix.substring(i);                break ;             }          }          if (i == 0) {             // None of the suffix was present             targetString += suffix;          }          length = targetString.length();        }     }    format.parse(targetString, parsePos);    int endIndex = parsePos.getIndex();    if (endIndex == length) {       break; // Number is acceptable    }    // Parse ended early    // Since incomplete numbers don't always parse, try    // to work out what went wrong.    // First check for an incomplete positive prefix       if (positivePrefixLen > 0 &&          endIndex < positivePrefixLen &&          length <= positivePrefixLen &&          targetString.regionMatches(0, positivePrefix,                                     0, length)) {          break; // Accept for now       }       // Next check for an incomplete negative prefix       if (negativePrefixLen > 0 &&          endIndex < negativePrefixLen &&          length <= negativePrefixLen &&          targetString.regionMatches(0, negativePrefix,                                     0, length)) {          break; // Accept for now       }       // Allow a number that ends with the group       // or decimal separator, if these are in use       char lastChar =                targetString.charAt(originalLength - 1);       int decimalIndex =                targetString.indexOf(decimalSeparator);       if (format.isGroupingUsed() &&          lastChar == groupingSeparator &&          decimalIndex == -1) {         // Allow a "," but only in integer part         break;       }       if(format.isParseIntegerOnly() == false &&          lastChar == decimalSeparator &&          decimalIndex == originalLength - 1) {          // Allow a ".", but only one          break;       }       // No more corrections to make: must be an error       if (errorListener != null) {          errorListener.insertFailed(this, offset,                                     str, a);       }       return;    } while (true == false);       // Finally, add to the model       super.insertString(offset, str, a);    }    public void             addInsertErrorListener(InsertErrorListener 1) {    if (errorListener == null) {       errorListener = 1 ;       return;    }    throw new IllegalArgumentException(             "InsertErrorListener already registered");    }    public void          removeInsertErrorListener(InsertErrorListener 1) {       if (errorListener == 1) {          errorListener = null;       }    }    public interface InsertErrorListener {       public abstract void                insertFailed(NumericPlainDocument doc,                int offset, String str, AttributeSet a);    }    protected InsertErrorListener errorListener;    protected DecimalFormat format;    protected char decimalSeparator;    protected char groupingSeparator;    protected String positivePrefix;    protected String negativePrefix;    protected int positivePrefixLen;    protected int negativePrefixLen;    protected String positiveSuffix;    protected String negativeSuffix;    protected int positiveSuffixLen;    protected int negativeSuffixLen;    protected ParsePosition parsePos = new ParsePosition(0);    protected static DecimalFormat defaultFormat =                   new DecimalFormat(); } 

The constructors allow you to create a model initialized in various different ways. You can specify the initial content and the formatter to be used. If you don't specify a formatter, the constructor installs a default DecimalFormat whose characteristics depend on the locale in which the control is used. The setFormat method can be used to change the formatting object at any stage. However, changing the formatter while the control is in use is not recommended because it may be confusing to the user. In practice, this method is most likely to be used to configure the formatter shortly after the model is created, probably as part of the construction of a complete text field that uses it, such as the one that will be shown later in this section.

There are several important features of the formatter that the model implementation needs to be aware of. These are:

  • The character used as the decimal separator (typically a period).

  • The character used as the grouping separator (typically a comma).

  • The prefix used to introduce positive numbers and its length.

  • The suffix used to end positive numbers and its length.

  • The prefix used to introduce negative numbers and its length.

  • The suffix used to end negative numbers and its length.

For convenience, all these attributes are extracted and stored locally for use in the insertstring method. It is important to understand how the prefixes and suffixes work. With normal conventions, a negative number is introduced by a minus sign and has no special terminator, while a positive number has no prefix or suffix. In this case, the formatter would be set as follows:

Positive prefix: (empty string)
Positive suffix: (empty string)
Negative prefix: -
Negative suffix: (empty string)

In this case, any string that does not start with a minus sign is considered to be a positive number. Now consider a different convention in which numbers are always expressed with a leading dollar sign and negative numbers start and end with brackets. You can create such a formatter by using the Decimal-Format setNegativePrefix, setPositivePrefix, setNegativeSuffix, and setPositiveSuffix methods, like this:

 DecimalFormat f = new DecimalFormat(); f.setPositivePrefix("$"); f.setPositiveSuffix(""); f.setNegativePrefix("($"); f.setNegativeSuffix(")"); 

Now, every number must start with either ($ or $ and, furthermore, if it starts with ($, it must end with ). With this setting, the user can't just start by typing a number.

Core Note

Although this example won't implement it, in cases like this you could help the user by looking at the first key pressed and, if it is a number, adding the $ yourself. Similarly, if the user starts with (, you could add the $. This would be implemented in a subclass of NumericPlainDocument that would know the format being applied it would not be appropriate to do this for all numeric text fields.



Implementing the Model The insertString Method

Now let's look in detail at how the insertString method works. This method should simply insert the String that is passed to it into the model, provided that the characters in the String make sense in the context in which they are applied. To check their validity, insertString merges them with whatever is already in the model and passes the result to the formatter's parse method. If what is there is a valid number, parse will process the complete string and the insertString method will just insert the new data in the model. If the data does not look like a number, however, parse will return before it has scanned the whole string. This does not, as we know, mean that the data should be rejected. It just means that some more checking needs to be done to eliminate certain special cases. The ParsePosition object passed to parse records the location within the string at which parsing ended. Sometimes, however, this does not correspond to the location of the formatting error, as you'll see as you read through the description of the insertString method that follows.

Core Note

If you're not interested in the details of the insertString method, you can skip ahead to "Implementing the Numeric Text Field". None of this code will teach you anything about Swing text components.



The first part of insertstring deals with the handling of sign prefixes and suffixes. Here is the issue that it is trying to deal with. Suppose, as in this case, that all numbers must start with ($ and end with ). Suppose also that the user has typed ($4 so far. If you pass this to the formatters parse method, you'll get an error, because it expects the opening negative prefix to be balanced with the corresponding suffix. To ensure that this string gets parsed properly, the suffix needs to be added. But what is the point of this? If you know that this number will fail for this reason, why bother compensating for it? The issue is that the parse method doesn't tell you exactly why the scan failed. In this case, the ParsePosition object would indicate a failure at position 0 the start of the string. That's not very useful typing the string a would produce the same result. Because you can't distinguish lack of a suffix from any other error at position 0, the only way you can determine whether the number is valid is to add the suffix before invoking parse. This will allow parse to concentrate on other errors in the number.

Even doing that is not simple. First, you have to check whether there actually is a positive or negative prefix present and work out which it is. The obvious way to do this is to look at the start of the string. In the example we are using here, the positive prefix would be considered to be present if it started with $ and the negative prefix if it started with ($, so there is no ambiguity. However, what if the positive prefix were (+ and the negative prefix ( and the user types (+4? In this case, simply looking at the start of the string would indicate that both prefixes are present. To remove the ambiguity, if both prefixes appear to be present, we take it that the longer of the two is actually in use. This correctly chooses the positive prefix.

When you've worked out the correct prefix, you just select the corresponding suffix and add it. That works for the case you've seen so far, but it isn't always the right thing to do. At some stage, the user will get as far as actually typing the suffix. Now it wouldn't be correct to add all of the suffix. Returning to our original example, if the user has typed ($4), the whole suffix has been supplied, but the logic so far would have us add the suffix again, giving ($4) ). To avoid this, we do the obvious thing: look at the end of the string and work out whether some of the suffix has been added. If it has, all that is necessary is to append the rest of the suffix. If the negative suffix were ) -, and the user had typed ($4), only the - would now be added.

Once the matter of a balancing suffix has been dealt with, the modified string can be passed to the parse method. If the number between the prefix and suffix (if they exist) is valid, parse will complete normally. However, if there is anything wrong with the number, you need to work out what it is and decide whether it is a real error or just the result of the user not having typed enough of the number yet for it to be valid.

There are several possibilities:

  • The user has typed some, but not all, of the positive or negative prefix.

  • Grouping is in use and the user has just typed the grouping separator.

  • The user has just typed the decimal separator.

Recognizing the first case is simple because both prefixes are known. Care needs to be taken if one of the prefixes is the empty string, because every string starts with an empty string. If the start of a nonempty prefix is present, and the string consists entirely of that segment, the parse error should be ignored and the string inserted into the model. Note that, in these cases, because the prefix is incomplete, it won't have been recognized by the processing carried out before the parse method, so the corresponding suffix won't have been appended.

The other two cases are just as straightforward. A string that ends in a decimal or grouping separator is not parsed as valid so, for example, the strings 4. or 4, are not seen as legal and return an error. It looks like you can recognize this by checking the last character in the string, but a little care is needed. Consider what happens when ($4. is typed in by the user. Because this string starts with a negative prefix, it is amended to ($4.) before being presented to the parse method and now the decimal separator does not appear at the end of the string! The code actually checks the character that was at the end of the string before the suffix was appended.

Even this is not the end, though. There can only be one decimal separator in a number. The parse method will detect that ($4.4.) is illegal and it will indicate that the second period is in error. To avoid letting this through, the code also checks whether there are any more decimal separators in the number and rejects the number if there are. Finally, if grouping is in use, the grouping separator can only be used to the left of the decimal separator in other words, ($3 .123, is not valid. Again, parse will indicate that the , is in error, as it would with ($3,. To reject the first and accept the second, the code checks whether there is a decimal separator and, if there is, it is an error.

Retrieving the Model Value

Having dealt with the issue of checking the model's input to ensure that it is valid, the remaining facility that is required is to extract the value as a number. The getNumberValue, getLongValue, and getDoubleValue methods are provided to return the value as a Number, a Long, or a Double as appropriate. The getNumberValue method uses the formatters parse method (which returns a Long object if the content represents a value without a fractional part or a Double if it does not) and just returns whatever parse returns. However, even though the user input is carefully checked, the value in the model might still not be a valid number. For example, if the user types ($ and presses RETURN, parse will fail. In this case, getNumberValue will throw a ParseException.

If you want the field content as a Long or a Double, you can use get-LongValue or getDoubleValue. These methods both use getNumberValue to get the value as a Number, and then do the appropriate conversion: getDoubleValue returns the value directly if it is a Double, or creates a new Double if it is a Long. However, getLongValue is slightly different: If the result is a Long, it returns it, but if it is a Double, it throws a ParseException because converting the Double to a Long would result in a loss of information.

Implementing the Numeric Text Field

With the model implemented, it is a simple matter to wrap it with a suitable subclass of JTextField to produce a numeric text field, as shown below in Listing 1-8.

Listing 1-8 A Numeric Text Field
 package AdvancedSwing.Chapterl; import javax.swing.*; import javax.swing.text.*; import java.awt.*; import java.awt.event.*; import java.text.*; public class NumericTextField extends JTextField       implements NumericPlainDocument.InsertErrorListener {    public NumericTextField() {       this(null, 0, null);    }    public NumericTextField(String text, int columns,                DecimalFormat format) {       super(null, text, columns);       NumericPlainDocument numericDoc =                (NumericPlainDocument)getDocument();       if (format != null) {          numericDoc.setFormat(format);       }       numericDoc.addInsertErrorListener(this);    }    public NumericTextFieldfield (int columns,                DecimalFormat format) {       this(null, columns, format);    }    public NumericTextField(String text) {       this(text, 0, null);    }    public NumericTextField(String text, int columns) {       this(text, columns, null);    }    public void setFormat(DecimalFormat format) {                ((NumericPlainDocument)                getDocument()).setFormat(format);    }    public DecimalFormat getFormat() {                return ((NumericPlainDocument)                getDocument()).getFormat();    }    public void formatChanged() {       // Notify change of format attributes.       setFormat(getFormat());    }    // Methods to get the field value    public Long getLongValue() throws ParseException {                return ((NumericPlainDocument)                getDocument()).getLongValue();    }    public Double getDoubleValue() throws ParseException {                return ((NumericPlainDocument)                getDocument()).getDoubleValue();    }    public Number getNumberValue() throws ParseException {                return ((NumericPlainDocument)                getDocument()).getNumberValue();    }    // Methods to install numeric values    public void setValue(Number number) {       setText(getFormat().format(number));    }    public void setValue(long 1) {       setText(getFormat().format(1));;    }    public void setValue(double d) {       setText(getFormat().format(d));    }    public void normalize() throws ParseException {       // format the value according to the format string       setText(getFormat().format(getNumberValue() ) ) ;    }    // Override to handle insertion error    public void insertFailed(NumericPlainDocument doc,                   int offset, String str,                   AttributeSet a) {       //By default, just beep       Toolkit.getDefaultToolkit().beep();    }    // Method to create default model    protected Document createDefaultModel() {       return new NumericPlainDocument();    }    // Test code    public static void main(String[] args) {       DecimalFormat format =                new DecimalFormat("#,###.###");       format.setGroupingUsed(true);       format.setGroupingSize(3) ;       format.setParseIntegerOnly(false);       JFrame f = new JFrame("Numeric Text Field Example");       final NumericTextField tf =                new NumericTextField(10, format);       tf.setValue((double)123456.789) ;       JLabel lbl = new JLabel("Type a number: ");       f.getContentPane().add(tf, "East");       f.getContentPane().add(lbl, "West");        tf.addActionListener(new ActionListener() {          public void actionPerformed(ActionEvent evt) {             try {                tf.normalize();                Long 1 = tf.getLongValue();                System.out.println("Value is (Long)" + 1);             } catch (ParseException el) {                try {                   Double d = tf.getDoubleValue();                   System.out.println("Value is                                      (Double)" + d);               } catch (ParseException e2) {                  System.out.println(e2);               }             }          }       });       f.pack();       f.setVisible(true);    } } 

A NumericTextField wraps a NumericPlainDocument. Its constructors allow you to specify various different parameters to initialize the model, including the formatter and the initial content in the form of a String. It also allows you to change the format after the text field has been created (using setFormat), or to have the model update its view of its format if some aspect of it has changed (the formatchanged method). To create a numeric text field in which grouping is used but fractions are not, you might do the following:

 DecimalFormat f = new DecimalFormat( ); f.setGroupingUsed(true);     // Use "," f.setGroupingSize(3);        // 3 digits between "," f.setParseIntegerOnly(true); // Only integers: no decimal point NumericTextField tf = new NumericTextField(10, f); 

If, later, you want to set a nondefault negative suffix or prefix, you might do this:

 f.setNegativePrefix("("); f.setNegativeSuffix(")"); t.formatchanged(); 

It is unlikely that you would want to do this, but the facility exists nevertheless.

The NumericTextField also has pass-through methods that allow you to get the numeric value out of the model by calling the model's getNumber-Value, getDoubleValue, and getLongValue methods. The setValue methods allow you to change the value in the model by supplying a new value as a Number (which could be a Long or a Double) or a primitive long or double. You can also change the value by invoking setText, which is inherited from JTextField and accepts a String value, which would, of course, be parsed and checked by the model's insertstring method.

The last public method of interest is the normalize method. This causes the fields content to be read and formatted by the model's formatter, and then written back to the model. This method causes the text field's content to be displayed in the canonical format specified when it was created. For example, suppose the formatter was created with the formatting string #,###.##. The parser does not insist that the decimal separators are present as the number is typed and, if they are typed, it does not require them to be in the correct places. As a result, the user could type something untidy like 1,2,3,4,5,67 or the shorthand 123456. Calling normalize would cause either of these to be changed to 1,234,567 and written back to the text field. This would normally be done after the user presses return, to tidy up the field's display content.

The NumericTextField implementation includes a main method that displays a NumericTextField and lets you type into it. When you press RETURN, its ActionListener calls normalize to put the text field content into its canonical form and then calls getLongValue to extract the number typed as a Long. If this fails, it is assumed that the number has a fractional part and getDoubleValue is called. If both of these fail, the number must be incorrectly formatted and an error message is printed.

To try this example, use the command:

 java AdvancedSwing.Chapter1.NumericTextField 

The field is initialized with the value 123456.789 supplied as a double. Because the implementation of setValue uses the formatter to produce the corresponding String for insertion into the model, the representation you see in the text field (see Figure 1-12) includes the group separator.

Figure 1-12. Using the NumericTextField class.
graphics/01fig12.gif

You can verify that the text field works as it should by trying to type illegal characters, such as letters, or attempting to supply more than one decimal point. Illegal values should not appear in the text field and cause a beep to alert you to the fact that your keystroke has been ignored. If you press RETURN, the value that you typed into the input field will be printed. Here are some examples:

 Value is (Double)-123456.789 Value is (Double)-4.7 Value is (Long)-4 

Notice that a Double is returned when the number has a fractional part and a Long when it does not.

A Text Field with Look-Ahead

Now that you've seen two custom text fields that rely for their functionality on specialized data models, let's look at another way to provide additional functionality. What we want to do this time is to build a text field that has the ability to "look ahead" and guess what the user is trying to type as each key is pressed. When it has a good guess to offer, it would display the guess in the text field, allowing the user to accept it immediately. If the guess is wrong, the user would be allowed to continue typing, providing more letters for the text field to use as the basis for another guess.

This type of input field is useful in cases in which the user is typing one value from a (possibly long) set of legal possibilities, such as the name of somebody in the same organization. If the names of all a company's employees are held centrally, it might be reasonable to have the input field match the user's keystrokes to names in the employee database and bring up the name of the best match available. With a little more sophistication, you could add a fuzzy matching algorithm that allowed users to match names that sound like the one they have typed part of. Of course, we're not going to cover all of that in this section. The text field that will be implemented here will be open-ended it will delegate the job of turning the user's keystrokes into a suitable guess to another class that can be plugged in as appropriate. To demonstrate, we'll also provide a plug-in class that offers a guess from an array of possible choices.

To begin with, let's look at how the look-ahead text field works in practice. To try it out, type the following command:

 java AdvancedSwing.Chapter1.LookAheadExample 

This creates a frame with an input field, pre-configured with the following short list of words:

aback abacus abandon abashed
abate abdomen abide ability
baby back backache backgammon

This set of words is chosen to demonstrate how the look-ahead process works. Click in the input field and press "a." Immediately, the text field looks at the list of possible words, matches the first entry, "aback," and displays it see Figure 1-13.

Figure 1-13. A text field with look-ahead.
graphics/01fig13.gif

Notice that the cursor appears after the letter a and that the part of the word that was added by the look-ahead process, back, is selected. The reason for selecting the added text is to allow the user to easily delete it if it is wrong:

Pressing the backspace key now would clear the extra text and leave the cursor where it is. On the other hand, if you continue to type, the text field continues to make guesses as to what it is you are trying to type. The next two letters, b and a, match all the possibilities in the configured word list. If you now type n, the content of the text field changes to abandon, with the letters d, o, and n selected. Now if you type any letter other than d, the text field runs out of guesses and just lets you continue typing without intervening any more.

You'll notice also that no guessing takes place when you use the backspace or delete key, but once you start typing other keys, you get new guesses. For example, if you delete everything but the first two characters, you'll be left with ab. Now press d and you'll be offered abdomen. Delete the d (which actually takes two keystrokes) and type i and the content changes to abide.

Now that you've seen the control working, let's examine the implementation. The first design decision to make is where the look-ahead functionality belongs. Up to now, the clever code has been in the text component's model and it is perfectly possible to add code to the model that would provide most of the features that you've just seen. However, there is one small problem how to arrange for the text added by the look-ahead to be selected for easy deletion? As you know, the model deals only with holding the text field's content it cannot select text. Selection is managed by the text component itself, in response to operations on the caret. There is no mechanism in the text components for the model to directly dictate where the caret is and therefore what is selected. The only logical place to add this feature, therefore, is in the text component. The example shown in Figure 1-13 uses a subclass of JTextField called LookAheadTextField, the implementation of which is shown in Listing 1-9.

Listing 1-9 A Text Field with Look-Ahead
 package AdvancedSwing.Chapter1; import javax.swing.*; import javax.swing.text.*; import java.awt.event.*; public class LookAheadTextField extends JTextField {    public LookAheadTextField() {       this(0, null);    }    public LookAheadTextField(int columns) {       this(columns, null);    }    public LookAheadTextField(int columns,                   TextLookAhead lookAhead){       super(columns);       setLookAhead(lookAhead);       addActionListener(new ActionListener() {          public void actionPerformed(ActionEvent evt) {             // Remove any existing selection             setCaretPosition(getDocument().getLength());          }       });       addFocusListener(new FocusListener() {          public void focusGained(FocusEvent evt) {          }          public void focusLost(FocusEvent evt) {             if (evt.isTemporary() == false) {                // Remove any existing selection                setCaretPosition(getDocument().getLength());             }          }       });    }    public void setLookAhead(TextLookAhead lookAhead) {       this.lookAhead = lookAhead;    }    public TextLookAhead getLookAhead() {       return lookAhead;    }    public void replaceSelection(String content) {       super.replaceSelection(content);       if (isEditable() == false || isEnabled() == false) {          return;       }       Document doc = getDocument();       if (doc != null && lookAhead != null) {          try {             String oldContent =                   doc.getText(0, doc.getLength());             String newContent =                   lookAhead.doLookAhead(oldContent);             if (newContent != null) {                // Substitute the new content                setText(newContent);                // Highlight the added text                setCaretPosition(newContent.length());                moveCaretPosition(oldContent.length());             }          } catch (BadLocationException e) {             // Won't happen          }       }    }    protected TextLookAhead lookAhead;    // The TextLookAhead interface    public interface TextLookAhead {       public String doLookAhead(String key);    } } 

A LookAheadTextField is associated with an implementation of the interface LookAheadTextField.TextLookAhead, which provides the code that knows how to map from the text typed by the user to a suitable guess as to what the user intends. This interface has only one method:

 public String doLookAhead(String key); 

The key argument supplies the text that the user has typed so far. Using this information and any internal state it retains, the doLookAhead method chooses a candidate word and returns it. The returned string is used to fill the text field, unless null is returned, when it is assumed that the content of the text field does not correspond to any word that the look-ahead mechanism recognizes. In this case, whatever the user has typed is left in the input field, unmodified. An object that implements the TextLookAhead interface can be provided to the constructor, or set later using the setLookAhead method.

All of the useful work is actually carried out in the replaceSelection method. The replaceSelection implementation that all text components inherit from JTextComponent first removes anything in the component that is selected, and then adds whatever is in the String argument that it is given to the document at the position that was occupied by the start of the selection. If nothing was selected initially, the text is added at the initial position of the caret. In either case, the caret is moved to the end of the inserted text.

To catch all the user input as it is typed, the replaceSelection method is overridden. The user's input is first merged into the existing text by using the superclass implementation. At this point, the text component contains what it would have contained had the look-ahead not been implemented. This text is then extracted from the model using the Document getText method, and then passed to the doLookAhead method of the configured TextLookAhead object. If null is returned, there is no guess to replace the user's typing with, so nothing more is done.

If a string is returned, however, it is used to replace the text component's content by passing it to the setText method, which writes whatever it is given directly to the model, without invoking the replaceSelection method again. The final step is to highlight the part of the returned String that was added to the key that was passed in. To achieve this, the extra characters are selected by first placing the caret at the end of the new content, and then moving it forward so that it is after the last character that the user typed. The act of placing the caret using setCaretPosition and then moving it with moveCaretPosition creates a selection covering the text between those two locations. Placing the caret at the end and then moving it backward both creates the selection and leaves the caret in the correct location for the user to continue typing should the result of the look-ahead be incorrect.

There is one final piece of the text field that needs to be implemented. If the user types several characters and the look-ahead returns whatever the user was trying to type, the user will probably press RETURN or move the keyboard focus elsewhere. At this point, the text field still has the part of the text added by the look-ahead mechanism selected, which looks untidy. To get around this, an ActionListener is added that will be activated when the RETURN key is pressed and a FocusListener is added to handle loss of focus. In both cases, the caret will be moved to the end of the text field, clearing the selection. Note that the FocusListener clears the selection only if the loss of focus is permanent. A permanent loss of focus occurs when the focus is moved to another component in the same window. If the user moves the focus to another window, a temporary focus change occurs and the focus will return to the text field when the user brings the window that it is part of back to the foreground. In this case, the selection should not be cleared and the caret should remain where it is.

The final piece of the puzzle is the TextLookAhead object. You can implement any algorithm that you need for guessing what the user is trying to type. The look-ahead algorithm used in the example you have just seen is a very basic one it just looks through a set of Strings for one that starts with whatever is already in the text field. Nevertheless, this provides a template for writing other, more complex, variations that might require, for example, a database search. You can see the implementation, in a class called string-ArrayLookAhead, in Listing 1-10.

Listing 1-10 A Simple Look-Ahead Implementation
 package AdvancedSwing.Chapter1; import javax.swing.*; import javax.swing.text.*; public class StringArrayLookAhead implements                LookAheadTextField.TextLookAhead {    public StringArrayLookAhead() {       values = new String[0];    }    public StringArrayLookAhead(String[] values) {       this.values = values;    }    public void setValues(String[] values) {       this.values = values;    }    public String[] getValues() {       return values;    }    public String doLookAhead(String key) {       int length = values.length;       // Look for a string that starts with the key       for (int i = 0; i < length; i++) {          if (values[i].startsWith(key) == true) {             return values[i];          }       }       //No match found - return null       return null;    }    protected String[] values; } 

This class is configured with an array of strings, passed to the constructor or via the setvalues method. The work is done by doLookAhead, which searches through the array for a String that starts with the characters of the string passed to it. Obviously, for a production implementation, you would probably want to make this more efficient by using a faster searching algorithm. The LookAheadTextField used in the example used in this section is actually initialized as follows:

 StringArrayLookAhead lookAhead =             new StringArrayLookAhead(values); LookAheadTextField tf = new LookAheadTextField(             20, lookAhead); // Code omitted // The possible look-ahead values public static String [] values = new String[] {    "aback", "abacus", "abandon", "abashed", "abate",    "abdomen", "abide", "ability",    "baby", "back", "backache", "backgammon" }; 

Text Actions and Keyboard Mappings

So far, you've seen how the text component's model works and how to replace it in order to impose some constraint on what the user can type into the component. The next piece of the text component architecture that we'll look at is the MVC controller part, specifically the piece of the controller that handles keyboard input; as noted earlier, the controller also separately deals with mouse and focus events, which are directed to the caret.

As far as the controller is concerned, there are actually two types of keystroke: those that result in the key typed being stored in the model and those that result in some action within the component. Keystrokes that fall into the latter category are usually a single character used in conjunction with the CTRL or ALT key and so can be considered keyboard shortcuts. The keyboard shortcuts that are associated with a text component depend on the text component itself, the editor kit that the text component is using, and on the look-and-feel being used. The operations that a text component can perform are referred to as actions; these actions are made accessible to the user through keyboard mappings. For example, all the text components support copy, cut, and paste actions, but the key combinations that produce these actions vary depending on which look-and-feel is installed. Table 1-4 shows the default mappings for these actions.

Table 1-4. Keyboard Mappings for the Copy, Cut, and Paste Actions
Metal Windows Motif
Copy CTRL+C CTRL+C CTRL + INSERT
Cut CTRL+X CTRL+X SHIFT + DELETE
Paste CTRL+V CTRL+V SHIFT + INSERT
Text Actions

The complete set of actions for a text component is the union of the actions supported by the editor kit and by the text component itself. Usually, the editor kit supplies the bulk of the available actions and the text component adds a small number of actions specific to itself. Because all the standard text components use the DefaultEditorKit or one derived from it, they all have a common set of editing operations that are provided by actions implemented by the DefaultEditorKit class.

Every action, whether it is provided by an editor kit or by a text component, is implemented as a class derived from the abstract base class Text-Action, which is derived from the AbstractAction class. Text actions, therefore, all implement the Swing Action interface. As a result, they can be called upon to do whatever they are supposed to do (for example, paste some text from the clipboard) by invoking their actionPerformed methods. Later in this chapter, you'll see how a simple text action is implemented.

The TextAction class has only two public and two protected methods of its own:

 public abstract class TextAction extends AbstractAction {    public TextAction(String name);    public static final Action[] augmentList(Action[]                list1, Action[] list2);    protected final JTextComponent                getTextComponent(ActionEvent e);    protected final JTextComponent getFocusedComponent(); } 

The constructor associates a human-readable name with the text action. This name can be used as the text that would appear if this action were posted on a menu or a toolbar. The getTextComponent and getFocusedComponent methods are both used within the implementation of a text action to determine which text component the action should be performed on. You'll see how this works in "Implementing Overwrite Mode in a Text Field".

Editor kits and text components both have a getActions method that returns an array of Action objects representing the complete set of actions that they implement. The text component getActions method is usually implemented as a call to the static TextAction method augmentList, merging the set of actions that the specific text component provides with those provided by JTextComponent, which simply pretends to support the set of actions provided by its installed editor kit. The set of actions implemented by DefaultEditorKit, and therefore available from all text components, is shown in Table 1-5.

The name in the first column of Table 1-5 is a string value by which the action is known. The editor kit or text component will usually declare a symbolic name that maps to each specific action. These names are part of the public API of the component. The paste action, for example, can be referred to using the constant value DefaultEditorKit.pasteAction. The actual object that implements this operation can be found by invoking the get-Actions method of a text component and searching the returned array of Action objects for one whose name is DefaultEditorKit.pasteAction. Usually, however, you won't need to do this, because text actions are usually used directly only when mapping them to keystrokes, a process that requires the action's symbolic name rather than the object reference itself.

Table 1-5. Text Actions Implemented by DefaultEditorKit
Name Action
caret-backward Moves the caret backward one character.
beep Causes a beep.
caret-begin Moves the caret to the start of the document.
caret-begin-line Moves the caret to the start of the current line.
caret-begin-paragraph Moves the caret to the start of the current word.
caret-begin-word Moves the caret to the start of the current paragraph.
caret-down Moves the caret down a line.
caret-end Moves the caret to the end of the document.
caret-end-line Moves the caret to the end of the current line.
caret-end-paragraph Moves the caret to the end of the current paragraph.
caret-end-word Moves the caret to the end of the current word.
caret-forward Moves the caret forward one character.
caret-next-word Moves the caret to the start of the next word.
caret-previous-word Moves the caret to the start of the previous word.
caret-up Moves the caret up by one line.
copy-to-clipboard Copies the currently selected text onto the system clipboard.
cut-to-clipboard Deletes the text that is currently selected and places it on the system clipboard.
default-typed Inserts the last key typed in the document model.
delete-next Deletes the character that immediately follows the caret.
delete-previous Deletes the character immediately before the caret.
insert-break Inserts a newline into the document.
insert-content Places the keystroke that caused this action into the document.
insert-tab Inserts a tab in the document.
page-down Moves the caret down one page.
page-up Moves the caret up one page.
paste-from-clipboard Pastes the content of the system clipboard into the document immediately before the current caret position, deleting anything currently selected.
select-all Selects the entire document.
select-line Makes the current line into the selection.
select -paragraph Makes the current paragraph into the selection.
select-word Makes the current word into the selection.
selection-backward Moves the caret backward one position, extending the current selection.
selection-begin Extends the selection to the start of the document.
selection-begin-line Extends the selection to the start of the current line.
selection-begin-paragraph Extends the selection to the start of the current paragraph.
selection-begin-word Extends the selection to the start of the current word.
selection-down Moves the caret down one line and moves the selection with it.
selection-end Extends the selection to the end of the document.
selection-end-line Extends the selection to the end of the current line.
selection-end-paragraph Extends the selection to the end of the current paragraph.
selection-end-word Extends the selection to the end of the current word.
selection-forward Moves the caret forward one position, extending the current selection.
selection-next-word Extends the selection to the start of the next word.
selection-previous-word Extends the selection to the start of the previous word.
selection-up Moves the caret up one line and moves the selection with it.
set-read-only Makes the editor read-only.
set-writable Switches the editor into read-write mode.

The more complex styledEditorKit inherits all the actions provided by DefaultEditorKit and supplies an extra set of operations that are appropriate for text components that handle text with more complex attributes, such as JTextPane and JEditorPane. These extra operations are listed in Table 1-6.

The individual text components may define their own specific actions that are additional to the ones supplied by their editor kits. In fact, of the standard Swing components, only JTextField uses this facility, to add an action called notify-field-accept, which posts an ActionEvent to all registered ActionListeners. This action, which is inherited by JpasswordField, is used when the user presses RETURN, to notify listeners that the user has finished typing into the field. In the next section, you'll see exactly how this action is activated by the RETURN key and later you'll find out how to disable it if necessary.

Table 1-6. Text Actions Supplied by StyledEditorKit
Name Action
font-family-SansSerif Select a Sans Serif font.
font-family-Monospaced Select a monospaced font.
font-family-Serif Select a Serif font.
font-size-8 Set font size to 8 points.
font-size-10 Set font size to 10 points.
font-size-12 Set font size to 12 points.
font-size-14 Set font size to 14 points.
font-size-16 Set font size to 16 points.
font-size-18 Set font size to 18 points.
font-size-24 Set font size to 24 points.
font-size-36 Set font size to 36 points.
font-size-48 Set font size to 48 points.
font-bold Toggle bold attribute on and off.
font-italic Toggle the italic attribute on and off.
font -underline Toggle the underline attribute on and off.
left-justify Left -justify paragraph(s).
center-justify Center paragraph(s).
right-justify Right-justify paragraph(s).
Keymaps and Key Bindings

The actions that a text component defines for itself or inherits from its editor kit are accessible directly to the programmer, but they can't be used by the user unless mappings, examples of which were shown in Table 1-4, are created between individual actions and keystrokes. The establishment of these particular mappings allows the user to access the cut, copy, and paste features of a text component by simply pressing CTRL+X, CTRL+C, or CTRL+V (or the corresponding keystrokes in the selected look-and-feel) when the text component has the focus. In this section, you'll see the data structures that hold the keyboard mapping information and how the mappings themselves are created.

The KeyBinding and KeyMap Classes

Two classes are used to create and maintain key-to-action mappings KeyBinding and KeyMap. The first of these is more properly called JText-Component .KeyBinding because it is a static inner class of JTextComponent. Its function is simply to map the specification of a key, or combination of keys, to the name of a text component action. Here is its definition:

 public static class KeyBinding {    public Keystroke key;    public String actionName;    public KeyBinding(Keystroke key, String actionName) { } 

The Keystroke object specifies the keys that the user will use to activate the corresponding action. The Keystroke class has a static method called getKeyStroke that returns an appropriate object given a keycode name and the modifier keys that must be pressed along with the key. The actionName argument is the name of the action as specified in the public API of the text component or its associated editor kit. Here, for example, is how you would create a KeyBinding object that would request the mapping of the default editor kit's paste action to the key sequence CTRL+V:

 KeyBinding binding = new KeyBinding(          Keystroke.getKeyStroke(KeyEvent.VK_V,                   InputEvent.CTRL_MASK),          DefaultEditorKit.pasteAction); 

The KeyBinding object does not actually create a keyboard mapping it is used in the process of building the complete set of mappings for a component, which is held in a Keymap object. Keymap is an interface, defined as follows:

 public interface Keymap {    public String getName();    public Action getAction(Keystroke key);    public Keystroke[] getBoundKeyStrokes();    public Action[] getBoundActions();    public Keystroke[] getKeyStrokesForAction(Action a);    public boolean isLocallyDefined(Keystroke key);    public void addActionForKeyStroke(Keystroke key, Action a);    public void removeKeyStrokeBinding(Keystroke keys);    public void removeBindings();    public Action getDefaultAction();    public void setDefaultAction(Action a);    public Keymap getResolveParent();    public void setResolveParent(Keymap parent); } 

A keymap essentially contains mappings of Keystroke objects to Action objects, while the Action object is usually a TextAction (although, in fact, it need not be). As the user presses keys, each single key press and its associated modifiers (the states of the SHIFT, CTRL, and ALT keys) is used to build a Keystroke object that is then used to search the text components keymap. If a corresponding Keystroke is found, the associated Action is performed. If no mapping is found, the keymap's default action, if there is one, is used instead. You'll see what the default action does later in this section.

Keymaps are actually hierarchical in nature; any keymap can be created with another keymap as its parent, or a parent can be associated with it later using the setResolveParent method. The set of all keymaps in an application is maintained by JTextComponent. When the JTextComponent class is first loaded, it creates the first Keymap object, which contains the default action and a set of mappings of keys to the standard actions provided by the DefaultEditorKit, as shown in Table 1-7. This Keymap is known as the default Keymap.

Table 1-7. Keyboard Mappings in the Default Keymap
Keys Action
VK_BACK_SPACE DefaultEditorKit.deletePrevCharAction
VK_DELETE DefaultEditorKit.deleteNextCharAction
VK_RIGHT DefaultEditorKit.forwardAction
VK_LEFT DefaultEditorKit.backwardAction
Creating Individual Keymaps

To create a new keymap, the JTextComponent addKeymap method is used:

 public static Keymap addKeymap(String name, Keymap parent); 

A new keymap initially has no key bindings of its own, but it does inherit those of its parent. Many keymaps, including the ones created for the standard Swing text components, have the default keymap as their direct parent. As a result, the key mappings shown in Table 1-7 are available in all of these keymaps (and hence in all Swing components). A keymap can have a name, which must be unique within an application. Given the name, you can find the corresponding keymap using the following static JTextComponent method:

 public static Keymap getKeymap(String name); 

The name of the default keymap is available in the static field JTextComponent.default_keymap, so you can get a reference to the default keymap and use it to create a new keymap called MYKEYMAP as follows:

 Keymap defaultKeymap =    JTextComponent.getKeymap(JTextComponent.DEFAULT_KEYMAP); Keymap myKeymap =    JTextComponent.addKeymap("MYKEYMAP", defaultKeymap); 

This new keymap contains only the mappings shown in Table 1-7.

The name given to a keymap is visible to the entire application and can be given to the getKeymap method to get a reference to it. If you want to create a private and anonymous keymap, you can do so by supplying a null name:

 Keymap privateKeymap =    JTextComponent.addKeymap(null, defaultKeymap); 

Such a keymap is accessible only through the reference stored in the variable privateKeymap.

Creating Keymaps for Text Components

As you've seen, a Keymap contains mappings from Keystrokes to Actions, while KeyBindings map Keystrokes to action names. When keys are pressed, the text component uses its Keymap to determine what action to take, not the KeyBindings. To build a Keymap from a set of KeyBindings, you need a way to obtain the Action corresponding to a given action name, which is specified in the KeyBinding, such as DefaultEditorKit.paste-Action. However, there is no simple way to do this: The Actions are usually created internally by editor kits and components. Only the names are publicly available, which is why KeyBinding objects are used to specify the required key mappings for a component. The only way to map a name to an Action, in order to create an entry in a Keymap, is to call the JTextComponent or EditorKit getActions method, which returns an array containing all the Actions that it supports. Each Action has an associated name, which can be obtained using its getvalue method. Here, for example, is a code extract that prints the names of all the Actions supported by JTextField:

 public JTextField tf = new JTextField(); Action[] actions = tf.getActions(); for (int i = 0; i < actions.length; i++) {    System.out.print1n(actions[i].getvalue(Action.NAME)); } 

Given a set of KeyBinding objects, it is possible to construct a Keymap by searching the list of Actions for one with the name of each action in the Key-Binding list, and then using the addActionForKeyStroke method to add an entry to the Keymap:

 public void addActionForKeyStroke(Keystroke key, Action a); 

This is something of a long-winded process, especially if you want to install a set of mappings in a keymap, which is usually the case. For example, all the Swing text components have mappings for the full set of actions supported by DefaultEditorKit (see Table 1-5). To make it easier to build the key mappings for a text component, JTextComponent has a convenience method called loadKeymap, defined as follows:

 public static void loadKeymap(Keymap map,       KeyBinding[] bindings, Action[] actions); 

This method adds to the keymap supplied, as its first argument, a mapping corresponding to each binding in the second argument. The third argument is the list of Actions that are searched for whose names match the ones specified in the array of KeyBinding objects. If no suitable Action is found in the actions array for any of the bindings, no corresponding mapping is added; this is not considered to be an error. Because key bindings are look-and-feel specific, as you can see from Table 1-4, the keymap for each standard text component is installed by its UI class when the component is created. If the look-and-feel is switched at any time, a new keymap is created to reflect the bindings appropriate for the new look-and-feel.

Keeping a dedicated keymap for each text component would be expensive, because, unless the programmer takes special steps to customize individual components, the keymaps for all instances of JTextField, for example, are the same. Because of this, each text component type shares a common key-map; only one instance of this keymap is created and it is installed in every instance of that component. It follows that there are separate keymaps created for each of JTextField, JPasswordField, JTextArea, JTextPane, and JEditorPane, making a total of five keymaps no matter how many of these components exist within the application.

Core Note

Actually, each look-and-feel creates its own keymap for a particular component type, so there will actually be five keymaps for each look-and-feel that the application has used. In the rest of this chapter, for the sake of brevity, we'll assume that only one look-and-feel is in use, so there will be one keymap per component type.



You can get and set the keymap for a specific component instance using the following JTextComponent methods:

 public void setKeymap(Keymap map); public Keymap getKeymap(); 

Each of the per-component type keymaps is, of course, stored by JTextComponent and can be retrieved without having an instance of that component by invoking the static getKeymap method and passing it the name given to addKeymap when it was created. The names used for the keymaps created for the standard text components are actually the names of the UI classes associated with those components. That is, the keymap for JTextField when the Metal look-and-feel is in use is stored with the name "MetalTextFieldUI," the one for JTextArea under the name "BasicTextAreaUI" (because the Metal look-and-feel uses the common UI class for JTextArea, whereas it extends BasicTextFieldUI to create MetalTextFieldUI for JTextField) and so on. Thus, you can get a reference to the keymap shared by all the JTextFields in the Metal look-and-feel like this:

 Keymap map = JTextComponent.getKeymap("MetalTextFieldUI"); 

or like this, assuming that the Metal look-and-feel is selected:

 JTextField tf = new JTextField(); Keymap map = tf.getKeymap(); 

Whichever method you choose, you get exactly the same reference returned, because there is only one keymap shared by all JTextField objects for a given look-and-feel. The second method is more convenient because you don't need to know which look-and-feel is selected to retrieve the keymap, or to understand the algorithm used to create the name.

Resolving Key Bindings

As noted earlier, when the user types something into a text component, the key code and modifiers from the KeyEvent are used to create a Keystroke object, which is then used to search the component's keymap for a corresponding Action. If no mapping is found for the Keystroke, the search continues with the keymap's resolving parent, set when the keymap was created by the addKeymap method (or later using setResolveParent). This process continues until a mapping is found or until there are no more parents to search.

As an example, consider what happens when the backspace key is pressed in a JTextArea. The keymap for this component actually consists of a Key-map created by the JTextArea UI class to map a subset of the actions of DefaultEditorKit and a reference to the default keymap as its resolving parent, which contains the mappings shown in Table 1-7. A Keystroke object for the VK_BACK_SPACE key (with no modifiers) is built and searched for in the JTextArea's keymap. Because this doesn't contain an entry for this Keystroke, the search continues in the default keymap, where a mapping to the DefaultEditorKit deletePrevCharAction will be found and used to implement the effect of the backspace key. The standard text components all have a two-level keymap like this, by default. As you'll see shortly, you can add extra levels to a component's keymap to change the way it reacts to selected keys.

The Default Keyboard Action

If the process of resolving a Keystroke does not produce a mapping, the Keymap's default action is used. The default action can be obtained by calling the Keymap's getDefaultAction method. Every Keymap can have a default action, but none is required to have one. In fact, a Keymap has no default action unless you assign one to it using the setDefaultAction method, or it inherits one from its resolving parent. The default Keymap has a default action that inserts the key that the user typed into the text component at the current location of the caret. This is, in fact, how data gets into a text component. Because the Keymaps used by the standard text components all have the default Keymap as their resolving parents, you can change the way in which most keys are handled for every text component by installing a different default action in this Keymap. Alternatively, you can modify the handling of input characters for a specific component type (for example, for all JText-Areas) by changing the default action of the Keymap for that component type, because the newly installed default action will override the one in the default Keymap.

Disabling the RETURN Key in a Text Field

The Keymap for JTextField (and JPasswordField) has an entry that maps the RETURN key to an action (called notify-field-accept) that generates an ActionEvent to inform listeners that the content of the input field is valid. Sometimes, however, you won't need to use this event and, in many cases, the fact that this mapping exists can cause problems. Lets look at a simple example that demonstrates the problem.

Suppose you have a frame that contains a form-like layout consisting of several text fields, in which the user is expected to fill in all the fields on the form and press a button to have the form processed. Typically, you would have an ok button and a Cancel button at the bottom of the form and the first input field would have the input focus when the frame appears. An experienced user would want to drive this form by using the tab key to move between the fields and then pressing RETURN when the form is filled, as an alternative to clicking the OK button by moving the mouse over it or using the TAB key to move to the button and activating it with the space bar. The difference between these two approaches can be very annoying for the user if some of the fields have default values that are acceptable, because in the latter case, to submit the completed form the user might have to press the TAB key several times to reach the OK button.

In this situation, you would want to make the OK button the default button for its containing frame. The default button can be activated by pressing the RETURN key, without needing to give it the keyboard focus. This looks like the right thing to do, because the OK button would then be activated no matter how many fields the user changes before the form content is correct and no matter where the input focus happens to be. Unfortunately, the default button does not work when there are JTextFields in the frame and one of them has the focus. The reason for this is that the RETURN key is seen first by the focused JTextField, which has an action registered for it in its keymap that would just generate an ActionEvent from the text field. In this situation, of course, you are unlikely to be using the ActionEvent, because the user will be moving between fields using the TAB key, so this event is not going to be delivered to any listeners. If you are going to take the trouble to process fields as the user completes them, you will probably use a FocusListener, which will be activated as the user moves out of each field and into the next one.

Removing the RETURN key from the Keymap

What you need to do in this case is to remove the text field's mapping for the RETURN key. If you can do this, the RETURN key won't be swallowed by the text field and will get passed instead to the default button, which will be activated to signal the completion of the form. Listing 1-11 shows the most straightforward implementation of this idea.

Listing 1-11 Changing the Text Field Keymap
 package AdvancedSwing.Chapter1; import javax.swing.*; import javax.swing.text.*; import java.awt.event.*; public class PassiveTextField1 extends JTextField {    public static void main(String[] args) {       JFrame f = new JFrame("Passive Text Field");       f.getContentPane().setLayout(                   new BoxLayout(f.getContentPane(),                   BoxLayout.Y_AXIS));       final JTextField ptf = new JTextField(32);       JTextField tf = new JTextField(32);       JPanel p = new JPanel ();       JButton b = new JButton("OK");       p.add(b);       f.getContentPane().add(ptf);       f.getContentPane().add(tf);       f.getContentPane().add(p);       Keymap map = ptf.getKeymap(); // Gets the shared map       Keystroke key =             Keystroke.getKeyStroke(KeyEvent.VK_ENTER, 0);       map.removeKeyStrokeBinding(key);       ActionListener l = new ActionListener() {          public void actionPerformed(ActionEvent evt) {             System.out.print1n("Action event                                from a text field");          }       };       ptf.addActionListener(l);       tf.addActionListener(1);       // Make the button the default button       f.getRootPane().setDefaultButton(b);       b.addActionListener(new ActionListener() {          public void actionPerformed(ActionEvent evt) {             System.out.print1n("Content of text field: <"                   + ptf.getText(} +">");          }       });       f.pack();       f.setVisible(true);    } } 

This example creates two text fields and a button, which is made the default button for the frame. The difference between the two text fields is that an extra operation is performed on the top one before it is displayed. Here is the code that performs that operation:

 Keymap map = ptf.getKeymap();   // Gets the shared map Keystroke key = Keystroke.getKeyStroke(KeyEvent.VK_ENTER, 0); map.removeKeyStrokeBinding(key) ; 

This code gets the text field's keymap, builds a Keystroke object that corresponds to the RETURN key being pressed, and then removes whatever mapping is attached to the RETURN key using the Keymap removeKeyStrokeBinding method. This should result in the text field at the top of the form having no mapping for the return key. If you run this example using the command

 java AdvancedSwing.Chapter1.PassiveTextFieldl 

you'll get the layout shown in Figure 1-14.

Figure 1-14. Disabling the RETURN key in a text field.
graphics/01fig14.gif

First, type something in the upper text field and press RETURN. When you do so, the ok button will change its appearance to show that it has been activated, and you'll see the content of the text field printed in the window from which you started the example program. This message was, as you can see from the code, printed from an event handler connected to the button, not to the text field. As you can also see from Listing 1-11, there is also an Action-Listener associated with the text field. The fact that the message that it would print if were activated does not appear demonstrates that the text field is not generating an event when the RETURN key is pressed and this is confirmed by the fact that the RETURN key is successfully activating the default button.

So far, all seems well. However, there is a side effect. The other text field has not had its keymap changed, or so you might think. Certainly, there has been no explicit modification of its keymap. However, if you type some text into it and press RETURN, you'll find that it behaves in the same way as the other text field the button fires and the text field's ActionListener is not activated. What has happened here is that the keymap retrieved by the code in the main method is a shared keymap used by all text fields. Changing that keymap makes the same change for every JTextField using that keymap in other words, for all JTextFields that do not have a private keymap that your application explicitly installed and that does not depend on the shared JTextField keymap. If your application never needs to use the JTextField's ActionEvent, this is probably a side effect you can live with. However, it is not satisfactory as a general solution.

Creating a Replacement Keymap

Because the simple solution isn't completely satisfactory, perhaps it would be better to create an entirely new keymap for our text field. The new keymap would need to have all the same mappings as the standard JTextField, except for the case of the RETURN key. Creating such a keymap is not very difficult. Unfortunately, you can't clone keymaps because the Keymap interface does not include a clone operation, but you can arrange to copy a keymap, keystroke by keystroke, and in doing so, you can omit the mapping for the RETURN key.

When is the appropriate time to create the new keymap? Because the key-map will be derived from the one used by JTextField, you can't create it until the JTextField keymap has been initialized, which happens during construction of the JTextComponent part of the JTextField object. If we create a derived class of JTextField, then after the JTextField constructor has been executed, its keymap will be valid and we could extract it (using getKeymap), create a slightly modified version, and then install the new one using setKeymap. Creating a new component is more convenient than adding a few lines of code to change the keymap, as was done in Listing 1-11, because it encapsulates the operation as part of a new type of object that the programmer can use without needing to know how it works.

However, this would only be a partial solution. The new component has a keymap that won't react to the RETURN key. However, what happens if the application allows the user to switch from one look-and-feel to another? Because the key mappings are look-and-feel dependent, they will be switched as part of the changeover from one look-and-feel to another. When this happens, the new keymap that was installed in the constructor will be replaced by a keymap for JTextField, with the mapping for RETURN installed. To work around this problem, you need to catch this change of key-map and install a modified keymap instead. Fortunately, this case is easy to detect because a switch of look-and-feel first sets the component's keymap to null (as the old look-and-feel is switched out) and then to a non-null value as the new keymap is installed. These operations are performed by calling the setKeymap method, which is also called to install the initial keymap during construction. If you add the code to modify the keymap to setKeymap and only make modifications when the installed keymap is null, you won't need to add code to the constructor and the keymap changes will persist over a switch of look-and-feel. Listing 1-12 shows the implementation details.

Listing 1-12 Creating a New Keymap for a Text Component
 package AdvancedSwing.Chapter1; import javax.swing.*; import javax.swing.text.*; import java.awt.event.*; public class PassiveTextField2 extends JTextField {    public PassiveTextField2() {       this(null, null, 0);    }    public PassiveTextField2(String text) {       this(null, text, 0);    }    public PassiveTextField2(int columns) {       this(null, null, columns);    }    public PassiveTextField2(String text, int columns) {       this(null, text, columns);    }    public PassiveTextField2(Document doc,                             String text, int columns) {       super(doc, text, columns);    }    public void setKeymap(Keymap map) {       if (map == null) {          // Uninstalling keymap.          super.setKeymap(null);          sharedKeymap = null;          return;       }       if (getKeymap() == null) {          if (sharedKeymap == null) {             // Initial keymap, or first             // keymap after L&F switch.             // Generate a new keymap             sharedKeymap = addKeymap(null,                               map.getResolveParent());             Keystroke[] strokes = map.getBoundKeyStrokes();             for (int i = 0; i < strokes.length; i++) {                Action a = map.getAction(strokes[i]);                if (a.getValue(Action.NAME) ==                            JTextField.notifyAction) {                   continue;                }                sharedKeymap.addActionForKeyStroke(                            strokes[i], a) ;             }          }          map = sharedKeymap;       }       super.setKeymap(map);    }    protected static Keymap sharedKeymap;    // Test method    public static void main(String[] args) {       JFrame f = new JFrame("Passive Text Field");       f.getContentPane().setLayout(new BoxLayout(                   f.getContentPane(),                   BoxLayout.Y_AXIS));       final PassiveTextField2 ptf =                   new PassiveTextField2(32);       JTextField tf = new JTextField(32);       JPanel p = new JPanel();       JButton b = new JButton("OK");       p.add(b);       f.getContentPane().add(ptf);       f.getContentPane().add(tf);       f.getContentPane().add(p);       ActionListener 1 = new ActionListener() {          public void actionPerformed(ActionEvent evt) {              System.out.print1n("Action event from a                                 text field");          }       };       ptf.addActionListener(1);       tf.addActionListener(1);       // Make the button the default button       f.getRootPane().setDefaultButton(b);       b.addActionListener(new ActionListener() {          public void actionPerformed(ActionEvent evt) {             System.out.print1n("Content of text field: <"                   + ptf.getText() +">");          }       }) ;       f.pack();       f.setvisible(true);    } } 

The code to copy the keymap is shown in the setKeymap method. The keymap for JTextField consists of two parts the default keymap and the part that contains the usual key mappings for the actions handled by DefaultEditorKit plus the extra action for the RETURN key, which has the default keymap as its resolving parent. To create the new keymap, the second of these two parts is copied into a new keymap, which also has the default keymap as its resolving parent. The code that clones the keymap is very straightforward. The first step is to create an empty keymap with the default keymap as its parent, using the addKeymap method. Then, the set of bound keystrokes from the JTextField map is obtained and the action for each keystroke is copied into the new keymap using the addActionForKeyStroke method. The action associated with the RETURN key, recognized by comparing the name in each of the Action objects with the name used by JTextField, is not copied during this process. The resulting keymap is functionally identical to the one used by JTextField, except that it does not handle the RETURN key. Finally, the new keymap is installed.

If this code executed every time setKeymap were called when the installed keymap is null, we would end up with a modified keymap for every instance of this new component, because the first keymap installed in every such component satisfies this criterion. To avoid this overhead, what we actually need to do is create a new (shared) keymap the first time setKeymap is called when the installed keymap is null. On subsequent setKeymap calls, the shared keymap already created would be used. This would be fine, except that it wouldn't recognize a switch of look-and-feel. Fortunately, during a look-and-feel switch, all the keymaps are set to null before being reloaded, so we can use the occurrence of a setKeymap (null) call as a trigger to clear the reference to the shared keymap, so that it will be recreated the next time a real keymap is loaded.

If you run this example using the command

 java AdvancedSwing.Chapter1.PassiveTextField2 

you'll get the same layout as was shown in Figure 1-14, but now the results are different. Type some text in the upper text field, which has the new key-map, and press return. As before, the default button will activate and the text that you typed will appear in the window from which you started the example. Now, if you type something in the lower field, which is an unmodified JTextField, and press RETURN, the default button does not fire and you get the message "Action event from a text field," which is printed in the text fields ActionListener, showing that the lower JTextField still generates an ActionEvent.

Another Method Intercepting the RETURN key

Before leaving this example, there is one more way of achieving the same effect that we'll show you. So far, we've tried to unmap the RETURN key by adjusting the component's keymap. It seems that removing the mapping for a key in this way is not a simple operation because of the shared nature of key-maps, so what about an implementation that doesn't interfere with the key-map at all? This would avoid all the problems that you saw in the previous two sections. To see how this is possible, let's look at how keys are handled by the text components.

When a key is pressed, the JComponent processKeyEvent method is called. This tries to deal with the key by passing it to various methods, as follows:

  1. If the Swing focus manager is installed, pass it to the focus manager.

  2. If the key has not been consumed, pass it to the processKeyEvent of java. awt. Component, which will result in it being dispatched to registered KeyListeners.

  3. If the key has still not been consumed, pass it to the JComponent processComponentKeyEvent method.

  4. Finally, if processComponentKeyEvent did not consume the key, look for a mapping in the Keystroke registry.

The Swing text components map keys to actions in step 3 of the process, whereas the default button mechanism is part of the Keystroke registry, which appears at step 4. This is why the RETURN key is seen by the key mapping mechanism first and why mapping RETURN to a no-operation instruction (NO-OP) within the text components would not work the fact that there is a mapping of any kind, even one that simply discards the key, would cause JTextComponent to consume the RETURN key in step 3.

However, this sequence also holds the key to solving the problem of preserving the RETURN key so that the default button can grab it. If, at step 3, we could arrange for the key not to be consumed, it would survive to step 4 and the default button would work. And this is easy to arrange just override the JTextComponent processComponentKeyEvent method and return immediately when the RETURN key is received, instead of allowing the key mapping code to handle it. This has the same effect as unmapping the key, but none of the complications that you saw before. The implementation is shown in Listing 1-13.

Listing 1-13 Ignoring Mapped Keys Without Changing the Keymap
 package AdvancedSwing.Chapter1; import javax.swing.*; import javax.swing.text.*; import java.awt.event.*; public class PassiveTextField extends JTextField {    public PassiveTextField() {       this(null, null, 0);    }    public PassiveTextField(String text) {       this(null, text, 0);    }    public PassiveTextField(int columns) {       this(null, null, columns);    }    public PassiveTextField(String text, int columns) {       this(null, text, columns);    }    public PassiveTextField(Document doc, String text,                            int columns) {       super(doc, text, columns);    }    public void processComponentKeyEvent(KeyEvent evt) {       switch (evt.getID()) {       case KeyEvent.KEY_PRESSED:       case KeyEvent.KEY_RELEASED:          if (evt.getKeyCode() == KeyEvent.VK_ENTER) {             return;          }          break;       case KeyEvent.KEY_TYPED:          if (evt.getKeyChar() == '\r') {             return;          }          break;       }       super.processComponentKeyEvent(evt);    }    // Test method    public static void main(String[] args) {       // Unchanged from Listing 1-12: not shown    } } 

As with the previous example, the constructors of this class just mimic those of JTextField so that you can use a PassiveTextField interchangeably with JTextField. The important part of this object is the processComponentKeyEvent method. As you can see, it works slightly differently depending on the type of event received. KEY_PRESSED AND KEY_RELEASED events carry a key code for each key, while the KEY_TYPED event contains the Unicode character for the key, not the key code. As a result, the test for the RETURN key needs to be slightly different for these two cases, but the action taken when it is detected is the same just return without consuming it. All other keys are passed to the superclass processComponentKeyEvent method, which uses the installed keymap to handle them and will consume them if there is a valid mapping. To verify that this works, use the command:

 java AdvancedSwing.Chapterl.PassiveTextField 

You should see the same results as you saw with the previous example.

Whether you prefer this simple implementation or the one in Listing 1-12 is a matter of taste. Certainly, the code in Listing 1-13 is the much simpler of the two and it is far easier to understand. It also has the virtue of being independent of look-and-feel switching. From the purist's point of view, however, it may not be so desirable, because it works by subverting the text components' key mapping mechanism rather than working with it to achieve the desired effect. The difference between these solutions would become less clear-cut if you wanted to take this example a step further and suppress several actions from the keymap. To pursue the solution shown in Listing 1-13 would mean expanding the switch statement to test for each individual key, with the consequent performance overhead for every key that you type. By contrast, the approach taken in Listing 1-12 has no additional overhead after you install the correct keymap.

Implementing Overwrite Mode in a Text Field

The last example in this section demonstrates several aspects of the text component architecture that you've seen in this chapter. When you type text into the standard Swing components, the characters that you type are inserted at the location of the caret, moving existing content, if any, to the right to make room. The only way to replace text with something new is to manually delete it with the BACKSPACE or DELETE key, or to select the characters that you want to remove and then type the replacement text. In some cases, it would be more convenient to switch to a mode in which the characters that you type directly overwrite those already in the text field. Although the Swing text controls do not provide this facility, you can implement it using techniques that you have already seen in this chapter.

To provide a usable overwrite feature, you need to do three things:

  1. Implement the mechanics of the overwrite mode in the control itself. That is, arrange for each new character to replace the one to the right of the cursor instead of being placed before it.

  2. Arrange for the user to be able to toggle between insertion and overwriting. This should be a global setting, so that all the text fields that are capable of overwrite are in the same mode, to avoid confusing the user.

  3. Provide some way to indicate to the user whether a text field is in insert or overwrite mode.

Each of these three aspects will be described separately next.

Implementing the Overwrite Capability

You've already seen that there are two ways to control what happens when the user types into a text control: You can override the model insertstring method and implement a different policy within the document model itself, or you can reimplement the JTextComponent replaceSelection method. You have seen examples of both techniques in this chapter. Either of these methods could be used to implement an overwrite mode, but there is a good reason for choosing one over the other.

Suppose you chose to add this functionality in the model's insertstring method. To do this, you would create a custom document type derived from PlainDocument and override insertstring so that it would either insert the text at the given offset if the model is in insert mode, or first remove the required number of characters from the model and then insert the new content in its place if it is in overwrite mode. The number of characters to be removed would be the length of the replacement string, or the number of characters between the start offset and the end of the model if that is smaller. The code to do this is very simple; here's an example implementation:

 public void insertString(int offset, String str,                          AttributeSet a) throws                          BadLocationException {    if (overwriting == true) {       int length = str.length();       int overlapSize = getLength() - offset;       int overwriteSize = length <= overlapSize ? length :                                      overlapSize;       if (overwriteSize > 0) {          remove(offset, overwriteSize);       }    }    super.insertString(offset, str, a); } 

This would work in almost all cases. However, it falls short in one respect. Consider what happens when the user selects a range of characters from the text field and then types a single replacement character. Let's take an example to illustrate the point. Suppose that the control contains the following (not very original) text:

 abcdefgh 

and that the user selects the letters d, e, and f, and then types 1. What should the result be? In a normal text field that supports only direct insertion, the selected text would be removed and replaced by the 1, so that the control would then contain:

 abc1gh 

In a control with overwrite turned on, the user would expect the same result in other words, replacement of selected text should take precedence over the use of the overwrite mode. Now let's see what happens if you choose to implement the overwrite mode in the control's data model, as shown before.

The process of replacing the text is actually performed in two steps. First, the selected text is removed using the model's remove method. Second, the new text is inserted at the caret location by calling insertString. In this case, if the control contains the string abcdefgh, and then after the remove method is called, the model would contain

 abcgh 

and the caret would be positioned before the letter g. Next, the insert-String method is called to insert the single-character string 1 at offset 3. Using the implementation shown before (or any equivalent implementation), one character would be removed from the model to make room for the 1, which would then be inserted in its place. The control would then contain the text

 abc1h 

which is not what the user would expect to see.

The problem with this approach is that the insertstring method is not aware of the remove operation that precedes it. Because it is written to replace characters rather than move them out of the way, it does this even when it really shouldn't.

The alternative to this is to put the overwrite functionality in the JText-Component replaceSelection method instead. This is a much better solution, because replaceSelection is responsible for the complete operation both removing the selected text, if any, and inserting the new text. Because of this, it can arrange to perform an overwrite operation only if there is no text selected for removal. The details are shown in Listing 1-14.

Listing 1-14 A Text Control That Supports Overwriting of Existing Content
 package AdvancedSwing.Chapter1; import javax.swing.*; import javax.swing.text.*; import java.awt.*; import java.awt.event.*; public class OverwritableTextField extends JTextField {    public OverwritableTextField() {       this(null, null, 0);    }    public OverwritableTextField(String text) {       this(null, text, 0);    }    public OverwritableTextField(int columns) {       this(null, null, columns);    }    public OverwritableTextField(String text, int columns) {       this(null, text, columns);    }    public OverwritableTextField(Document doc,                                 String text,                                 int columns) {       super(doc, text, columns);       overwriteCaret = new OverwriteCaret();       super.setCaret(overwriting ?                   overwriteCaret : insertCaret);    }    public void setKeymap(Keymap map) {       if (map == null) {          super. setKeymap (null) ;          sharedKeymap = null;          return;       }       if (getKeymap() == null) {          if (sharedKeymap == null) {             // Switch keymaps. Add extra bindings.             removeKeymap(keymapName);             sharedKeymap = addKeymap(keymapName, map);             loadKeymap(sharedKeymap, bindings,                        defaultActions);          }          map = sharedKeymap;       }       super.setKeymap(map);    }    public void replaceSelection(String content) {       Document doc = getDocument();       if (doc != null) {          // If we are not overwriting, just do the          // usual insert. Also, if there is a selection,          // just overwrite that (and that only).          if (overwriting == true &&                getSelectionStart() == getSelectionEnd()) {             // Overwrite and no selection. Remove             // the stretch that we will overwrite,             // then use the usual code to insert the             // new text.             int insertPosition = getCaretPosition();             int overwriteLength = doc.getLength() -                                   insertPosition;             int length = content.length();             if (overwriteLength > length) {                overwriteLength = length;             }             // Remove the range being overwritten             try {                doc.remove(insertPosition, overwriteLength);             } catch (BadLocationException e) {                // Won't happen             }          }       }       super.replaceSelection(content);    }    // Change the global overwriting mode    public static void setOverwriting(boolean overwriting) {       OverwritableTextField.overwriting = overwriting;    }    public static boolean isOverwriting() {       return overwriting;    }    // Configuration of the insert caret    public void setCaret(Caret caret) {       insertCaret = caret;    }    // Allow configuration of a new    // overwrite caret.    public void setOverwriteCaret(Caret caret) {       overwriteCaret = caret;    }    public Caret getOverwriteCaret() {       return overwriteCaret;    }    // Caret switching    public void processFocusEvent(FocusEvent evt) {       if (evt.getID() == FocusEvent.FOCUS_GAINED) {          selectCaret();       }       super.processFocusEvent(evt);    }    protected void selectCaret(){       // Select the appropriate caret for the       // current overwrite mode.       Caret newCaret = overwriting ?                      overwriteCaret : insertCaret;       if (newCaret != getCaret()) {          Caret caret = getCaret();          int mark = caret.getMark();          int dot = caret.getDot();          caret.setVisible(false);          super.setCaret(newCaret);          newCaret.setDot(mark);          newCaret.moveDot(dot);          newCaret.setVisible(true);       }    }    protected Caret overwriteCaret;    protected Caret insertCaret;    protected static boolean overwriting = true;    public static final String toggleOverwriteAction =                         "toggle-overwrite";    protected static Keymap sharedKeymap;    protected static final String keymapName =                         "OverwriteMap";    protected static final Action[] defaultActions = {                         new ToggleOverwriteAction()    };    protected static JTextComponent.KeyBinding [] bindings = {       new JTextComponent.KeyBinding(             Keystroke.getKeyStroke(KeyEvent.VK_INSERT, 0),             toggleOverwriteAction)    };    // Insert/overwrite toggling action    public static class ToggleOverwriteAction                        extends TextAction {       ToggleOverwriteAction() {          super(toggleOverwriteAction);       }       public void actionPerformed(ActionEvent evt) {          OverwritableTextField.setOverwriting(                   !OverwritableTextField.isOverwriting());          JTextComponent target = getFocusedComponent();          if (target instanceof OverwritableTextField) {             OverwritableTextField field =                         (OverwritableTextField)target;             field.selectCaret();          }       }    }       public static void main(String[] args) {        JFrame f = new JFrame("Overwrite test");        OverwritableTextField tf =                    new OverwritableTextField(20);        f.getContentPane().add(tf, "North");        tf = new OverwritableTextField(20);        f.getContentPane().add(tf, "South");        f.pack();        f.setVisible(true);    } } 

Listing 1-14 shows a control called OverwritableTextField, derived from JTextField, that does everything that JTextField can do and also implements a toggle to allow the user to overwrite existing text or to insert new text. The state of the toggle is a static boolean called isOverwriting that can be set using the static setoverwriting method. Because this is a static attribute, all instances of OverwritableTextField are either in insert mode or overwrite mode at any given time. Initially, overwriting is selected for the entire application.

The details of the overwrite operation are contained in the replace-Selection method. If the control is in insert mode, there is no need to do anything special, so the replaceSelection method of JTextField is invoked directly. This is also true if there is a non-empty selection, which is detected by calling the getSelectionStart and getSelectionEnd methods and testing for inequality, which indicates that some text has been selected. If the control is in overwrite mode and there is no selection, it is necessary to overwrite existing text with the new content, by first removing it and then adding the replacement characters.

In the simplest case, all that is necessary to determine how much text to remove from the control is to get the length of the text string passed to replaceSelection and remove that many characters. This doesn't work, however, if the replacement string is longer than the number of characters left after the insertion point. For example, suppose the model contains the string

 abcdefg 

and the replaceSelection method is called with the caret at offset 5 (that is, before the letter f) and with three characters to insert. According to the simple algorithm in the last paragraph, we would want to remove three characters from offset 5 to replace them with the three new characters. However, there are only two characters in the model after offset 5 and attempting to remove three would cause the Document remove method to throw a Bad-LocationException. Obviously, the correct thing to do is to remove the smaller of the number of characters in the replacement string and the number of characters between the caret position and the end of the model. This is exactly what the replaceSelection method in Listing 1-14 does. The most common case in which this happens, of course, is when the user is adding text to the end of a text field, when there would be no characters left to remove. In this special case, having the control in overwrite mode is no different from having insert mode selected. Once the characters to be replaced have been removed, the new text can be inserted by calling the original replaceSelection method. Because there is no selection and the caret is not moved by the remove operation, this will just insert the new text in the correct place.

Switching Between Insert and Overwrite Modes

The OverwritableTextField provides the static methods setOverwriting and isOverwriting to enable its operating mode to be set and read. The user, of course, cannot call either of these methods, so how is it possible for the user to change the mode to overwrite or insert? The obvious way to do this is to map the INSERT key so that it calls the setOverwriting method with the appropriate value to select the required mode. For consistency with existing user interface models, in this implementation the insert key behaves as a toggle, flipping all the OverwritableTextFields in the application into insert mode the first time it is pressed, into overwrite mode the next time, and so on.

You already know that to map a key so that it performs some operation on a text component you need to add an entry to the text component's keymap. In this case, we need to map the INSERT key to some action that calls the static setOverwriting method. You've already seen how to change existing keymaps. Here, we want to take the keymap for JTextField, which OverwritableTextField will inherit, and add a single entry to it, without affecting the JTextField keymap. To do this, we create a new keymap that has the JTextField keymap as its resolving parent and add the mapping for the INSERT key to it. This activates the new mapping and makes the mappings for JTextField available within the new control without changing the mappings for JTextField itself. The code that implements the creation of the keymap is shown in the setKeymap method in Listing 1-14. As with the example shown in Listing 1-12, the new keymap is created here so that the mapping is preserved even if the application's look-and-feel is changed. Care is taken to create only a single copy of the new keymap, because it can be shared by all instances of OverwritableTextField. The single copy of the keymap is held in the static variable sharedKeymap. It is created and installed in the control when setKeymap is called, but only if there is no keymap currently installed. This allows the programmer to override the default keymap with another one by explicitly calling setKeymap.

How is the action for the INSERT key added to the keymap and mapped to the code that switches the operating mode of the control? Here is the code that creates the keymap:

 public void setKeymap(Keymap map) {    if (map == null) {       super.setKeymap(null);       sharedKeymap = null;       return;    }    if (getKeymap() == null) {       if (sharedKeymap == null) {          // Switch keymaps. Add extra bindings.          removeKeymap(keymapName);          sharedKeymap = addKeymap(keymapName, map);          loadKeymap(sharedKeymap, bindings, defaultActions);       }       map = sharedKeymap;    }    super.setKeymap(map); } 

The extra key binding is added by the loadKeymap call. When this method is invoked, the new keymap will not have any bindings of its own, and will inherit the bindings of JTextField (because the argument to this method, map, will be the keymap being installed for JTextField and the new map is created with this map as its resolving parent). The loadKeymap method creates a keymap entry for each item in the bindings array mapping the Keystroke in each entry to the corresponding Action in defaultActions, using the Action name to provide the linkage. Here is how bindings and defaultActions are defined:

 public static final String toggleOverwriteAction =                            "toggle-overwrite"; protected static JTextComponent.KeyBinding[] bindings = {    new JTextComponent.KeyBinding(       Keystroke.getKeyStroke(KeyEvent.VK_INSERT, 0),                              toggleOverwriteAction) }; protected static final Action[] defaultActions = {                              new ToggleOverwriteAction() }; 

Both arrays contain just one entry. The bindings entry maps the INSERT key (with no modifier keys pressed) to an Action called "toggle-overwrite," while the defaultActions array contains a reference to a single Action implemented in the class ToggleOverwriteAction. Actions on text components are always implemented by extending the abstract class Text-Action, which is derived from the Swing AbstractAction class. Here is how ToggleOverwriteAction is implemented:

 public static class ToggleOverwriteAction extends TextAction  {    ToggleOverwriteAction() {       super(toggleOverwriteAction);    }    public void actionPerformed(ActionEvent evt) {          OverwritableTextField.setOverwriting(             !OverwritableTextField.isOverwritingf));          JTextComponent target = getFocusedComponent();          if (target instanceof OverwritableTextField) {             OverwritableTextField field =                            (OverwritableTextField)target;             field.selectCaret();          }       }    } } 

The constructor gives the Action a name in this case, the Action will be called "toggle-overwrite"; as noted above, this name connects this Action to the key binding for the INSERT key, so the loadKeymap method will arrange for the actionPerformed method of this object to be invoked if the INSERT key is pressed when the keyboard focus is directed to any OverwritableTextField that has the keymap created earlier.

Core Note

If you are curious about how this mapping works, here are the details. All Keymaps returned by addKeymap are instances of the inner class JTextComponent.DefaultKeymap, which contains a Hashtable that is initially empty. When loadKeymap is called, it takes the Action name from a bindings entry and looks for the Action in the defaultActions array with that name, which will locate the reference to the single ToggleOverwriteAction object held in the defaultActions array. It then creates an entry in the Hashtable with the Keystroke from the bindings entry as the key and the ToggleOverwriteAction reference as the data stored under that key. Later, when the INSERT key is pressed in an OverwritableTextField, the processComponentKeyEvent method builds the corresponding Keystroke object and uses it to access the Hashtable in the keymap. Because two Keystrokes are equal if the keys are the same and the modifiers are the same, this will find the ToggleOverwriteAction object. The Action is then performed by invoking that object's actionPerformed method.



The last piece of the puzzle is how to implement the actionPerformed method. This method has to do everything necessary to switch all the Over-writableTextFields between insert and overwrite modes. Because this state is held as a static member, changing it simply entails using the static method isOverwriting to get the current setting as true or false (in which true represents overwrite mode and false insert mode), flipping it and writing it back with setOverwriting. This simple operation changes the state for all instances of the control, which read the state from the static member overwriting when necessary.

This is not quite all that the actionPerformed method does, however. The rest of the code is responsible for switching the caret between the shape used when the text field is in insert mode and an alternative used to indicate that overwriting is selected. This aspect of the actionPerformed method is discussed in the next section.

Switching the Cursor

To provide a different caret for insert and overwrite modes, you need to do the following:

  1. Allow two different caret objects to be configured.

  2. Set the correct caret for the current mode and switch the caret when the mode changes.

  3. Provide the implementation of a custom caret, at least for the overwrite case.

The first of these is easy to provide: OverwritableTextField inherits the setCaret method from JTextComponent. This method will be called (from the JTextField UI class for the active look-and-feel) when the object is constructed to install the usual single-line caret that you see in all the Swing text components. This caret will continue to be used when the control is in overwrite mode. As you can see from Listing 1-14, the setCaret method is overridden to store a reference to this caret in the instance variable insertCaret. This is necessary because a different caret will be installed when the mode is switched and it will be necessary to restore the original one when the control is reverted to insert mode. To configure a new caret for use in overwrite mode, the setOverwriteCaret method is provided. This method just stores the caret in the instance variable overwriteCaret for later use.

To select a particular caret, the setCaret method of JTextComponent is invoked. When should this method be called? There are two occasions when it is necessary to install a caret:

  • When the control is created, the caret appropriate for the initial mode should be installed.

  • When the mode is changed, the old caret should be removed and the new one activated.

The initial caret is installed at the end of the OverwritableTextField constructor, based on the setting of the overwrite flag. The caret installed is either the standard one installed from the look-and-feel class, or the overwrite caret. Because the overwrite caret cannot be customized before the constructor completes, a default overwrite caret, the implementation of which you'll see shortly, is used if the text field starts in overwrite mode (which is the default).

Core Note

The overwrite flag is a static member that is initially set to true. The setting of this flag cannot be influenced by any of the constructors, so it appears that all OverwritableTextFields will initially have the overwrite caret installed. This is not true however if the setOverwriting method is called with argument false, instances of this control created subsequently will use the insert cursor. This is consistent with all instances of OverwritableTextField using the same type of cursor at all times.



Handling the change of mode from insert to overwriting and vice versa is more difficult. This happens when the user presses the INSERT key when the focus is directed at one of the (possibly many) OverwritableTextFields in the application. Obviously, it is simple enough to change the caret of the OverwritableTextField that receives the INSERT key you'll see the code that does this in the actionPerformed method of the keymap's ToggleOver-writeAction, shown above. When this action is triggered, there is no information available within the event that indicates to the actionPerformed method which control was the target of the action. However, it needs this information in order to change its caret. This is a common problem for all actions activated from a keymap and it is solved by code in JTextComponent that remembers which is the active text component, defined as the text component that currently has the input focus. JTextComponent tracks all focus events for all text components and remembers which text component last received the focus. The static getFocusedComponent method can be used to get a reference to the active component. Having obtained this reference, the actionPerformed method of the ToggleOverwriteAction switches the caret by calling the selectCaret method of the currently active OverwritableTextField, if the current text field is indeed an OverwritableTextField.

Core Note

Under normal circumstances, when the ToggleOverwriteAction is activated, the active text component should be an OverwritableTextField. This will not be the case, however, if a programmer incorrectly bound this action in the keymap of a different text component The explicit test in the actionPerformed method avoids a ClassCastException that would otherwise occur if this mistake were made.



The selectCaret method chooses the appropriate caret for the components operating mode. If this differs from the caret currently installed, the current one is replaced by the new one. Care is needed when installing a new caret in case there is currently a selection within the text field. When you install a caret using the JTextComponent setCaret method, it is positioned at the start of the text field and any existing selection is cleared. This is not user-friendly; The caret should remain in its current location and any existing selection should be preserved. To arrange for this, the current mark and dot positions are saved and then restored using the set Dot and moveDot method. The first of these re-establishes the original mark, while the second resets the original dot position and, by moving the caret away from the mark, recreates the selection. This also works if there was no selection, because in this case the mark and dot positions would have been the same.

It is simple, then, to deal with changing the caret of the active OverwritableTextField, because it is directly involved in the mode-switching operation. However, it is also necessary to do the same for all of the other OverwritableTextFields. One way to achieve this would be to keep track of all such objects and process all of them during a mode switch. However, this would be time-consuming and inefficient if a large number of these objects were in use.

There is, in fact, a better way to handle this. A text component's caret appears only when it has the focus and, furthermore, the only caret that can be used is the one in the focused text component. Therefore, it is possible to delay selecting the correct caret for an OverwritableTextField until it actually gets the focus. At this point, the selectCaret method can be called and if the caret that corresponds to the value of the overwrite attribute is not the same as the currently selected caret, the new one will be installed. With this approach, when the mode is switched, only those OverwritableTextFields that are used after the switch will actually have their caret changed. To make this possible, OverwritableTextField overrides the processFocusEvent method of Component and calls selectCaret when it gains the focus, as you can see in Listing 1-14.

Implementing a Custom Caret

The last piece of the OverwritableTextField is the custom caret that will be used when the control is in overwrite mode. In this section, you'll see how the overwrite caret that the control installs by default is implemented. Because this caret can be changed using the setOverwriteCaret method, you can use the same mechanism used here to create your own custom overwrite caret to replace the default one.

Before looking at the implementation of the caret, let's see how it works. If you type the following command

 java AdvancedSwing.Chapter1.OverwritableTextField 

you'll see a window that contains two OverwritableTextFields stacked one above the other, as shown in Figure 1-15. When the window first appears, the upper text field, which has the focus, has the overwrite caret selected. You can see that it is not the same as the single-line insert caret it looks like a small black blob. If you type a few characters into the text field, the caret moves to the right, staying ahead of the text. Now move the caret to the left by a few characters using the mouse or the arrow keys and you'll see that, as it moves over the text, the caret blob surrounds the character that it is placed over and the color of that character changes to contrast with that of the caret (see Figure 1-15). You'll also notice that the width of the caret adjusts automatically to match the width of the character that it is placed over.

Figure 1-15. A text field with a custom caret.
graphics/01fig15.gif

Next, with the caret over some text, type a few more characters. Now you'll see that the new text overwrites the existing text instead of being inserted to the left of the caret and that the caret moves so that it is positioned over the next character to be replaced. If you want to contrast this with the behavior of the usual insert caret, press the INSERT key and you'll find that the caret changes and that the text field is now in insert mode, as you can verify by typing more characters. Press the INSERT key again to switch back to overwrite mode and notice that the caret becomes a blob again. With overwrite mode selected, press the TAB key and the caret will move to the second text field. Because all the OverwritableTextFields are always in the same mode, the caret in the second text field will also be the overwrite caret. To see that this is true, press INSERT again and the caret in the lower text field will switch back to insert mode; if you now press tab, the focus moves to the upper text field, which will display an insert cursor.

Every caret implements the Caret interface, defined in the Swing text package. There is a concrete implementation of this interface in the class DefaultCaret, which provides the single-line caret that appears in all the standard text components. The simplest way to implement your own caret is to extend this class and override the two methods that deal with managing the cursor's on-screen appearance the paint and damage methods.

When the caret moves, the region of the text component underneath its old location is repainted so that the caret disappears. The caret is then responsible for redrawing itself at its new location. To arrange for this to happen, DefaultCaret calls its own damage method, which is responsible for determining which part of the text component should be repainted to cause the cursor to appear and then calls the text component's repaint method to cause the painting operation to occur. The damage method is given a Rectangle that indicates the new position of the caret; because only the actual caret implementation knows its own shape, the width of this Rectangle will not correspond to the size of the caret itself only the x and y and height members of the Rectangle object can be trusted the height member reflects the height of the drawing area of the text field. When the text component is painted, the paint code in its UI class will notice that the damaged area includes the location occupied by the caret and will call the caret's paint method to have it draw itself. That's how the caret painting mechanism works. Now let's look at how this is implemented for the default overwrite caret used by the OverwritableTextField, shown in Listing 1-15.

Listing 1-15 A Custom Caret
 package AdvancedSwing.Chapter1; import javax.swing.text.*; import javax.swing.plaf.*; import java.awt.*; public class OverwriteCaret extends DefaultCaret{    protected synchronized void damage(Rectangle r) {       if (r != null) {          try {             JTextComponent comp = getComponent();             TextUI mapper = comp .getUI() ;             Rectangle r2 = mapper.modelToView(comp,                                             getDot() + 1);             int width = r2.x - r.x;             if (width == 0) {                width = MIN_WIDTH;             }             comp.repaint(r.x , r.y, width, r.height);             // Swing 1.1 beta 2 compat             this.x = r.x;             this.y = r.y;             this.width = width;             this.height = r.height;          } catch (BadLocationException e) {          }       }    }    public void paint(Graphics g) {       if (isVisible( ) ) {          try {             JTextComponent comp = getComponent();             TextUI mapper = comp.getUI();             Rectangle r1 = mapper.modelToView(comp,                                               getDot());             Rectangle r2 = mapper.modelToView(comp,                                               getDot() + 1);             g = g.create();             g.setColor(comp.getForeground());             g.setXORMode(comp.getBackground());             int width = r2.x - r1.x;             if (width ==0) {                width = MIN_WIDTH;             }             g.fillRect(r1.x, r1.y, width, rl.height);             g.dispose();          } catch (BadLocationException e) {          }       }    }    protected static final int MIN_WIDTH = 8; } 

The overwrite caret is a rectangular blob with two characteristics that determine how the damage and paint methods are implemented:

  1. The width of the caret is variable and matches that of the character that it appears over.

  2. Because the caret appears over the text instead of between the characters, it must be drawn in such a way that it contrasts with both the characters in the text field and its background.

The first point is very important for the damage method. Ideally, this method should arrange for a region of the text component the width of the cursor to be repainted. This means that the width of the character under the caret needs to be known for the damage method to do its job properly. The coordinates of the left side of the caret are passed to the damage method in its Rectangle argument. To compute the width, you need to know the coordinates of the next character. This can be done by using the text component UI class's modelToView method, as shown in Listing 1-15:

 Rectangle r2 = mapper.modelToView(comp, getDot() + 1); 

The getDot method returns the model offset of the character under the caret. Hence, getDot () + 1 corresponds to the offset of the character after the cursor and the line above will store the coordinates of that character in the Rectangle r2. The rest of the damage method calculates the width of the area to be repainted. Notice that if the computed width is zero, the repaint width is set to 8 pixels. This ensures that an 8-pixel caret will be shown when the text component is empty and when the caret is at the extreme right end of the text component.

The paint method uses similar logic to recalculate the size and location of the caret block; because it isn't given the caret position as an argument, it calls modelToView twice to get the offsets of both sides of the caret. Once the bounds and location of the caret are known, the block is colored by using the Graphics fillRect method, filling it with the foreground color. The Graphics object is switched into XOR mode while filling the rectangle, so that the color of the text under the caret, which was drawn in the component's foreground color, is switched to that of the component's background to make it visible.

 

 



Core Swing
Core Swing: Advanced Programming
ISBN: 0130832928
EAN: 2147483647
Year: 1999
Pages: 55
Authors: Kim Topley

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net