Chapter 19. Inside Text Components

In this chapter:

19.1 Text package overview

A truly exhaustive discussion of the text package is beyond the scope of this book. However, in this chapter we hope to provide enough information about text components, and their underlying constituents, to leave you with a solid understanding of their inner workings. Picking up where chapter 11 left off, we continue our discussion of the most significant aspects of the text package classes and interfaces. This chapter concludes with an example of a custom text field used for several variations of date and time selection. In the next chapter, we continue our study of text components with the development of a full-featured word processor application. The examples in chapter 20 demonstrate practical applications of many of the complex topics covered in this chapter.

 

19.1.1 More about JTextComponent

abstract class javax.swing.text.JTextComponent

Associated with each JTextComponent is a set of Actions which are normally bound to specific KeyStrokes (see 2.13) and managed in a hierarchically resolving set of Keymaps (see 19.1.23). We can retrieve a text component’s Actions as an array with the getActions() method. We can retrieve and assign a new Keymap with getKeymap() and setKeymap() respectively.

All text components share a set of default Actions. Each of these Actions are instances of TextAction by default (see 19.1.24). JTextComponent provides a private static EditorKit (see 19.1.25) which consists of a set of four pre-built TextActions shared by all text components through the use of a default Keymap instance (see 19.1.26).

JTextComponent maintains a private reference to the text component that most recently had the keyboard focus. TextActions are designed to take advantage of this, and each TextAction will operate on this component when invoked in the event that the source of the invoking event is not a text component.

Document content is structured hierarchically by Element implementations (see 19.1.9). Each Element maintains a set of attributes encapsulated in implementations of the AttributeSet interface (see 19.1.12). Many Elements also contain one or more child Elements. Attributes that apply to one element also apply to all child Elements, but not vice versa. Each Element has an associated start and ! end Position (see 19.1.6).

AttributeSets can be applied manually to a region of text. However, it is often more convenient to use Styles (see 19.1.14). Styles are AttributeSet implementations that we do not instantiate directly. Rather, Styles are created and maintained by instances of StyleContext (see 19.1.16), and each Style has an associated name allowing easy reference. StyleContext also provides a means for shar! ing AttributeSets across a document or possibly multiple documents, and is particularly useful in large documents.

The cursor of a text component is defined by implementations of the Caret interface (see 19.1.19). We can retrieve the current Caret with getCaret(), and assign a new one with setCaret(). A text component’s Caret is instantiated (but not maintained) by its UI delegate. So when the L&F of a particular text component changes, the Caret in use will also change. JTextComponent supports the addition of CaretListeners that will receive CaretEvents whenever the position of the Caret changes.

Text components also support an arbitrary number of highlights through implementations of the Highlighter interface (see 19.1.17). Highlighters are most often used to indicate a specific selection. They can also be used for many other things such as marking new text additions. Highlighter maintains each highlighted region as an implementation of Highligher.Highlight, and each Highlight can be rendered using a Highlighter.HighlightPainter implementation. As with Carets, a text area’s Highlighter is instantiated by its UI delegate. We can assign/retrieve a text component’s Highlighter with setHighlighter() and getHighlighter() respectively.

JTextComponent also maintains a bound focusAccelerator property, as we discussed in chapter 11, which is a char that is used to transfer focus to a text component when the corresponding key is pressed simultaneously with the ALT key. JTextComponent defines a private Action called focusAction whose actionPerformed() method calls requestFocus(). Initially foc! usAction is not attached to the text component (that is, it is turned off). To activate it we use the setFocusAccelerator() method. Sending \0 to the setFocusAccelerator() method turns it off. Internally, this method searches through all registered KeyStrokes and checks whether any are associated with focusAction, using the getActionForKeyStroke() method. If any are found they are unregistered, using the unregsiterKeyboardAction() method of JComponent. Finally the character passed in is used to construct a KeyStroke to register and associate with focusAction. This action is registered such that it will be invoked whenever the top-level window containing the given text component has the focus:

    // from JTextComponent.java
    registerKeyboardAction(focusAction,KeyStroke.getKeyStroke(aKey,ActionEvent.ALT_MASK),
                           JComponent.WHEN_IN_FOCUSED_WINDOW);

Each text component uses a sub-class of BasicTextUI as its UI delegate. As we mentioned above, each text component also has an EditorKit for storing Actions. This EditorKit is referenced by the UI delegate. JTextField and JTextArea have default editor kits assigned by the UI delegate, whereas JEditorPane and JTextPane maintain their own editor kits independent of their UI delegate.

Unlike most Swing components, a text component’s UI delegate does not directly define how that text component is rendered and laid out. Rather, it implements the ViewFactory interface (see 19.1.29) which requires the implementation of one method: create(Element e). This method returns a View instance (see 19.1.28) responsible for rendering the given Element. Each Element has an associated View that is used to render it. There are many different views provided in the text package, and it is rare that we will need to implement our own! (although this is certainly possible). JTextArea, JTextField, and JPasswordField have specific Views returned by their UI delegate’s create() method. JEditorPane and JTextPane Views are created by the current EditorKit.

We can retrieve a Point location in the coordinate system of a text component corresponding to a character offset with JTextComponent’s viewToModel() method. Similarly, we can retrieve a Rectangle instance describing the size and location of the View responsible for rendering an Element occupying a given character offset with modelToView().

JTextComponent’s margin property specifies the space to use between its border and its document content. Also note that standard clipboard operations can be programmatically performed with the cut(), copy(), and paste() methods.

19.1.2 The Document interface

abstract interface javax.swing.text.Document

In MVC terms, the model of a text component contains the text itself, and the Document interface describes this model. A hierarchical set of Elements (see 19.1.9) define the structure of a Document. Each Document contains one or more root Elements, potentially allowing more than one way of structuring the same content. Most documents only have one structure, and hence one root element. This element can be accessed with getDefaultRootElement(). All root elements, including the default root ele! ment, are accessible with getRootElements(), which returns an Element array.

 

Note: We will not discuss the details of maintaining multiple structures, as this is very rarely desired. See the API docs for examples of situations in which multiple structures might be useful.

Documents maintain two Positions which keep track of the beginning and end positions of the content. These can be accessed with getStartPosition() and getEndPosition() respectively. Documents also maintain a length property, accessible with getLength(), that maintains the number of contained characters.

The Document interface declares methods for adding and removing DocumentListeners (see 19.1.8), for notification of any content changes, and UndoableEditListeners (allowing easy access to built-in undo/redo support -- refer back to chapter 11 for an example of adding undo/redo support to a text area).

Methods for retrieving, inserting, and removing content are also declared: getText(), insertString(), and remove(). Each of these throws a BadLocationException if an illegal (i.e. nonexistent) location in the document is specified. The insertString() method requires an AttributeSet instance describing the attributes to apply to the given text (null can be used for this parameter). Plain text components will not pay any attention to this attribute set. Text compo! nents using a StyledDocument instance most likely will pay attention to these attributes.

The createPosition() method is intended for inserting a Position instance at a given index, and the putProperty() and getProperty() methods are meant to insert and retrieve various properties stored in an internal collection.

The render() method is unique. It takes a Runnable as parameter, and is intended to ensure thread safety by not allowing document content to change while that Runnable is running. This method is used by each text component’s UI delegate during painting.

 

19.1.3 The StyledDocument interface

abstract interface javax.swing.StyledDocument

This interface extends the Document interface to add functionality for working with Styles and other AttributeSets. Implementations are expected to maintain a collection of Style implementations. This interface also declares the notion of character and paragraph attributes, and logical styles. What these mean is specific to each StyledDocument implementation (we will discuss these more when we talk about DefaultStyledDocument in 19.1.11).

The setCharacterAttributes() method is intended to assign a given set of attributes to a given range of document content. A boolean parameter is also required which is meant to specify whether or not pre-existing attributes of the affected content should be overwritten (true) or merged (false -- only new attributes are assigned). The setParagraphAttributes() method should work the same as setCharacterAttributes(), but is meant to apply to the number of paragraphs spanned by a given range of content. The getFont(), getBackground(), and getForeground() methods take an AttributeSet parameter, and are intended to be used for convenient access to the corresponding attribute in the given set (if it exists).

StyledDocuments are meant to allow addition, removal, and retrieval of Styles from an internal collection of Styles. The addStyle() method is intended to take a String and parent Style as parameters and return a new Style with the given name and given Style as its resolving parent. The getLogicalStyle() method should ret! urn a Style corresponding to the paragraph containing the given character offset. The setLogicalStyle() method is intended to assign a Style to the paragraph containing the given character offset. The getStyle() and removeStyle() methods should retrieve and remove a Style with the given name, respectively, in the internal collection.

The getCharacterElement() and getParagraphElement() methods are intended to allow retrieval of Elements corresponding to a given character offset. The definition of these methods will vary based on the definition of paragraph and character Elements in a StyledDocument implementation. Typically, a character Element represents a range of text containing a given offset, and a paragraph Element represents a paragraph containing the given offset.

19.1.4 AbstractDocument

abstract class javax.swing.text.AbstractDocument

AbstractDocument implements the Document interface and provides a base implementation for text component models. Two provided classes that extend AbstractDocument are used by the Swing text components as their default model: PlainDocument and DefaultStyledDocument. PlainDocument is used by all the plain text components, such as JTextArea, JTextField and its subclass, JPasswordField. It provides support for character data content only and does not support markup (i.e. multiple fonts, colors, etc.) of this content. DefaultStyledDocument is used by more sophisticated text components such as JEditorPane and its subclass, JTextPane. It provides support for markup of text by implementing the StyleDocument interface.

AbstractDocument specifies a mechanism that separates the storage of character data from the structuring of that data. Thus, we have the capability to store our text however we like without concern for how the document is structured and marked up. Similarly, we can structure a document with little concern for how its data is stored. The significance of this structure-storage separation will make more sense after we have discussed Elements and attributes below. Character data is stored in an instance of the inner Content interface which we will also discuss below.

This class defines functionality for a basic read/write locking scheme. This scheme enforces the rule that no write can occur while a read is occurring. However, multiple reads can occur simultaneously. To obtain a read lock we use the render() method. This method releases the read lock when it finishes executing the Runnable passed to it. No other access methods acquire such a lock (making them non-thread safe). The getText() method, for example, does not acquire a read lock. In a multithreaded environment, any text retrieved with this method may be corrupted if a write was occurring at the time the text was retrieved.

The read lock is basically just an increment in an internal variable that keeps track of the number of readers. The readLock() method does this for us, and will force the current thread to wait until no write locks exist. When the Runnable finishes executing, the internal reader-count variable is decremented. The readUnlock() method is responsible for this. Note that both of these methods will simply do nothing and return if the current thread is the writer thread. Also note that a StateInvariantError exception will be thrown if a read unlock is requested when there are no readers.

The write lock is a reference to the writing thread. The writeLock() and writeUnlock() methods take care of this for us. Whenever a modification is requested the write lock must first be obtained. If the writer thread is not null, and is not the same as the invoking thread, writeLock() blocks the current thread until the current writer releases the lock by calling writeUnlock().

If we intend to use the protected reader and writer locking methods ourselves in a sub-class, we are encouraged to ensure that a readUnlock() call will be made no matter what happens in the try block, using the following semantics:

  // From AbstractDocument.java
  try {
    readLock();
    // do something
  } finally {
    readUnlock();
  }

All methods that modify document content must obtain a write lock before any modification can take place. These methods include insertString() and remove().

AbstractDocument’s dump() method prints the document’s Element hierarchy to the given PrintStream for debugging purposes. For example, the following class will dump a JTextArea’s Element hierarchy to standard output.

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

public class DumpDemo extends JFrame
{
  JTextArea m_editor;

  public DumpDemo() {
    m_editor = new JTextArea();
  
    JScrollPane js1 = new JScrollPane(m_editor);
    getContentPane().add(js1, BorderLayout.CENTER);

    JButton dumpButton = new JButton("Dump");
    dumpButton.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        ((PlainDocument) m_editor.getDocument()).dump(System.out);
      }
    });

    JPanel buttonPanel = new JPanel();
    buttonPanel.add(dumpButton);

    getContentPane().add(buttonPanel, BorderLayout.SOUTH);

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

  public static void main(String[] args) {
    new DumpDemo();
  }   
}

Typing this text in the JTextArea:

Swing is

powerful!!

...produces the following output when the dump() method is invoked (this will make more sense after we discuss Elements in 19.1.9).

<paragraph>

<content>

[0,9][Swing is

]

<content>

[9,20][powerful!!

]

<content>

[20,21][

]

<bidi root>

<bidi level

bidiLevel=0

>

[0,21][Swing is

powerful!!

]

AbstractDocument also includes several significant inner classes and interfaces. Most we will discuss in appropriate places later in this chapter. A brief overview is appropriate here:

abstract class AbstractDocument.AbstractElement: This class implements the Element and MutableAttributeSet interfaces, allowing instances to act both as Elements and the mutable AttributeSets that describe them. This class also implements the TreeNode interface, providing an easy means of displaying document structure with a JTree.

class AbstractDocument.BranchElement: A concrete sub-class of AbstractDocument.AbstractElement that represents an Element which can contain multiple child Elements (see 19.1.9).

class AbstractDocument.LeafElement: A concrete sub-class of AbstractDocument.AbstractElement that represents an Element which cannot contain child Elements (see 19.1.9).

static abstract interface AbstractDocument.Content: Defines the data storage mechanism used by AbstractDocument sub-classes (see 19.1.9).

static abstract interface AbstractDocument.AttributeContext: Used for efficient AttributeSet management (see 19.1.16).

static class AbstractDocument.ElementEdit: Extends AbstractUndoableEdit, implements DocumentEvent.ElementChange (see 19.1.7), and allows document changes to be undone and redone.

class AbstractDocument.DefaultDocumentEvent: Extends CompoundEdit and implements DocumentEvent (see 19.1.7). Instances of this class are used by documents to create UndoableEdits, which can be used to create UndoableEditEvents for dispatching to UndoableEditListeners. Instances of this class are also fired to any registered DocumentListeners (see 19.1.8) for change notification.

19.1.5 The Content interface

abstract static interface javax.swing.text.AbstractDocument.Content

In order to implement a data storage mechanism for text, AbstractDocument provides the static Content interface. Every Document character storage mechanism must implement this interface. (Images and other embedded objects are not considered part of a document’s content.) Each Content instance represents a sequence of character data, and provides the ability to insert, remove, and retrieve character data with the insertString(), remove(), getString(), and getChars() methods.

 

Note: A special convenience class called Segment allows access to fragments of actual document text without the need to copy characters into a new array for processing. This is used internally by text components to speed up searching and rendering large documents.

Implementations of Content must also provide the ability to create position markers that keep track of a certain location between characters in storage with the createPosition() method. These markers are implementations of the Position interface.

Content implementations provide UndoableEdit objects that represent the state of storage before and after any change is made. The insertString() and remove() methods are meant to return such an object each time they are invoked, allowing insertions and removals to be undone and redone.

Two Content implementations are included in the javax.swing.text package: StringContent and GapContent. StringContent stores character data in a normal char array. GapContent also stores data in a char array but it purposefully leaves an empty space, a gap, in this array. "The gap is moved to the location of changes to take advantage of common behavior where most changes are in the same! location. Changes that occur at a gap boundary are generally cheap and moving the gap is generally cheaper than moving the array contents directly to accommodate the change"API. This gap is strictly used for internal efficiency purposes and is not accessible outside of this class.

 

Note: StringContent was used in earlier implementations of PlainDocument and DefaultStyledDocument, but has been replaced by GapContent (which extends a package private class called GapVector). The gap buffer algorithm used in GapContent is very efficient for keeping track of large numbers of Positions and, interestingly, is used in the popular emacs editor.

 

19.1.6 The Position interface

abstract interface javax.swing.text.Position

This interface consists of one method, getOffset(), which returns an int value representing the location, or offset, from the beginning of the document’s content. Figure 19.1 illustrates what happens to a Position marker when text is inserted and removed from storage. This figure starts by showing a document containing "Swing text" as its content. There are initially Position markers at offsets 0, 4, and 7. When we remove the characters from offset 4 through 9 the Position at offset 7 is moved to offset 4. At this point there are two Positions at offset 4 and the document content is "Swin". When we insert "g is" at offset 4 both Positions at offset 4 are moved to offset 8 and the document content becomes "Swing is".

 

Note: The term range refers to a sequence of characters between two Position markers as shown in figure 19.1.

Figure 19.1 Position movement

19.1.7 The DocumentEvent interface

abstract interface javax.swing.event.DocumentEvent

Changes to a Document’s content are encapsulated in implementations of the DocumentEvent interface, the default implementation of which is AbstractDocument.DefaultDocumentEvent. There are three types of changes that can occur to document content: CHANGE, INSERT, and REMOVE (fields defined within the DocumentEvent.EventType inner class). DocumentEvent ! also defines an interface within it called ElementChange. Implementations of this interface, the default of which is AbstractDocument.ElementEdit, are responsible for storing information about changes to the structure of a document for use in, among other things, undo and redo operations. AbstractDocument handles the firing of DefaultDocumentEvents appropriately with its fireXXUpdate() methods.

The getChange() method is meant to take an Element instance as parameter, and return an instance of DocumentEvent.ElementChange describing the elements that were added and/or removed, as well as the location of a change. The getDocument() method should return a reference to the Document instance that generated this event. The getLength() method is intended to return the length of a change, and the getOffset() method should return the offset at which a change beg! an. The getType() method is meant to return an instance of Document.EventType specifying the type of change that occurred to the document.

19.1.8 The DocumentListener interface

abstract interface javax.swing.event.DocumentListener

Instances of this interface can be attached to Document implementations to be notified of changes in that document’s content. It is important to note that this notification will always occur after any content has been updated. Knowing this, it is even more important to realize that we should not perform any changes to the content of a document from within a DocumentListener. This can potentially result in an infinite loop in situations where a document event causes another to be fired.

 

Note: Never modify the contents of a document from within a DocumentListener.

The insertUpdate() and removeUpdate() methods are meant to give notification of content insertions and removals respectively. The changedUpdate() method is intended to provide notification of attribute changes.

19.1.9 The Element interface

abstract interface javax.swing.text.Element

Elements provide a hierarchical means of structuring a Document’s content. Associated with each Element is a set of attributes encapsulated in an AttributeSet implementation. These attributes provide a means of specifying the markup of content associated with each Element. AttributeSets most often take the form of Style implementations and are grouped together inside a StyleContext objec! t. StyleContext objects are used by StyledDocument implementations such as DefaultStyledDocument. The objects that are responsible for actually rendering text components are implementations of the abstract View class. Each Element has a separate View object associated with it, and each View recognizes a predefined set of attributes used in the actual rendering and layout of that Element.

 

Note: Elements are objects that impose structure on a text component’s content. They are actually part of the document model, but they are also used by views for text component rendering.

The getAttributes() method is meant to retrieve an AttributeSet collection of attributes describing an Element. The getElement() method is intended to fetch a child Element at the given index, where the index is given in terms of the number of child Elements. The getElementCount() method should return the index of the Element closest to the provided document content offset. The getElementCount() method is meant to return the number of child Elements an Element contains (0 if the parent Element itself is a leaf). The isLeaf() method is intended to tell us whether or not an Element is a leaf element, and getParentElement() should return an Element’s parent Element.

The getDocument() method is meant to retrieve the Document instance an Element belongs to. The getStartOffset() and getEndOffset() methods should return the offset of the beginning and end of an Element, respectively, from the beginning of the document. The getName() method is intended to return a short String description of an Element.

AbstractDocument defines the inner class AbstractElement, which implements the Element interface. As we mentioned earlier, there are two subclasses of AbstractElement defined within AbstractDocument: LeafElement and BranchElement. Each LeafElement has a specific range of content text associated with it (this range can change when content is inserted, removed, or replaced--figures 19.2! and 19.3 illustrate). LeafElements cannot have any child Elements. BranchElements can have any number of child Elements. The range of content text associated with BranchElements is the union of all content text associated with their child LeafElements. (Thus the start offset of a BranchElement is the lowest start offset of all its child LeafElements, and its end offset is the highest end offset of all its ch! ild LeafElements.) DefaultStyledDocument provides a third type of element called SectionElement which extends BranchElement. The meaning of each type of element differs depending on the type of document.

The text package also includes an ElementIterator class, which is designed to traverse an Element hierarchy in a depth first fashion (i.e. postorder -- see 17.1.2). Methods first(), current(), depth(), next(), and previous() can be used to obtain information about, and programmatically traverse, an Element hierarchy. We can construct an ElementIterator object by providing either a Document or an Element to the ElementIterator constructor. If a Document is provided, the default root Element of that document is used as the root of the Element hierarchy traversed by ElementIterator.

 

Note: ElementIterator does not provide any thread safety by default, so it is our responsibility to ensure that no Element changes occur during traversal.

 

19.1.10 PlainDocument

class javax.swing.text.PlainDocument

This class extends AbstractDocument and is used by the basic text components: JTextField, JPasswordField, and JTextArea. When enforcing certain input, usually in a JTextField, we normally override AbstractDocument’s insertString() method in a PlainDocument sub-class (see the discussion of JTextField in chapter 11 for an example).

PlainDocument uses a BranchElement as its root and has only LeafElements as children. In this case each LeafElement represents a line of text and the root BranchElement represents the whole document text. PlainDocument identifies a BranchElement as "paragraph" and a LeafElement as "content". Note that the notion of a paragraph in PlainDocument is much different than our normal notion of a paragraph. Usually we think of paragraphs as sections of text separated by line breaks. However, PlainDocument considers each section of text ending with a line break as a line of "content" in its never-ending "paragraph". Figure 19.2 and 19.3 show the structure of a sample PlainDocument and illustrate how Elements and their associated Positions can change when document content changes.

Figure 19.2 Sample PlainDocument structure

In figure 19.2 we see a PlainDocument containing three elements. Two LeafElements represent two lines of text and are children of the root BranchElement. Note that the this root element begins at offset 0, the start offset of the first LeafElement, and ends at 19, the end offset of the last LeafElement. This document would be displayed in a JTextArea as:

Swing is

powerful!!

 

Note: The line break at the end of the second LeafElement is always present at the end of the last Element in any PlainDocument. It does not represent a line break that was actually inserted into the document and is not counted when the document length is queried using the getLength() method. Thus the length of the document shown in figure 19.2 would be returned as 19.

Now suppose we insert two line breaks at offset 5. Figure 19.3 shows the structure that would result from this addition.

Figure 19.3 Sample PlainDocument structure after inserting two line breaks at offset 19

This document would now be displayed in a JTextArea as:

Swing

is

powerful!!

JTextArea, JTextField, and JPasswordField use PlainDocument as their model. Only JTextArea allows its document to contain multiple LeafElements. JTextField and its JPasswordField subclass allow only one LeafElement.

19.1.11 DefaultStyledDocument

class javax.swing.text.DefaultStyledDocument

DefaultStyledDocument provides significantly more power over the PlainDocument structure described above. This StyledDocument implementation (see 19.1.3) is used for marked up (styled) text. JTextPane uses an instance of DefaultStyledDocument by default (although this may change based on its content type).

DefaultStyledDocument uses and instance of its inner SectionElement class as its root Element, which has only instances of AbstractDocument.BranchElement as children. These BranchElements represent paragraphs, referred to as paragraph Elements, and they contain instances of AbstractDocument.LeafElement as children. These LeafElements represent what are referred to as character ! Elements. Character Elements represent regions of text (possibly multiple lines within a paragraph) that share the same attributes.

We can retrieve the character Element occupying a given offset with the getCharacterElement() method, and we can retrieve the paragraph Element occupying a given offset with the getParagraphElement() method.

We will discuss attributes, AttributeSets, and their usage details soon enough. However, it is important to understand here that AttributeSets assigned to DefaultStyledDocument Elements resolve hierarchically. For instance, a character Element will inherit all attributes assigned to itself, as well as those assigned to the parent paragraph Element. Character Element attributes override those of the same type defined in the parent paragraph Element’s AttributeSet.

 

Note: The Elements used by DefaultStyledDocument are derived from AbstractDocument.AbstractElement, which implements both the Element and MutableAttributeSet interfaces. This allows these Elements to act as their own AttributeSets, and use each other as resolving parents.

Figure 19.4 shows a simple DefaultStyledDocument in a JTextPane with two paragraphs.

Figure 19.4 A two-paragraph DefaultStyledDocument, with several different attributes, in a JTextPane.

Using AbstractDocument’s dump() method to display this document’s Element structure to standard output (see 19.1.4), we get the following:

<section>

<paragraph

RightIndent=0.0

LeftIndent=0.0

resolver=NamedStyle:default {name=default,nrefs=2}

FirstLineIndent=0.0

>

<content

underline=false

bold=true

foreground=java.awt.Color[r=0,g=128,b=0]

size=22

italic=false

family=SansSerif

>

[0,6][Swing

]

<paragraph

RightIndent=0.0

LeftIndent=0.0

resolver=NamedStyle:default {name=default,nrefs=2}

FirstLineIndent=0.0

>

<content

underline=false

bold=false

foreground=java.awt.Color[r=0,g=0,b=0]

size=12

italic=false

family=SansSerif

>

[6,9][is ]

<content

underline=false

bold=false

foreground=java.awt.Color[r=0,g=0,b=0]

size=12

italic=false

family=SansSerif

>

[9,19][extremely ]

<content

underline=false

bold=false

foreground=java.awt.Color[r=0,g=0,b=192]

size=18

italic=true

family=SansSerif

>

[19,27][powerful]

<content

underline=false

bold=true

foreground=java.awt.Color[r=255,g=0,b=0]

size=20

italic=false

family=SansSerif

>

[27,28][!]

<content>

[28,29][

]

<bidi root>

<bidi level

bidiLevel=0

>

[0,29][Swing

is extremely powerful!

]

Note the use of <section>, <paragraph>, and <content> to denote SectionElement, BranchElement, and LeafElement respectively. Also note that the <paragraph> and <content> tags each contain several attributes. The <paragraph> attributes represent paragraph Element attributes and the <content> attributes represent character Element attributes. We will discuss specific attributes in more detail below. Note that the <bidi root> tag specifies a second root Element allowing bidirectional text (this functionality is incomplete as of Java 2 FCS).

We can assign paragraph and character attributes to a region of text with the setParagraphAttributes() and setCharacterAttributes() methods respectively. These methods require a start and end offset, specifying the region to apply the attributes to, as well as an AttributeSet containing the attributes, and a boolean flag specifying whether or not to replace pre-existing attributes with the new attributes.

Regarding the range of text, paragraph attributes will be applied to paragraph Elements that contain at least some portion of the specified range. Character attributes will be applied to all character Elements that intersect that range. If the specified range only partially extends into a character Element, that Element will be split into two, so that only the specified range of text will receive the new attributes (this splitting is handled by an instance of the ElementBuffer inner class).

Regarding the boolean flag, if the flag is true, all pre-existing paragraph Element attributes are removed before the new set is applied. Otherwise, the new set is merged with the old set, and any new attributes overwrite pre-existing attributes. Character attributes work in a similar way, but they do not change paragraph attributes at all--they simply override them.

DefaultStyledDocument also defines the notion of logical paragraph Styles. A logical paragraph Style acts as the resolving parent of a paragraph Element’s AttributeSet. So attributes defined in a paragraph Element’s AttributeSet override those defined in that paragraph’s logical Style. We can change a specific paragraph Element’s logical style with the setLogicalStyle() method. The logical style of each paragraph defaults to StyleContext.DEFAULT_STYLE (which is empty by default).

JTextPane implements getParagraphAttributes(), setParagraphAttributes(), getLogicalStyle(), and setLogicalStyle() methods which communicate directly with its StyledDocument. JTextPane’s paragraph attributes and logical style setXX() methods apply to the paragraph the caret currently resides in if there is no selection. If there is a selection, these methods apply to all paragraphs spanned by! the selected region. JTextPane’s paragraph attributes and logical style getXX() methods apply to the paragraph currently containing the caret.

JTextPane also implements getCharacterAttributes() and setCharacterAttributes() methods. If there is a selection, the setCharacterAttributes() method will act as described above, splitting Elements as needed. If there is no selection, this method will modify JTextPane’s input attributes.

 

Note: JTextPane’s input attributes is a reference to an AttributeSet which changes with the location of the caret. This reference always points to the attributes of the character Element at the current caret location. We can retrieve it at any time with JTextPane’s getInputAttributes() method. Whenever text is inserted in a JTextPane, the current input attributes will be applied to that text by default. However, any attributes explicitly assigned to newly inserted text will override those defined! in the current input attributes.

A StyleContext instance (see 19.1.16) is associated with each DefaultStyledDocument. As we mentioned in the beginning of this chapter, the Style interface describes a named mutable AttributeSet, and the StyledDocument interface describes a Document which manages a set of Styles. A DefaultStyledDocument’s StyleContext ins! tance is what performs the actual management, creation, and assignment of that document’s Styles. If a StyleContext is not provided to the DefaultStyledDocument constructor, a default version is created.

JTextPane defines several methods for adding, removing, and retrieving Styles, as well as specific attributes within a given AttributeSet (such as the getFont() and getForeground() methods). Calls to these methods are forwarded to methods of the same signature in JTextPane’s StyledDocument, and, in the case of DefaultStyledDocument, these calls are forwarded to the StyleContext in charge of all the Styles.

DefaultStyledDocument also includes several significant inner classes:

static class DefaultStyledDocument.AttributeUndoableEdit: This class extends AbstractUndoableEdit to allow AttributeSet undo/redo functionality with Elements.

class DefaultStyledDocument.ElementBuffer: Instances of this class are used to manage structural changes in a DefaultStyledDocument, such as the splitting of Elements, or the insertion and removal of text, resulting in the modification of, and the insertion and/or removal of, various Elements. This class also plays a critical role in constructing AbstractDocument.DefaultDocumentEvents (see 19.1.4).

static class DefaultStyledDocument.ElementSpec: This class describes an Element that can be created and inserted into a document in the future with an ElementBuffer.

protected class DefaultStyledDocument.SectionElement: This class extends AbstractDocument.BranchElement and acts as a DefaultStyledDocument’s default root Element. It contains only BranchElement children (representing paragraphs).

19.1.12 The AttributeSet interface

abstract interface javax.swing.text.AttributeSet

An attribute is simply a key/value pair (as in a Hashtable) that should be recognized by some View implementation available to the text component being used. As we know from our discussion above, each Element in a DefaultStyledDocument has an associated set of attributes which resolves hierarchically. The attributes play a critical role in how that piece of the document will be rendered by a View. For example, one commonly used attribute is FontFamily. The FontFamily attribute key is an Object consisting of the String "family". The FontFamily attribute value is a String representing the name of a font (i.e. "monospaced"). Other examples of attribute keys include "Icon" and "Component," whose values are instances of Icon and Component respectively.

If an attribute is not recognized by a View, the Element associated with that view will not be rendered correctly. Thus, there is a predefined set of attributes that is recognized by the Swing View classes, and these attribute keys should be considered reserved -- in other words, all new attributes should use new keys. These predefined attribute keys are all accessible as static Objects in the StyleConstants class (see 19.1.15).

Sets of attributes are encapsulated in implementations of either the AttributeSet interface, the MutableAttributeSet interface (see 19.1.13), or the Style interface (see 19.1.14). Style extends MutableAttributeSet which, in turn, extends AttributeSet. The AttributeSet interface describes a read-only set of attributes because it does not provide methods for changing, adding, or removing attributes from that set.

The containsAttribute() and containsAttributes() methods are intended for checking whether an AttributeSet contains a given attribute key/value pair, or any number of such pairs. The copyAttributes() method is meant to return a fresh, immutable copy of the AttributeSet it is invoked on. The getAttributeCount() method should return the number of attributes contained in a set, and getAttributeNames() should retrieve an Enumeratio! n of the keys describing each attribute. The isDefined() method is intended for checking whether a given attribute key corresponds to an attribute directly stored in the AttributeSet the method is invoked on (resolving parents are not searched). The isEqual() method is meant to compare two AttributeSets and return whether or not they contain identical attribute key/value pairs. The getResolveParent() method should return a reference to an AttributeSet’s resolving parent, if any, and the getAttribute() me! th! od is intended to return the value of an attribute corresponding to a given key.

The AttributeSet interface also provides four empty static interfaces: CharacterAttribute, ColorAttribute, FontAttribute, ParagraphAttribute. The only reason these interfaces exist is to provide a signature (i.e. information about the class in which it is defined) which is expected of each attribute key. This signature can be used to verify whether an attribute belongs to a certain category (see 19.1.15).

Only one direct implementation of the AttributeSet interface exists within the text package: StyleContext.SmallAttributeSet. A SmallAttributeSet is an array of attribute key/value pairs stored in the alternating pattern: key1, value1, key2, value2, etc. (thus the number of attributes contained in a SmallAttributeSet is actually half the size of its array). An array is used for storage because AttributeSet describes a read-only set of attributes, and using an array is more memory-efficient than dynamically resizable storage such as that provided by a Hasht! able. However, it is less time-efficient to search through an array than a Hashtable. For this reason, SmallAttributeSet is intended to be used only for small sets of attributes. These sets are usually shared between several Elements. Because of the way sharing works (see 19.1.16), the smaller the set of attributes the better candidate that set is for being shared.

19.1.13 The MutableAttributeSet interface

abstract interface javax.swing.text.MutableAttributeSet

The MutableAttributeSet interface extends the AttributeSet interface and declares additional methods intended to allow attribute addition, removal, and resolving parent assignment: addAttribute(), addAttributes(), setResolveParent(), removeAttribute(), and two variations of removeAttributes().

MutableAttributeSet also has two direct implementations within the text package: AbstractDocument.AbstractElement and SimpleAttributeSet. The fact that AbstractElement implements MutableAttributeSet allows such Elements to act as resolving parents to one another. It also reduces object overhead by combining structural information about a region of text with that region’s stylistic attributes.

SimpleAttributeSet uses a Hashtable to store attribute key/value pairs because it must be dynamically resizable. By nature, a Hashtable is less efficient than an array in memory usage, but more efficient in look-up speed. For this reason, SimpleAttributeSets are used for large sets of attributes that are not shared.

 

Note: In the past few sections we have alluded to the importance of efficiency in attribute storage. Efficiency here refers to both memory usage and speed of attribute location. To summarize the issues: A View uses attributes to determine how to render its associated Element. These attribute values must be located, by key, within that Element’s attribute set hierarchy. The faster this location occurs the quicker the view is rendered and the more responsive the user interface becomes. So look-up speed is a large factor in deciding how to store attribute key/value pairs. Memory usage is also a large issue. Obtaining efficient look-up speed involves sacrificing efficient memory usag! e, and vice-versa. This necessary trade-off is taken into account through the implementation of the different attribute storage mechanisms described above, and the intelligent management of when each mechanism is used. We will soon see that the StyleContext class acts as, among other things, this intelligent manager.

 

19.1.14 The Style interface

abstract interface javax.swing.text.Style

The Style interface extends MutableAttributeSet and provides the ability to attach listeners for notification of changes to its set of attributes. Style also adds a String used for name identification. The only direct implementation of the Style interface is provided by StyleContext.NamedStyle. Internally, NamedStyle maintains its own private AttributeSet implementation that contains all its attributes. This AttributeSet can be an instance of StyleContext.SmallAttributeSet or SimpleAttributeSet, and may switch back and forth between these types over the course of its lifetime (this will become clear after our discussion of StyleContext).

19.1.15 StyleConstants

class javax.swing.text.StyleConstants

The StyleConstants class categorizes predefined attribute keys into members of four static inner classes: CharacterConstants, ColorConstants, FontConstants, and ParagraphConstants. These Objects are all aliased from their outer class, StyleConstants, so they are more easily accessible (aliasing here means providing a reference to an object of an inner class). Also, both ColorConst! ants and FontConstants keys are aliased by CharacterConstants to provide a sensible hierarchy of attribute key organization.

 

Note: Not all aliased keys use the same name in each class. For instance, FontFamily in StyledConstants is an alias of Family in StyledConstants.CharacterConstants. However, Family in StyledConstants.CharacterConstants is an alias of Family (the actual key) in StyledConstants.FontConstants. Each is a reference to the same key object and it makes no differ! ence which one we use.

Most keys are self-explanatory in meaning. The StyleConstants API documentation page contains a helpful diagram illustrating the meaning of some of the less self-explanitory attribute keys that apply to paragraphs of styled text. (Each of the keys illustrated in this diagram is an alias of the actual key defined in StyleConstants.ParagraphConstants.)

StyleConstants also defines static methods for assigning and retrieving many predefined attributes in an AttributeSet. For example, to assign a specific font family attribute to an AttributeSet (assuming it is mutable), we can use StyleConstantssetFontFamily() method.

19.1.16 StyleContext

class javax.swing.text.StyleContext

StyleContext implements the AbstractDocument.AttributeContext interface, and declares a set of methods used to modify or fetch new instances of AttributeSet implementations. AbstractContext was designed with the understanding that the implementor may use more than one type of AttributeSet implementation to store sets of attributes. The decision to use one type over another may be based on any number of factors, and StyleContext takes full advantage of this design.

StyleContext‘s main role is to act as a container for Styles that may be used by one or more DefaultStyledDocuments. It maintains a private NamedStyle instance used to store its Styles and allow access by name. Each of these contained Styles is also an instance of NamedStyle. So, to clarify, StyleContext maintains a NamedStyle instance whose key/value pai! rs are of the form String/NamedStyle.

StyleContext also maintains a subset of these NamedStyle values in a Hashtable. Only those NamedStyle’s whose AttributeSet contains 9 or less attributes are stored in this Hashtable and their AttributeSets are maintained as instances of SmallAttributeSet. Those NamedStyles with an AttributeSet containing 10 or more attributes are not stored in the Hashtable, and their AttributeSets are maintained as instances of SimpleAttributeSet.

This partitioning is managed dynamically by StyleContext, and is the result of combining the AbstractContext design with the use of a compression threshold (a hard-coded int value of 9). Whenever an attribute is added or removed, StyleContext checks the number of attributes in the target AttributeSet. If the resulting set will contain 9 or less attributes it remains, or is converted to, a SmallAttributeSet and is added to the Hashtable if it wasn’t alre! ady there. If the resulting set will contain 10 or more attributes it remains, or is converted to, a SimpleAttributeSet and is removed from the Hashtable if it was already there.

The reason for this partitioning is to support efficient AttributeSet sharing. Most styled documents contain many distinct regions of identically styled text. These regions normally have a small number of attributes associated with them. It is clear that the best thing to do in this situation is to assign the same AttributeSet to each of these regions. And the best AttributeSet implementation to use for this is SmallAttributeSet due to its superior memory efficiency (since look-up speed is a minor issue with a very small number of attributes). Larger sets of attributes are, in general, rare. The best AttributeSet implementation to use for this is SimpleAttributeSet due to its superior look-up capabilities (since memory usage will most likely be a minor issue with a relatively small number of SimpleAttributeSets).

19.1.17 The Highlighter interface

abstract interface javax.swing.text.Highlighter

This interface describes how specific regions of text can be marked up with instances of the inner Highlighter.Highlight interface. A Highlight maintains a beginning and end offset, and a reference to an instance of the inner Highlighter.HighlightPainter interface. A HighlightPainter’s only responsibility is to render the background of a specific region of text.

A text component’s UI delegate is responsible for maintaining its Highlighter. For this reason the Highlighter can change when a text component’s look and feel changes. JTextComponent provides methods for working with a text component’s Highlighter so we generally ignore the fact that such methods really get forwarded to the UI delegate.

A Highlighter is intended to maintain an array of Highlighter.Highlight instances, and we are meant to be able to add to this array using the addHighlight() method. This method takes two ints defining the range of text to highlight, as well as a Highlighter.HighlightPainter instance specifying how that Highlight should be rendered. Thus, by defining various HighlightPainters, we can add an arbitrary number of highlighted regions with distinct visual effects.

The range a Highlight encompasses is meant to be modified with the changeHighlight() method, and Highlights can be removed from a Highlighter’s array with the removeAllHighlights() or removeHighlight() methods. The paint() method is meant to manage the rendering of all a Highligher’s Highlights.

We can assign a new Highlighter with JTextComponent’s setHighlighter() method. Similarly, we can retrieve a reference to the existing one with JTextComponent’s getHighlighter() method. Each JTextComponent also maintains a selectionColor property which specifies the color to use in rendering default highlights.

19.1.18 DefaultHighlighter

class javax.swing.text.DefaultHighlighter

DefaultHighlighter extends the abstract LayeredHighlighter class. LayeredHighlighter implements the Highlighter interface and defines a paintLayeredHighlights() method, which is responsible for managing potentially multiple overlapping Highlights. LayeredHighlighter also declares an inner abstract static class called LayerPainter from which the static ! DefaultHighlighter.DefaultHighlightPainter extends. This implementation paints a solid background, behind the specified region of text, in the current text component selection color.

19.1.19 The Caret interface

abstract interface javax.swing.text.Caret

This interface describes a text component’s cursor. The paint() method is responsible for rendering the caret, and the setBlinkRate()/getBlinkRate() methods are meant to assign/retrieve a specific caret blink interval (normally in milliseconds). The setVisible() and isVisible() methods are intended to hide/show the caret and check for caret visibility, respectively.

The setDot()/getDot() methods are meant to assign/retrieve the offset of the caret within the current document. The getMark() method should return a location in the document where the caret’s mark has been assigned. The moveDot() method is intended to assign a mark position, and move the caret to a new location while highlighting the text between the dot and the mark. The setSelectionVisible()/isSelectionVisible() methods are meant to assign/query the visible state of the highlight specifying the currently selected text.

The setMagicCaretPosition()/getMagicCaretPosition() methods manage a dynamic caret position used when moving the caret up and down between lines with the arrow keys. When moving up and down between lines with an unequal number of characters, the magic position should place the caret as close to the same location within each line as possible. If the magic position is greater than the length of the current line, the caret should be placed at the end of the line. Note that this feature is common in almost all modern text applications, and is implemented for us in the DefaultCaret class.

The Caret interface also declares methods for the registration of ChangeListeners for notification of changes in the caret’s position: addChangeListener(), removeChangeListener().

19.1.20 DefaultCaret

class javax.swing.text.DefaultCaret

This class extends java.awt.Rectangle, and represents a concrete implementation of the Caret interface used by all text components by default. It is rendered as a blinking vertical line in the color specified by its associated text component’s caretColor property. DefaultCaret also implements the FocusListener, MouseListener, and MouseMotionListener interfaces.

The only MouseListener methods without empty implementations are mouseClicked() and mousePressed(). If a mouse click occurs with the left mouse button, and the click count is two (i.e. a double-click), mouseClicked() will invoke the Action returned by DefaultEditorKit.selectWordAction() to select the word containing the caret. If the click count is three, mouseClicked() will invoke the Action returned by DefaultEditorKit.selectLineAction() to select the line of text containing the caret. The mousePressed() method sends its MouseEvent parameter to DefaultCaret’s positionCaret() method, which sets the dot property to the document offset corresponding to the mouse press, and clears the magicCaretPosition property. The mousePressed() method also checks to see if the text component is enabled, and if ! it is, its requestFocus() method is invoked.

The only MouseMotionListener method without an empty implementation is mouseDragged(). This method simply passes its MouseEvent parameter to DefaultCaret’s moveCaret() method. The moveCaret() method determines the offset of the caret destination by passing the MouseEvent’s coordinates to the text component’s viewToModel() method. The moveDot() method is then invoked to actually move the caret to the determined position (recall that the moveDot() method sets the mark property and selects the text between the mark position and the new dot position).

Both FocusListener methods are nonempty. The focusGained() method checks whether the text component is editable, and if it is, the caret is made visible. The focusLost() method simply hides the caret. These methods are invoked when the text component gains or loses the focus.

We can customize the way a selection’s highlight appears by overriding DefaultCaret’s getSelectionPainter() method to return our own Highlighter.HighlightPainter implementation. We can also customize the appearance of a caret by overriding the paint() method. If we do reimplement the paint() method, however, we must also override the damage() method. The damage() method is passed a Rectangle! representing the region of the text component to repaint when the caret is moved.

For instance, the following is a simple DefaultCaret sub-class that renders a wide black caret.

class WideCaret extends DefaultCaret 
{
  protected int caretWidth = 6;

  protected void setWidth(int w) {
    caretWidth = w;
  }

  // Since DefaultCaret extends Rectangle, it inherits
  // the x, y, width, and height variables which are 
  // used here to allow proper repainting.
  protected synchronized void damage(Rectangle r) {
    if (r != null) {
      x = r.x - width;
      y = r.y;
      width = width;
      height = r.height;
      repaint();
    }
  }

  public void paint(Graphics g) {
    if(isVisible()) {
      try {
        TextUI mapper = getComponent().getUI();
        Rectangle r = mapper.modelToView(
          getComponent(), getComponent().getCaretPosition());
        g.setColor(getComponent().getCaretColor());
        g.fillRect(r.x, r.y, caretWidth, r.height - 1);
      } 
      catch (Exception e) {
        System.err.println("Problem painting cursor");
      }
    }
  }
}

19.1.21 The CaretListener interface

abstract interface javax.swing.event.CaretListener

This interface describes a listener that is notified whenever a change occurs in a text component’s caret position. It declares one method, caretUpdate(), which takes a CaretEvent as parameter. We can attach and remove CaretListeners to any JTextComponent with the addCaretListener() and removeCaretListener() methods respectively.

19.1.22 CaretEvent

class javax.swing.event.CaretEvent

This event simply encapsulates a reference to its source object (normally a text component). CaretEvents are passed to all attached CaretListeners whenever the associated text component’s caret position changes.

19.1.23 The Keymap interface

abstract interface javax.swing.text.Keymap

This interface describes a collection of bindings between KeyStrokes (see 2.13.2) and Actions (see 12.1.23). We are meant to add new KeyStroke/Action bindings to a Keymap with the addActionForKeyStroke() method. Like AttributeSets, Keymaps resolve hierarchically. Like Styles, Keymaps have a name used to reference them by.

We are meant to query the Action that corresponds to a specific KeyStroke with the getAction() method. If no corresponding Action is located in the Keymap, its resolving parents should be searched until either no more resolving parents exist, or a match is found. Similarly, we are intended to retrieve an array of KeyStrokes mapped to a given Action with the getKeyStrokesForAction() method. T! he isLocallyDefined() method is meant to check whether or not a given KeyStroke is bound to an Action in the Keymap under investigation. The removeBindings() method should remove all bindings in a Keymap, and the removeKeyStrokeBinding() method is intended to remove only those bindings corresponding to a given KeyStroke.

By default, all JTextComponents share the same Keymap instance. This is what enables the default functionality of the Backspace, Delete, and left and right arrow keys on any text component. For this reason, it is not a good idea to retrieve a text component’s Keymap and modify it directly. Rather, we are encouraged to create our own Keymap instance, and assign the default Keymap as its resolving parent. Also note that by assigning a resolving parent of null, we can effectively disable all bindings on a text component, other than tho! se in the given component’s Keymap itself (the underlying role Keymaps play in text components will become clear after we discuss DefaultEditorKit below).

We can obtain a text component’s Keymap with either of JTextComponent’s getKeymap() methods. We can assign a text component a new Keymap with the setKeymap() method, and we can add a new Keymap anywhere within the Keymap hierarchy with the addKeymap() method. We can also remove a Keymap from the hierarchy with the removeKeymap() method.

For example, to create and add a new Keymap to a JTextField and use the default text component Keymap as resolving parent, we might do something like the following:

Keymap keymap = myJTextField.getKeymap();

Keymap myKeymap = myJTextField.addKeymap("MyKeymap", keymap);

We can then add KeyStroke/Action pairs to myKeymap with the addActionForKeyStroke() method (we will see an example of this in the next section).

 

Note: Recall from section 2.13.4 that KeyListeners will receive key events before a text component’s Keymap. Although the use of Keymaps is encouraged, handling keyboard events with KeyListeners is still allowed.

19.1.24 TextAction

abstract class javax.swing.text.TextAction

EditorKits are, among other things, responsible for making a set of Actions available for performing common text editor functions based on a given content type. EditorKits normally use inner sub-classes of TextAction for this, as it extends AbstractAction (see 12.1.24), and provides a relatively powerful means of determining the target component to invoke the action on (by taking advantage of the fact that JTextComponent keeps track of the most recent text component with the focus, retrievable with its static getFocusedComponent() method). The TextAction constructor takes the String to be used as that action’s name, and passes it to its super-class constructor. When sub-classing TextAction, we normally define an actionPerformed() method, which is responsible for performing the desired action when passed an ActionEvent. Within this method, we can use TextAction’s getTextComponent() method to determine which text component the action s! hould be invoked on.

19.1.25 EditorKit

abstract class javax.swing.text.EditorKit

EditorKits are responsible for the following functionality:

Support for an appropriate Document model. An EditorKit is meant to specifically support one type of content, a String description of which should be retrievable with the getContentType() method. A corresponding Document instance should be returned by the createDefaultDocument() method, and the EditorKit should be able to read() and write() that Document to InputStreams/OutputStreams and Readers/Writers, respectively

Support for View production through a ViewFactory implementation. This behavior is actually optional, as View production will default to a text component’s UI delegate if its EditorKit’s getViewFactory() method returns null (see 19.1.28 and 19.1.29 for more about Views and the ViewFactory interface).

Support for a set of Actions that can be invoked on a text component using the appropriate Document. Normally these Actions are instances of TextAction and are defined as inner classes. An EditorKit’s Actions are meant to be retrievable in an array with its getActions() method.

19.1.26 DefaultEditorKit

class javax.swing.text.DefaultEditorKit

DefaultEditorKit extends EditorKit, and defines a series of TextAction sub-classes and corresponding name Strings (see API docs). Eight of these forty-six inner action classes are public, and can be instantiated with a default constructor: BeepAction, CopyAction, CutAction, DefaultKeyTypedAction, InsertBreakAction, InsertContentAction, InsertTabAction, and PasteAction. DefaultEditorKit maintains instances of all its inner Action classes in an array retrievable with its getActions() method. We can access any of these Actions easily by defining a Hashtable with Action.NAME keys and Action values:

  Hashtable actionTable = new Hashtable
  Action[] actions = myEditorKit.getActions();
  for (int i=0; i < actions.length; i++) {
    String actionName = (String) actions[i].getValue(Action.NAME);
    actionTable.put(actionName, actions[i]);
  }

We can then retrieve any of these Actions with DefaultEditorKit’s static String fields. For example, the following retrieves the action responsible for selecting all text in a document:

  Action selectAll = (Action) actionTable.get(DefaultEditorKit.selectAllAction);

These Actions can be used in menus and toolbars, or with other controls, for convenient control of plain text components.

DefaultEditorKit’s getViewFactory() method returns null, which means the UI delegate is responsible for creating the hierarchy of Views necessary for rendering a text component correctly. As we mentioned in the beginning of this chapter, JTextField, JPasswordField, and JTextArea all use a DefaultEditorKit.

Although EditorKits are responsible for managing a set of Actions and their corresponding names, they are not actually directly responsible for making these Actions accessible to specific text components. This is where Keymaps fit in. For instance, take a look at the following code showing how the default JTextComponent Keymap is created (from JTextComponent.java):

/**
 * This is the name of the default keymap that will be shared by all
 * JTextComponent instances unless they have had a different
 * keymap set. 
 */
public static final String DEFAULT_KEYMAP = "default";

/**
 * Default bindings for the default keymap if no other bindings
 * are given.  
 */
static final KeyBinding[] defaultBindings = {
  new KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_BACK_SPACE, 0),
    DefaultEditorKit.deletePrevCharAction),
  new KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0),
    DefaultEditorKit.deleteNextCharAction),
  new KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0),
    DefaultEditorKit.forwardAction),
  new KeyBinding(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0),
    DefaultEditorKit.backwardAction)
};

static {
  try {
    keymapTable = new Hashtable(17);
    Keymap binding = addKeymap(DEFAULT_KEYMAP, null);
    binding.setDefaultAction(new
      DefaultEditorKit.DefaultKeyTypedAction());
    EditorKit kit = new DefaultEditorKit();
    loadKeymap(binding, defaultBindings, kit.getActions());
  } catch (Throwable e) {
    e.printStackTrace();
    keymapTable = new Hashtable(17);
  }
}

19.1.27 StyledEditorKit

class javax.swing.text.StyledEditorKit

This class extends DefaultEditorKit and defines seven additional inner Action classes, each of which is publicly accessible: AlignmentAction, BoldAction, FontFamilyAction, FontSizeAction, ForegroundAction, ItalicAction, and UnderlineAction. All seven Actions are sub-classes of the inner StyledTextAction convenience class which extends TextAction.

Each of StyledEditorKit’s Actions apply to styled text documents, and they are used by JEditorPane and JTextPane. StyledEditorKit does not define its own capabilities for reading and writing styled text. Instead this functionality is inherited from DefaultEditorKit which only provides support for saving and loading plain text. The two StyledEditorKit sub-classes included with Swing, javax.swing.text.html.HTMLEditorKit and javax.swing.text.rtf.RTFEditorKit, do support styled text saving and loading for HTML and RTF content types respectively.

StyledEditorKit’s getViewFactory() method returns an instance of a private static inner class called StyledViewFactory which implements the ViewFactory interface as follows (from StyledEditorKit.java):

static class StyledViewFactory implements ViewFactory {
  public View create(Element elem) {
    String kind = elem.getName();
    if (kind != null) {
      if (kind.equals(AbstractDocument.ContentElementName)) {
        return new LabelView(elem);
      } else if (kind.equals(AbstractDocument.ParagraphElementName)) {
          return new ParagraphView(elem);
      } else if (kind.equals(AbstractDocument.SectionElementName)) {
          return new BoxView(elem, View.Y_AXIS);
      } else if (kind.equals(StyleConstants.ComponentElementName)) {
          return new ComponentView(elem);
      } else if (kind.equals(StyleConstants.IconElementName)) {
          return new IconView(elem);
      }
    }
    // default to text display
    return new LabelView(elem);
  }
}

The Views returned by this factory’s create() method are based on the name property of the Element passed as parameter. If an Element is not recognized, a LabelView is returned. In sum, because StyledEditorKit’s getViewFactory() method doesn’t return null, styled text components depend on their EditorKits rather than their UI delegates for providing Views. The opposite is true with plain text components, which rely on their UI delegate for View creation.

19.1.28 View

abstract class javax.swing.text.View

This class describes an object responsible for graphically representing a portion of a text component’s document model. The text package includes several extensions of this class meant for use by various types of Elements. We will not discuss these classes in detail, but a brief overview will be enough to provide a high level understanding of how text components are actually rendered.

abstract interface TabableView: Used by Views whose size depends on the size of tabs.

abstract interface TabExpander: This interface extends TabableView and is used by Views that support TabStops and TabSets (a set of TabStops). A TabStop describes the positioning of a tab character and the text appearing immediately after it.

class ComponentView: Used as a gateway view to a fully interactive embedded Component.

class IconView: Used as a gateway View to an embedded Icon.

class PlainView: Used for rendering one line of non-wrapped text with one font and one color.

class FieldView: Extends PlainView and adds specialized functionality for representing a single-line editor view (i.e. the ability to center text in a JTextField).

class PasswordView: Extends FieldView and adds the ability to render its content using the echo character of the associated component if it is a JPasswordField.

class LabelView: Used to render a range of styled text.

abstract class CompositeView: A View containing multiple child Views. All Views can contain child Views, but only instances of CompositeView and BasicTextUI’s RootView (discussed below) actually contain child Views by default.

class BoxView: Extends CompositeView and arranges a group of child Views in a rectangular box.

class ParagraphView: Extends BoxView and is responsible for rendering a paragraph of styled text. ParagraphView is made up of a number of child Elements organized as, or within, Views representing single rows of styled text. This View supports line wrapping, and if an Element within the content paragraph spans multiple lines, more than one View will be used to represent it.

class WrappedPlainView: Extends BoxView and is responsible for rendering multi-line, plain text with line wrapping.

All text components in Swing use UI delegates derived from BasicTextUI by default. This class defines an inner class called RootView which acts as a gateway between a text component and the actual View hierarchy used to render it.

 

Note: In chapter 22 we will take advantage of BasicTextUI’s root view while implementing a solution for printing styled text. The solution also requires us to implement a custom BoxView sub-class responsible for rendering each of its child Views to a Graphics instance used in the printing process (see 22.4).

19.1.29 The ViewFactory interface

abstract interface javax.swing.text.ViewFactory

This interface declares one method: create(Element elem). This method is responsible for returning a View, possibly containing a hierarchy of Views, used to render a given Element. BasicTextUI implements this interface, and unless a text component’s EditorKit provides its own ViewFactory, BasicTextUI’s create() method will be responsible for providing all Views. This is the case with plain text components: JTextField, JPasswordField, and JTextArea. However, styled text components, JEditorPane and JTextPane, vary greatly depending on their current content type. For this reason their Views are provided by the currently installed EditorKit. In this way, custom Views can be prov! id! ed to render different types of styled content.

19.2 Date and time editor

...by David M. Karr of Best Consulting and TCSI Corporation

The DateTimeEditor class is a panel containing a text field that allows display and editing of a date, time, or date/time value. It doesn't use direct entry of text, but uses the Up and Down arrow keys or mouse clicks on "spinner" buttons to increment and decrement field values (e.g. day, month, year, or hour, minute, second). The mouse can also be used to select particular subfields. The Left and Right arrow keys move the caret between fields.

This class is designed to be internationalized, although it assumes some conventions, such as a left-to-right reading direction. It doesn't have any locale-specific code, it just uses the locale framework integrated into Java 2. If the VM used doesn't support a particular locale, neither will this component. The Locale class encapsulates a "language", "country", and optional "variant". Each of these are strings. The possible values of "language" and "country" are defined in the ISO 639 and ISO 3166 standards, respectively. The variants are not standardized. For instance, the language codes for English, French, Chinese, and Japanese are "en", "fr", "zh", and "ja". The country codes for the USA, France, and Canada are "US", "FR", and "CA".

The current Locale setting is used to qualify the variety of resource class or properties file to obtain. For instance, a class name with a suffix of "_fr_FR" indicates resources for french in France. The suffix of "_fr_CA" indicates resources for french in Canada.

Java 2 has specific resource settings for most of the known locales, including currency formats, date formats, number formats, common text strings, etcetera. This is the information that the DateTimeEditor class uses indirectly, without having to manually encode locale-specific information.

DateTimeEditor uses several Swing classes including: JTextField, Keymap, AbstractAction, TextAction, and Caret. It also uses several non-Swing classes including: Collections, Calendar (both in package java.util), FieldPosition, and DateFormat (both i! n package java.text). The Collections class is used to sort a list of FieldPosition objects by the beginIndex of each FieldPosition. A custom Spinner class, described below, is used to allow incrementing or decrementing of values with the mouse.

DateTimeEditor's text field is an ordinary JTextField, and it uses the methods of JTextComponent to communicate with and manipulate its Caret.

The inner classes UpDownAction, BackwardAction, ForwardAction, BeginAction, and EndAction are subclasses of TextAction and AbstractAction, and are used to handle the arrow keys, and the Home and End keys. All of these inner classes are used in concert with the Keymap class to combine key definitions with action definitions.

The DateTimeEditor text field listens for caret state changes. It does this so it knows exactly which field the caret is in, and also to constrain the caret position to always be at the beginning of the current field.

The most interesting interactions are with DateFormat‘s fields and its format() method. What DateTimeEditor gains from this is the ability to know what field the caret is in, so it knows how to interpret the "increment" and "decrement" actions.

One of DateFormat‘s format() methods takes a Date value, a StringBuffer to write the stringified result into, and a single FieldPosition object. This last parameter is the key to the entire DateTimeEditor component. The format() method will update the given FieldPosition object with the begin and end offset for that field in the given date/time string. The DateTimeEditor has a hardcoded list of all fields from DateFormat. In a loop, it plugs in each of those constants into the format function, and then stores the resulting FieldPosition object. It then uses the Collections.sort() method to sort the list of FieldPositions by the beginning index of each. Using this sorted list and a given caret position, we can easily determine what field the caret resides in.

The Calendar class is used to fill in some functionality that the Date.setTime() method doesn't provide. In particular, in the code which increments or decrements the current field value, there are four DateFormat fields which cannot be set using Date.setTime(). Those fields are MONTH, WEEK_OF_MONTH, WEEK_OF_YEAR, and YEAR. For these fields, DateTimeEditor! manipulates a Calendar instance and then calls Calendar.getTime() to get a new Date value.

Figure 19.5 DateTimeEditor in the en_US locale

<<file figure19-5.gif>>

Figure 19.6 Spinner

<<file figure19-6.gif>>

The Code: DateTimeEditor.java

see \Chapter19\Karr

import java.awt.event.*;
import java.text.*;
import java.util.*;
import java.awt.*;
import javax.swing.*;
import javax.swing.border.*;
import javax.swing.text.*;
import javax.swing.event.*;

public class DateTimeEditor extends JPanel
{
   public static final long ONE_SECOND = 1000;
   public static final long ONE_MINUTE = 60*ONE_SECOND;
   public static final long ONE_HOUR = 60*ONE_MINUTE;
   public static final long ONE_DAY = 24*ONE_HOUR;
   public static final long ONE_WEEK = 7*ONE_DAY;
   public final static int TIME = 0;
   public final static int DATE = 1;
   public final static int DATETIME = 2;
   private int m_timeOrDateType;
   private int m_lengthStyle;
   private DateFormat m_format;
   private Calendar m_calendar = Calendar.getInstance();
   private ArrayList m_fieldPositions = new ArrayList();
   private Date m_lastDate = new Date();
   private Caret m_caret;
   private int m_curField = -1;
   private JTextField m_textField;
   private Spinner m_spinner;
   private AbstractAction m_upAction = 
      new UpDownAction(1, "up");
   private AbstractAction m_downAction = 
      new UpDownAction(-1, "down");

   private boolean m_settingDateText = false; // BUG FIX

   private int[] m_fieldTypes =
   {
      DateFormat.ERA_FIELD,
      DateFormat.YEAR_FIELD,
      DateFormat.MONTH_FIELD,
      DateFormat.DATE_FIELD,
      DateFormat.HOUR_OF_DAY1_FIELD,
      DateFormat.HOUR_OF_DAY0_FIELD,
      DateFormat.MINUTE_FIELD,
      DateFormat.SECOND_FIELD,
      DateFormat.MILLISECOND_FIELD,
      DateFormat.DAY_OF_WEEK_FIELD,
      DateFormat.DAY_OF_YEAR_FIELD,
      DateFormat.DAY_OF_WEEK_IN_MONTH_FIELD,
      DateFormat.WEEK_OF_YEAR_FIELD,
      DateFormat.WEEK_OF_MONTH_FIELD,
      DateFormat.AM_PM_FIELD,
      DateFormat.HOUR1_FIELD,
      DateFormat.HOUR0_FIELD
   };

   public DateTimeEditor()
   {
      m_timeOrDateType = DATETIME;
      m_lengthStyle = DateFormat.SHORT;
      init();
   }

   public DateTimeEditor(int timeOrDateType)
   {
      m_timeOrDateType = timeOrDateType;
      m_lengthStyle = DateFormat.FULL;
      init();
   }

   public DateTimeEditor(int timeOrDateType, int lengthStyle)
   {
      m_timeOrDateType = timeOrDateType;
      m_lengthStyle = lengthStyle;
      init();
   }

   private void init()
   {
      setLayout(new BorderLayout());
      m_textField = new JTextField();

      m_textField.setDocument(new DateTimeDocument()); // BUG FIX

      m_spinner = new Spinner();
      m_spinner.getIncrementButton().addActionListener(m_upAction);
      m_spinner.getDecrementButton().addActionListener(m_downAction);
      add(m_textField, "Center");
      add(m_spinner, "East");
      m_caret = m_textField.getCaret();
      m_caret.addChangeListener(new ChangeListener()
      {
        public void stateChanged(ChangeEvent evt)
        { 
          setCurField(); 
        }
      });
      setupKeymap();
      reinit();
   }

   protected class DateTimeDocument extends PlainDocument
   {
      public void insertString(int offset, 
       String str, AttributeSet a) throws BadLocationException 
      {
        if (m_settingDateText)
           super.insertString(offset, str, a);
      }
   } // BUG FIX

   public int getTimeOrDateType() { return m_timeOrDateType; }
   public void setTimeOrDateType(int timeOrDateType)
   {
      m_timeOrDateType = timeOrDateType;
      reinit();
   }

   public int getLengthStyle() { return m_lengthStyle; }
   public void setLengthStyle(int lengthStyle)
   {
      m_lengthStyle = lengthStyle;
      reinit();
   }
   
   public Date getDate() { return (m_lastDate); }
// public void setDate(Date date)
// {
//    m_lastDate = date;
//    m_calendar.setTime(m_lastDate);
//    m_textField.setText(m_format.format(m_lastDate));
//    getFieldPositions();
// }
   
   public void setDate(Date date) {
      m_lastDate = date;
      m_calendar.setTime(m_lastDate);
      m_settingDateText = true;
      m_textField.setText(m_format.format(m_lastDate));
      m_settingDateText = false;
      getFieldPositions();
   } // BUG FIX

   
   private int getFieldBeginIndex(int fieldNum)
   {
      int beginIndex = -1;
      for (Iterator iter = m_fieldPositions.iterator(); 
        iter.hasNext(); )
      {
         FieldPosition fieldPos = (FieldPosition) iter.next();
         if (fieldPos.getField() == fieldNum)
         {
            beginIndex = fieldPos.getBeginIndex();
            break;
         }
      }
      return (beginIndex);
   }
   
   private FieldPosition getFieldPosition(int fieldNum)
   {
      FieldPosition result = null;
      for (Iterator iter = m_fieldPositions.iterator(); 
        iter.hasNext(); )
      {
         FieldPosition fieldPosition = (FieldPosition) iter.next();
         if (fieldPosition.getField() == fieldNum)
         {
            result = fieldPosition;
            break;
         }
      }
      return (result);
   }
   
   private void reinit()
   {
      setupFormat();
      setDate(m_lastDate);
      m_caret.setDot(0);
      setCurField();
      repaint();
   }
   
   protected void setupFormat()
   {
      switch (m_timeOrDateType)
      {
         case TIME:
            m_format = DateFormat.getTimeInstance(m_lengthStyle);
            break;
         case DATE:
            m_format = DateFormat.getDateInstance(m_lengthStyle);
            break;
         case DATETIME:
            m_format = DateFormat.getDateTimeInstance(m_lengthStyle,
              m_lengthStyle);
            break;
      }
   }
   
   protected class UpDownAction extends AbstractAction
   {
      int m_direction; // +1 = up; -1 = down

      public UpDownAction(int direction, String name)
      {
         super(name);
         m_direction = direction;
      }
      
      public void actionPerformed(ActionEvent evt)
      {
         if (!this.isEnabled())
            return;
         boolean dateSet  = true;
         switch (m_curField)
         {
            case DateFormat.AM_PM_FIELD:
               m_lastDate.setTime(m_lastDate.getTime() +
                 (m_direction * 12*ONE_HOUR));
               break;
            case DateFormat.DATE_FIELD:
            case DateFormat.DAY_OF_WEEK_FIELD:
            case DateFormat.DAY_OF_WEEK_IN_MONTH_FIELD:
            case DateFormat.DAY_OF_YEAR_FIELD:
               m_lastDate.setTime(m_lastDate.getTime() +
                 (m_direction * ONE_DAY));
               break;
            case DateFormat.ERA_FIELD:
               dateSet = false;
               break;
            case DateFormat.HOUR0_FIELD:
            case DateFormat.HOUR1_FIELD:
            case DateFormat.HOUR_OF_DAY0_FIELD:
            case DateFormat.HOUR_OF_DAY1_FIELD:
               m_lastDate.setTime(m_lastDate.getTime() +
                 (m_direction * ONE_HOUR));
               break;
            case DateFormat.MILLISECOND_FIELD:
               m_lastDate.setTime(m_lastDate.getTime() + 
                 (m_direction * 1));
               break;
            case DateFormat.MINUTE_FIELD:
               m_lastDate.setTime(m_lastDate.getTime() +
                 (m_direction * ONE_MINUTE));
               break;
            case DateFormat.MONTH_FIELD:
               m_calendar.set(Calendar.MONTH,
                 m_calendar.get(Calendar.MONTH) + m_direction);
               m_lastDate = m_calendar.getTime();
               break;
            case DateFormat.SECOND_FIELD:
               m_lastDate.setTime(m_lastDate.getTime() +
                 (m_direction * ONE_SECOND));
               break;
            case DateFormat.WEEK_OF_MONTH_FIELD:
               m_calendar.set(Calendar.WEEK_OF_MONTH,
                 m_calendar.get(Calendar.WEEK_OF_MONTH) +
                   m_direction);
               m_lastDate = m_calendar.getTime();
               break;
            case DateFormat.WEEK_OF_YEAR_FIELD:
               m_calendar.set(Calendar.WEEK_OF_MONTH,
                 m_calendar.get(Calendar.WEEK_OF_MONTH) +
                   m_direction);
               m_lastDate = m_calendar.getTime();
               break;
            case DateFormat.YEAR_FIELD:
               m_calendar.set(Calendar.YEAR,
                 m_calendar.get(Calendar.YEAR) + m_direction);
               m_lastDate = m_calendar.getTime();
               break;
            default:
               dateSet = false;
         }

         if (dateSet)
         {
           int fieldId = m_curField;
           setDate(m_lastDate);
           FieldPosition fieldPosition = getFieldPosition(fieldId);
           m_caret.setDot(fieldPosition.getBeginIndex());

           m_textField.requestFocus();
           repaint();
         }
      }
   }

   protected class BackwardAction extends TextAction
   {
      BackwardAction(String name) { super(name); }
      
      public void actionPerformed(ActionEvent e)
      {
         JTextComponent target = getTextComponent(e);
         if (target != null)
         {
            int dot = target.getCaretPosition();
            if (dot > 0)
            {
               FieldPosition position = getPrevField(dot);
               if (position != null)
                  target.setCaretPosition(
                     position.getBeginIndex());
               else
               {
                  position = getFirstField();
                  if (position != null)
                     target.setCaretPosition(
                        position.getBeginIndex());
               }
            }
            else
               target.getToolkit().beep();
            target.getCaret().setMagicCaretPosition(null);
         }
      }
   }

   protected class ForwardAction extends TextAction
   {
      ForwardAction(String name) { super(name); }

      public void actionPerformed(ActionEvent e)
      {
         JTextComponent target = getTextComponent(e);
         if (target != null)
         {
            FieldPosition position = getNextField(
               target.getCaretPosition());
            if (position != null)
               target.setCaretPosition(position.getBeginIndex());
            else
            {
               position = getLastField();
               if (position != null)
                  target.setCaretPosition(
                     position.getBeginIndex());
            }
            target.getCaret().setMagicCaretPosition(null);
         }
      }
   }
   
   protected class BeginAction extends TextAction
   {
      BeginAction(String name) { super(name); }

      public void actionPerformed(ActionEvent e)
      {
         JTextComponent target = getTextComponent(e);
         if (target != null)
         {
            FieldPosition position = getFirstField();
            if (position != null)
               target.setCaretPosition(position.getBeginIndex());
         }
      }
   }

   protected class EndAction extends TextAction
   {
      EndAction(String name) { super(name); }

      public void actionPerformed(ActionEvent e)
      {
         JTextComponent target = getTextComponent(e);
         if (target != null)
         {
            FieldPosition position = getLastField();
            if (position != null)
               target.setCaretPosition(position.getBeginIndex());
         }
      }
   }
   
   protected void setupKeymap()
   {
      Keymap keymap = m_textField.addKeymap("DateTimeKeymap", null);
      keymap.addActionForKeyStroke(KeyStroke.getKeyStroke(
         KeyEvent.VK_UP, 0), m_upAction);
      keymap.addActionForKeyStroke(KeyStroke.getKeyStroke(
         KeyEvent.VK_DOWN, 0), m_downAction);
      keymap.addActionForKeyStroke(KeyStroke.getKeyStroke(
         KeyEvent.VK_LEFT, 0), new BackwardAction(DefaultEditorKit.
            backwardAction));
      keymap.addActionForKeyStroke(KeyStroke.getKeyStroke(
         KeyEvent.VK_RIGHT, 0), new ForwardAction(DefaultEditorKit.
            forwardAction));
      keymap.addActionForKeyStroke(KeyStroke.getKeyStroke(
         KeyEvent.VK_HOME, 0), new BeginAction(DefaultEditorKit.
            beginAction));
      keymap.addActionForKeyStroke(KeyStroke.getKeyStroke(
         KeyEvent.VK_END, 0), new EndAction(DefaultEditorKit.
            endAction));
      m_textField.setKeymap(keymap);
   }
   
   private void getFieldPositions()
   {
      m_fieldPositions.clear();
      for (int ctr = 0; ctr < m_fieldTypes.length; ++ ctr)
      {
         int fieldId = m_fieldTypes[ctr];
         FieldPosition fieldPosition = new FieldPosition(fieldId);
         StringBuffer formattedField = new StringBuffer();
         m_format.format(m_lastDate, formattedField, fieldPosition);
         if (fieldPosition.getEndIndex() > 0)
            m_fieldPositions.add(fieldPosition);
      }
      m_fieldPositions.trimToSize();
      Collections.sort(m_fieldPositions,
         new Comparator()
         {
            public int compare(Object o1, Object o2)
            {
               return (((FieldPosition) o1).getBeginIndex() -
                  ((FieldPosition) o2).getBeginIndex());
            }
         }
      );
   }

   private FieldPosition getField(int caretLoc)
   {
      FieldPosition fieldPosition = null;
      for (Iterator iter = m_fieldPositions.iterator(); 
         iter.hasNext(); )
      {
         FieldPosition chkFieldPosition = 
            (FieldPosition) iter.next();
         if ((chkFieldPosition.getBeginIndex() <= caretLoc) &&
             (chkFieldPosition.getEndIndex() > caretLoc))
         {
            fieldPosition = chkFieldPosition;
            break;
         }
      }
      return (fieldPosition);
   }

   private FieldPosition getPrevField(int caretLoc)
   {
      FieldPosition fieldPosition = null;
      for (int ctr = m_fieldPositions.size() - 1; ctr > -1; -- ctr)
      {
         FieldPosition chkFieldPosition =
            (FieldPosition) m_fieldPositions.get(ctr);
         if (chkFieldPosition.getEndIndex() <= caretLoc)
         {
            fieldPosition = chkFieldPosition;
            break;
         }
      }
      return (fieldPosition);
   }

   private FieldPosition getNextField(int caretLoc)
   {
      FieldPosition  fieldPosition = null;
      for (Iterator iter = m_fieldPositions.iterator(); 
         iter.hasNext(); )
      {
         FieldPosition chkFieldPosition = 
            (FieldPosition) iter.next();
         if (chkFieldPosition.getBeginIndex() > caretLoc)
         {
            fieldPosition = chkFieldPosition;
            break;
         }
      }
      return (fieldPosition);
   }

   private FieldPosition getFirstField()
   {
      FieldPosition result = null;
      try { result = ((FieldPosition) m_fieldPositions.get(0)); }
      catch (NoSuchElementException ex) {}
      return (result);
   }

   private FieldPosition getLastField()
   {
      FieldPosition result = null;
      try
      {
         result =
            ((FieldPosition) m_fieldPositions.get(
               m_fieldPositions.size() - 1));
      }
      catch (NoSuchElementException ex) {}
      return (result);
   }
   
   private void setCurField()
   {
      FieldPosition fieldPosition = getField(m_caret.getDot());
      if (fieldPosition != null)
      {
         if (m_caret.getDot() != fieldPosition.getBeginIndex())
            m_caret.setDot(fieldPosition.getBeginIndex());
      }
      else
      {
         fieldPosition = getPrevField(m_caret.getDot());
         if (fieldPosition != null)
            m_caret.setDot(fieldPosition.getBeginIndex());
         else
         {
            fieldPosition = getFirstField();
            if (fieldPosition != null)
               m_caret.setDot(fieldPosition.getBeginIndex());
         }
      }
      
      if (fieldPosition != null)
         m_curField = fieldPosition.getField();
      else
         m_curField = -1;
   }

   public void setEnabled(boolean enable)
   {
      m_textField.setEnabled(enable);
      m_spinner.setEnabled(enable);
   }

   public boolean isEnabled()
   { return (m_textField.isEnabled() && m_spinner.isEnabled()); }
  
   public static void main (String[] args)
   {
      JFrame frame = new JFrame();
      frame.addWindowListener(new WindowAdapter()
      {
         public void windowClosing(WindowEvent evt)
         { System.exit(0); }
      });

      JPanel panel = new JPanel(new BorderLayout());
      panel.setBorder(new EmptyBorder(5, 5, 5, 5));
      frame.setContentPane(panel);
      final DateTimeEditor field =
         new DateTimeEditor(DateTimeEditor.DATETIME, 
            DateFormat.FULL);
      panel.add(field, "North");

      JPanel buttonBox = new JPanel(new GridLayout(2, 2));
      JButton showDateButton = new JButton("Show Date");
      buttonBox.add(showDateButton);

      final JComboBox timeDateChoice = new JComboBox();
      timeDateChoice.addItem("Time");
      timeDateChoice.addItem("Date");
      timeDateChoice.addItem("Date/Time");
      timeDateChoice.setSelectedIndex(2);
      timeDateChoice.addActionListener(new ActionListener()
      {
         public void actionPerformed(ActionEvent evt)
         {
            field.setTimeOrDateType(timeDateChoice.
               getSelectedIndex());
         }
      });
      buttonBox.add(timeDateChoice);

      JButton toggleButton = new JButton("Toggle Enable");
      buttonBox.add(toggleButton);
      showDateButton.addActionListener(new ActionListener()
      { 
         public void actionPerformed(ActionEvent evt)
         { System.out.println(field.getDate()); } 
      });
      toggleButton.addActionListener(new ActionListener()
      { 
         public void actionPerformed(ActionEvent evt)
         { field.setEnabled(!field.isEnabled());} 
      });
      panel.add(buttonBox, "South");

      final JComboBox lengthStyleChoice = new JComboBox();
      lengthStyleChoice.addItem("Full");
      lengthStyleChoice.addItem("Long");
      lengthStyleChoice.addItem("Medium");
      lengthStyleChoice.addItem("Short");
      lengthStyleChoice.addActionListener(new ActionListener()
      {
         public void actionPerformed(ActionEvent evt)
         {
            field.setLengthStyle(lengthStyleChoice.
               getSelectedIndex());
         }
      });
      buttonBox.add(lengthStyleChoice);

      frame.pack();
      Dimension dim = frame.getToolkit().getScreenSize();
      frame.setLocation(dim.width/2 - frame.getWidth()/2,
         dim.height/2 - frame.getHeight()/2);
      frame.show();
   }
}

The Code: Spinner.java

see \Chapter19\Karr

import java.util.*;
import java.lang.reflect.*;
import java.awt.*;
import javax.swing.*;
import javax.swing.plaf.*;
import javax.swing.plaf.basic.*;

public class Spinner extends JPanel
{
   private int m_orientation = SwingConstants.VERTICAL;
   private BasicArrowButton m_incrementButton;
   private BasicArrowButton m_decrementButton;

   public Spinner() { createComponents(); }

   public Spinner(int orientation)
   {
      m_orientation = orientation;
      createComponents();
   }

   public void setEnabled(boolean enable)
   {
      m_incrementButton.setEnabled(enable);
      m_decrementButton.setEnabled(enable);
   }

   public boolean isEnabled()
   {
      return (m_incrementButton.isEnabled() &&
         m_decrementButton.isEnabled());
   }
   
   protected void createComponents()
   {
      if (m_orientation == SwingConstants.VERTICAL)
      {
         setLayout(new GridLayout(2, 1));
         m_incrementButton = new BasicArrowButton(
            SwingConstants.NORTH);
         m_decrementButton = new BasicArrowButton(
            SwingConstants.SOUTH);
         add(m_incrementButton);
         add(m_decrementButton);
      }
      else if (m_orientation == SwingConstants.HORIZONTAL)
      {
         setLayout(new GridLayout(1, 2));
         m_incrementButton = new BasicArrowButton(
            SwingConstants.EAST);
         m_decrementButton = new BasicArrowButton(
            SwingConstants.WEST);
         add(m_decrementButton);
         add(m_incrementButton);
      }
   }

   public JButton getIncrementButton() { 
      return (m_incrementButton); }
   public JButton getDecrementButton() { 
      return (m_decrementButton); }

   public static void main(String[] args)
   {
      JFrame frame = new JFrame();
      JPanel panel = (JPanel) frame.getContentPane();
      panel.setLayout(new BorderLayout());
      JTextField  field = new JTextField(20);
      Spinner spinner = new Spinner();

      panel.add(field, "Center");
      panel.add(spinner, "East");

      Dimension dim = frame.getToolkit().getScreenSize();
      frame.setLocation(dim.width/2 - frame.getWidth()/2,
         dim.height/2 - frame.getHeight()/2);
      frame.pack();
      frame.show();
   }
}

Understanding the Code:

 

Class DateTimeEditor

The m_fieldTypes array contains all of the field alignment constants defined in the DateFormat class. These are all of the pieces of a time or date value that we should expect to see. The order in this list is not important. Each value is plugged into DateFormat.format() to determine where each field is in the stringified date/time value.

The default constructor makes the field display date and time, in a SHORT format, which the DateFormat class describes as "completely numeric", such as "12.13.52" or "3:30pm". The second constructor can specify whether the field will display time, date, or date and time. In addition, it sets it into the FULL format, which the DateFormat class describes as "pretty completely specified", such as "Tuesday, April 12, 1952 AD" or "3:30:42pm PST". The third constructor can specify the time/date type, and the length style, being SHORT, MEDIUM, LONG, or FULL (fields in DateFormat).

Each of the constructors calls a common init() method, which initializes the caret, registers a ChangeListener on the caret (to update which field the caret is in), sets up the keymap (up, down, left, and right arrow keys), and calls the reinit() method which does some additional initialization (this method can be called any time, not just during initial construction).

The setupKeymap() method defines the keymap for the Up, Down, Left, and Right arrow keys. It first adds a new keymap with a null parent, so that no other keymaps will be used. It associates Actions with the key strokes we want to allow. Then the setKeymap() method is called to assign this keymap to our text field.

Each time a new date is set, either at initialization or by changing one of the field values, the getFieldPositions() method is called. This method uses the DateFormat.format() method, plugging in the Date value, and each one of the DateFormat fields. A new FieldPosition object is set which specifies the beginning and end indices for each field of the given date. All of the resulting FieldPosition objects are stored into the m_fieldPositions list, and sorted usi! ng the beginning index (using the Collections class). It is sorted in this fashion to make it easy to determine the field associated with a particular caret location. The BackwardAction and ForwardAction classes (see below) use this sorted list to quickly move to the previous or next date/time value.

After the m_fieldPositions list is set, several methods search that list, either directly or indirectly, to move to a particular field, or find out what the current field is. The getField(), getPrevField(), and getNextField() methods all take a caret location and return the current, previous, or next field, respectively. The getFirstField() and getLastField() methods return the first and last fields, respectively. And finally, the setCurField() method gets the f! ield the caret is in and adjusts the caret to lie at the beginning of the field. This method is used when a new date is set, or the user uses the mouse to set the caret location.

The setEnabled() and isEnabled() methods allow the component to be disabled or enabled, and to check on the enabled status of the component (which includes both the text field and the custom spinner).

The main() method of this class is used as a demonstration of its capabilities. It presents a DateTimeEditor, a "Show Date" button, and a "Toggle Enable" button. When the "Show Date" button is pressed, it prints the current date value shown in the field to standard output. (The string printed is always in the "english US" locale, irrespective of the current locale being used to display the DateTimeEditor.) When the "Toggle Enable" button is pressed, it will toggle the enabled status of the component which grays out the text field and the spinner buttons when disabled.

As of the first official Java 2 public release there is a bug in the area of distribution of key events. In the method setupKeymap(), we specifically limit the keymap so that only six keystrokes should be recognized in the component, the four arrow keys and the Home and End keys. However, as a result of this bug, some platforms will allow normal characters to be inserted into the field, violating the integrity of the Date value.

To work around this, a small amount of code can be added to this example to avoid the problem. The solution requires two pieces:

1. In the setDate() method, which is the only place where the text of the field should be modified, we toggle a flag just before and after setting the text, indicating that we are trying to set the text of the field.

2. We create a new class, DateTimeDocument, extending PlainDocument, and send an instance of this class to the setDocument method of JTextField. The insertString() method of DateTimeDocument only calls super.insertString() if the flag (from item 1) is true.

The exact changes are the following:

1. Add the declaration of m_settingDateText to the variables section:

private boolean m_settingDateText = false;

2. Change the setDate method to the following:

    public void setDate(Date date) {
      m_lastDate = date;
      m_calendar.setTime(m_lastDate);
      m_settingDateText = true;
      m_textField.setText(m_format.format(m_lastDate));
      m_settingDateText = false;
      getFieldPositions();
    }

3. In the init method, send an instance of DateTimeDocument to the setDocument method of the JTextField instance to set the Document:

m_textField.setDocument(new DateTimeDocument());

3. Add the DateTimeDocument class:

    protected class DateTimeDocument extends PlainDocument 
    {
      public void insertString(int offset, String str, AttributeSet a) throws BadLocationException
      {
        if (m_settingDateText)
          super.insertString(offset, str, a);
      }
    }

Class DateTimeEditor.UpDownAction

The UpDownAction class is used as the action for the "up" and "down" arrow keys. When executed, this will increment or decrement the value of the field the caret is in. When values "roll over" (or "roll
down"), like incrementing the day from "31" to "1", then this will change other fields, like the month field, in this example. One instance of this class is used to move in the "up" direction, and one instance is used to move in the "down" direction. for each field, it calculates the new time or date value, and uses
Date.setTime() or Calendar.set() to set the new date or time. It will check for all of the field types specified in the DateFormat class (also listed in the m_fieldTypes array), although several would never be seen in certain locales. If the component is presently disabled, no modifications will be performed on the data.

Class DateTimeEditor.BackwardAction

The BackwardAction class is used as the action for the left arrow key. When executed, it will move the text caret from the beginning of one field to the beginning of the previous field. It uses the getPrevField() method to get the field previous to the current one.

Class DateTimeEditor.ForwardAction

The ForwardAction class is used as the action for the right arrow key. When executed, it will move the text caret from the beginning of the current field to the beginning of the next field. It uses the getNextField() method to get the field following the current one.

Class DateTimeEditor.BeginAction & DateTimeEditor.EndAction

The BeginAction and EndAction classes move the text caret to the beginning of the first and last fields, respectively.

Class Spinner

The Spinner class just uses two BasicArrowButtons, in either a vertical or horizontal orientation. It provides an API to get the increment or decrement buttons so you can attach listeners to them.

Running the Code

DateTimeEditor can be compiled and executed as is. By default, it will present a date/time value in the current locale. You can experiment with this by setting the "LANG" environment variable to a legal locale string. It's possible that not all legal locale strings will show any difference in the presentation, or even be correctly recognized. I found only major locales like "es" (spanish), "fr" (french), and "it" (italian) would work.

When you push the "Show Date" button, it will print the english value of the Date to standard output. When you push the "Toggle Enable" button, it will toggle the enabled state of the text field. When it is disabled, the text is slightly grayed out, the up and down arrow keys do nothing, and the spinner buttons are insensitive. Figure 19.5 shows DateTimeEditor in action.

In addition, the Spinner class can be compiled and run as a standalone demonstration. When run, it will present an empty text field with the spinner buttons to the right of it. As presented, it doesn't do much, not showing any behavioral connection between the component (the text field) and the Spinner, but this does show what the Spinner looks like when connected to a component. Figure 19.6 shows what the Spinner class looks like when run.