I l@ve RuBoard |
![]() ![]() |
22.3 The Document ModelThe Document is the M part of the MVC (Model-View-Controller) architecture for all of Swing's text components. It is responsible for the text content of the component as well as relevant style information for text components that support styled text. The Document model must be simple enough to be used by JTextField, but powerful and flexible enough to be used by JTextPane and JEditorPane. Swing accomplishes this by providing the classes and interfaces shown in Figure 22-7. Figure 22-7. High-level Document class diagram![]() Basically, a Document partitions its content into small pieces called Elements. Each Element is small enough that its style information can be represented by a single AttributeSet. The Elements are organized into a tree structure[22] with a single root.
Swing provides the Document interface, which doesn't support styled text, and the StyledDocument interface, which does. But note that there is no StyledElement interface. Swing provides a single Element interface that does support style. The simpler document types (such as PlainDocument, which JTextField and JTextArea use by default) use Elements but don't assign any style information to them. 22.3.1 The Element InterfaceThe Element interface is used to describe a portion of a document. But note that an Element does not actually contain a portion of the document; it just defines a way of structuring a portion. It maintains a start offset and end offset into the actual text content, which is stored elsewhere by the Document. Each Element may have style information, stored in an AttributeSet, that applies to the entire Element. Figure 22-8 gives an example of how a Document that supports styled text must change a single Element representing a phrase in italics into a subtree of Elements when a word in the middle is changed from italic to bold. Figure 22-8. Sample Element structure![]()
22.3.1.1 PropertiesThe Element interface defines the properties shown in Table 22-19. The attributes property is the AttributeSet containing the style information that applies to this element. The document property provides access to the Document this Element describes.
The elementCount property specifies the number of children (possibly zero) the Element has, and element is an indexed property for accessing those children. The children are always kept in order, so you are assured that getElement(0) affects offsets in the document that appear before those affected by getElement(1). If this Element has no children, the leaf property is true. The parentElement property is the Element that contains this Element, or null if this Element is a root. The name property is a brief name for the Element and is often one of the values listed in Table 22-23. The startOffset and endOffset properties are offsets into the document text that specify the portion of the document the Element covers. Expect these values to change as text is inserted in or deleted from the document. These offsets are relative to the beginning of the document, not relative to the parentElement's startOffset. (Note that endOffset actually points to the position after the last character of the Element, as depicted in Figure 22-8.) 22.3.1.2 Element lookup method
22.3.2 The Document InterfaceThe Document interface is the foundation of the document model used by all Swing text components. It defines methods for manipulating the text content, methods for registering event listeners, and methods for accessing the Element tree. Actually, Document supports the existence of multiple Element trees, though typically there is only one. The idea is to support multiple ways of structuring the same document content. For example, a play could have one Element tree organized by act/scene/line and another organized by page/paragraph/sentence. Both trees cover the entire text of the play, just in different ways. (It would be a mistake to have one tree cover Act I and another tree cover Act II. If that's what you want, use two separate Documents.) 22.3.2.1 PropertiesTable 22-20 shows the properties defined by the Document interface. They are fairly straightforward. The defaultRootElement property is the root of this Document's Element tree, or if this Document has more than one tree, the root of the "default" tree. The rootElements property is an array of all the tree roots, including defaultRootElement.
The length property is the total number of "characters" in the Document. This contains all the actual characters in the document, including tabs and newlines[23] embedded Components and Icons (which count as one "character" each).
The startPosition and endPosition properties are offsets to the beginning and end (actually, one position past the last character) of the current document text. The type of these properties is Position, which keeps track of a particular location in a Document as text is inserted or deleted over time. We cover Position later in this chapter. 22.3.2.2 EventsImplementations of the Document interface fire DocumentEvents to indicate changes to the Document's contents. Document types that support undo also fire UndoableEditEvents. UndoableEditEvent and UndoableEditListener are covered in Chapter 18. DocumentEvent and DocumentListener are discussed later in this chapter. Document defines the following standard methods for managing event listeners:
22.3.2.3 ConstantsDocument defines the two constants shown in Table 22-21, which are intended to be used as keys when calling the getProperty( ) and putProperty( ) methods.
22.3.2.4 Text manipulation methodsThese methods manipulate the contents of the Document. They all throw a BadLocationException if you attempt to reference a document offset that does not exist (for example, if you try to insert text at offset 200 of a 100-character Document). BadLocationException (defined later in this chapter) is used throughout the text package to signal this condition.
22.3.2.5 Other methods
22.3.3 The AbstractDocument ClassMuch of the implementation of the Document interface is provided by the AbstractDocument class. One significant feature provided by this default implementation is a basic locking mechanism. Unlike most methods in Swing, certain methods in the classes that make up the document model are thread-safe. AbstractDocument is specifically designed so that it may be used by multiple threads. It also defines several important inner classes/interfaces:
22.3.3.1 PropertiesAbstractDocument defines the properties shown in Table 22-22. The important ones are defined by the Document interface. The documentProperties property (a java.util.Dictionary) allows direct access to the storage mechanism used in support of the getProperty( ) and putProperty( ) methods defined by Document. documentListeners and undoableEditListeners provide access to any registered listeners on the Document (as does the getListeners( ) method).
The documentFilter property (if not null) is an object that oversees and can influence any insertions, replacements, or deletions within the documentFilter's text content. We cover the DocumentFilter class later in this chapter. In addition to the expected defaultRootElement, AbstractDocument defines a bidiRootElement property, which is useful only if you're interested in the bidirectional level (as defined by the Unicode bidi algorithm) in mixed left-to-right and right-to-left text. The asynchronousLoadPriority property is not used outside of HTMLEditorKit. A value less than zero indicates that the Document should not be loaded asynchronously. 22.3.3.2 EventsAbstractDocument fires DocumentEvents and UndoableEditEvents when changes are made to the document. It implements the following standard methods for managing event listeners:
22.3.3.3 ConstantsAbstractDocument defines an attribute key constant and four attribute value constants for that key. These public constants are shown in Table 22-23.
22.3.3.4 ConstructorsSince AbstractDocument is an abstract class, its constructors are called only by subclasses. The constructors require arguments that are implementations of inner interfaces defined by AbstractDocument. Fortunately, Swing provides implementations for us.
22.3.3.5 Locking methodsAbstractDocument implements a basic locking mechanism that ensures that, at any given time, there is either a single writer of the document or zero or more readers. That is, if no one is writing to the document, anyone is allowed to read it or write to it. Once someone begins writing, no one is able to read until the writer has finished. Certain methods that technically "read" the document (such as getText( )) do not actually obtain a read lock to do so. The only method that obtains a read lock is the render( ) method, meaning that you are not guaranteed document stability when other access methods are implemented. This locking scheme is supported by the following methods. If you decide to use the existing document types, you don't have to understand all the details; the locks are exploited automatically. But if you decide to implement your own document type, it is important to understand how this works. Any code that modifies the text content, the Element tree, or the Document properties should be framed like this: try { writeLock( ); // Code that messes with the Document goes here. } finally { writeUnlock( ); }
22.3.3.6 Text manipulation methodsThese methods read and write the underlying Document content. The methods that modify the content must obtain a write lock before proceeding.
22.3.3.7 Other public methods
22.3.4 The PlainDocument ClassPlainDocument is a concrete subclass of AbstractDocument used for simple documents that do not need to manage complex formatting styles. The JTextField and JTextArea classes use PlainDocument as their default model. It's worth noting that PlainDocument provides more power than these components typically need. As a subclass of AbstractDocument, it supports AttributeSets, allowing the document to contain different fonts, colors, font styles, etc. These attributes are ignored when rendering the simple text components that use this document type. The Elements that make up a PlainDocument correspond to distinct lines of text that end with a newline (\n). Each line of text maps to a single LeafElement. All of these LeafElements are contained by a single BranchElement (the Document's root Element). PlainDocument always keeps its Element tree structured as two levels like this, but subclasses are free to implement other schemes. 22.3.4.1 PropertiesPlainDocument does not define any properties beyond those of AbstractDocument (see Table 22-22), though of course it provides implementations for abstract methods, such as getDefaultRootElement( ). PlainDocument does support setting its tab stop size, but clients must do this using putProperty(TabSizeAttribute, new Integer(size)) rather than with a set method. The default tab stop size is 8. 22.3.4.2 ConstantsPlainDocument defines the constants shown in Table 22-24.
The attribute values paired with these keys should be of type Integer. Swing never uses lineLimitAttribute, but PlainView does respect tabSizeAttribute when it draws text with tabs. 22.3.4.3 Constructors
22.3.4.4 Public methodsThe only new methods defined in this class are the following:
22.3.4.5 Restriction exampleFigure 19-1 shows a JFormattedTextField that restricts the length of its content. It is possible to achieve the same effect by using a custom Document model. Before the advent of JFormattedTextField and DocumentFilter in SDK 1.4, this was the only good way to restrict a field's content. In any case, it demonstrates how easy it can be to subclass PlainDocument. Here's the code for our custom Document model that restricts the length of its content. One complication is that the replace( ) method was added to AbstractDocument in SDK 1.4. We need to override replace( ), or else the user could exceed our length limit by selecting all and pasting in a bunch of text. But since our implementation calls super.replace( ), it won't even compile on versions prior to 1.4. There are ways around this (e.g., reflection), but it's not worth it for this simple example. With Version 1.4 and later, it's easier to use a JFormattedTextField anyway. // MaxLengthDocument.java // import javax.swing.*; import javax.swing.text.*; // An extension of PlainDocument that restricts the length of its content public class MaxLengthDocument extends PlainDocument { private int max; // Create a Document with a specified max length. public MaxLengthDocument(int maxLength) { max = maxLength; } // Don't allow an insertion to exceed the max length. public void insertString(int offset, String str, AttributeSet a) throws BadLocationException { if (getLength( ) + str.length( ) > max) java.awt.Toolkit.getDefaultToolkit( ).beep( ); else super.insertString(offset, str, a); } // We'd need to override replace( ) as well if running under SDK 1.4. // A sample main( ) method that demonstrates using MaxLengthDocument with a // JTextField. Note that new JFormattedTextField(new MaskFormatter("*****")) would // be easier. public static void main(String[] args) { Document doc = new MaxLengthDocument(5); // Set maximum length to 5. JTextField field = new JTextField(doc, "", 8); JPanel flowPanel = new JPanel( ); flowPanel.add(field); JFrame frame = new JFrame("MaxLengthDocument demo"); frame.setContentPane(flowPanel); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setSize(160, 80); frame.setVisible(true); } } 22.3.5 The StyledDocument InterfaceStyledDocument is an extension of the Document interface that can associate different styles with different parts of its text content. StyledDocument has methods that can assign new style attributes to blocks of text. It also supports named Styles. This interface defines the notions of "character attributes," "paragraph attributes," and "logical styles." This isn't so much a distinction of the attribute itself as it is a description of where it applies. For example, a green foreground color could be set as a character attribute, a paragraph attribute, or an attribute of a logical style. (Some attributes, such as indentation, affect only the paragraph level.) Character attributes apply to LeafElements; paragraph attributes apply to BranchElements. Logical styles also apply to BranchElements but can be overridden by local attributes. These local overrides can be either paragraph or character attributes. The implementing class decides exactly how it structures its Element tree, and therefore what "paragraph" means. 22.3.5.1 PropertiesStyledDocument does not define any properties beyond those it inherits from Document (see Table 22-20). Note that although logicalStyle, characterElement, and paragraphElement appear (by mechanical interpretation of the JavaBeans specification) to be indexed properties of StyledDocument, the "index" associated with them is a character offset into the document, not a simple array index. We omit these "properties" here and discuss the methods and those related to them in the descriptions that follow. 22.3.5.2 Style application methodsThe superinterface's insertString( ) method takes an AttributeSet, but if you want to change the style attributes of text already in the Document, you need to use the methods defined here:
22.3.5.3 Query methodsThese methods are used to retrieve style information at a particular document offset:
22.3.5.4 StyleContext delegation methodsThe StyledDocument interface defines several methods that exactly match methods provided by the StyleContext class. Though implementing classes could theoretically do something different, in practice they just delegate to an instance of StyleContext. (See Section 22.2.6 earlier in this chapter for a description of these methods.)
22.3.6 The DefaultStyledDocument ClassDefaultStyledDocument is the implementation of StyledDocument that JTextPane uses by default. It inherits much of its functionality from AbstractDocument but adds methods for style handling. DefaultStyledDocument structures its Element tree as shown in Figure 22-9. This, together with Figure 22-8, gives a good idea of how DefaultStyledDocument works. Figure 22-9. DefaultStyledDocument element tree structure![]() This default root has a child for each paragraph (delimited by the newline character, \n) in the Document. Each of these is a BranchElement with one or more LeafElement children. The number of LeafElements depends on the character attributes of the paragraph. Within a paragraph, any block of text with differing character attributes requires its own LeafElement. If the entire paragraph has the same set of character attributes, the corresponding BranchElement has only a single child. Character attributes are stored in the AttributeSets attached to the LeafElements. Paragraph attributes are stored in the AttributeSets attached to the BranchElements (excluding the root). Logical Styles are stored as the resolving parents of the AttributeSets that hold the paragraph attributes (recall that the Style interface extends AttributeSet). The AttributeSets that hold the character attributes and the logical Style attributes may have resolving parents, but the AttributeSets that hold the paragraph attributes may not (unless there is no logical Style assigned to that paragraph). Figure 22-9 doesn't show a resolveParent link between the AttributeSet for character attributes and the AttributeSet for paragraph attributes, and in practice there doesn't need to be one. All the Element objects that DefaultStyledDocument creates are subclasses of AbstractDocument.AbstractElement—which knows how to climb the Element tree when looking for attribute values. Keep this in mind if you plan to subclass DefaultStyledDocument to create your own Document types. DefaultStyledDocument sometimes refers to the root as the "section" Element, the middle tier as the "paragraph" Elements, and the leaf tier as the "content" Elements. DefaultStyledDocument always keeps its Element tree structured in three levels like this, but subclasses are free to implement other schemes. 22.3.6.1 PropertiesDefaultStyledDocument defines the properties shown in Table 22-25. The default-RootElement is the same as the one described earlier. The styleNames property is an Enumeration with the names of all the named Styles available to this Document.
22.3.6.2 EventsDefaultStyledDocument fires DocumentEvents and UndoableEditEvents when changes are made to the document, just as its superclass does. 22.3.6.3 ConstantDefaultStyledDocument defines the constant shown in Table 22-26.
22.3.6.4 Constructors
22.3.6.5 Content style methodsThe insertString( ), replace( ), remove( ), and getText( ) methods inherited from AbstractDocument query and alter the document's text content. The following methods query and alter the style attributes assigned to the text content:
22.3.6.6 StyleContext delegation methodsThe DefaultStyledDocument has several methods that delegate to the StyleContext established by its constructors. (See Section 22.2.6 for a full description of these methods.)
22.3.6.7 StyledDocument exampleHere's a quick example that manipulates a pane's StyledDocument to draw parentheses, brackets, and braces in different colors. It sets each half of a matching pair to the same color and sets mismatches to red. The interesting work happens in the run( ) method. To keep things simple, it doesn't try to ignore quoted parentheses or those in comments. // ParenMatcher.java // import javax.swing.*; import javax.swing.text.*; import java.awt.event.*; import java.awt.Color; import java.awt.BorderLayout; // A simple paren matcher public class ParenMatcher extends JTextPane implements Runnable { public static Color[] matchColor = { Color.blue, Color.magenta, Color.green }; public static Color badColor = Color.red; private AttributeSet[] matchAttrSet; private AttributeSet badAttrSet; public ParenMatcher( ) { // Create an array of AttributeSets from the array of Colors. StyleContext sc = StyleContext.getDefaultStyleContext( ); badAttrSet = sc.addAttribute(SimpleAttributeSet.EMPTY, StyleConstants.Foreground, badColor); matchAttrSet = new AttributeSet[matchColor.length]; for (int j=0; j < matchColor.length; j+=1) matchAttrSet[j] = sc.addAttribute(SimpleAttributeSet.EMPTY, StyleConstants.Foreground, matchColor[j]); } // Match and color the parens/brackets/braces. public void run( ) { StyledDocument doc = getStyledDocument( ); String text = ""; int len = doc.getLength( ); try { text = doc.getText(0, len); } catch (BadLocationException ble) { } java.util.Stack stack = new java.util.Stack( ); for (int j=0; j < text.length( ); j+=1) { char ch = text.charAt(j); if (ch == '(' || ch == '[' || ch == '{') { int depth = stack.size( ); stack.push(""+ch+j); // Push a String containing the char and the offset. AttributeSet aset = matchAttrSet[depth % matchAttrSet.length]; doc.setCharacterAttributes(j, 1, aset, false); } if (ch == ')' || ch == ']' || ch == '}') { String peek = stack.empty( ) ? "." : (String)stack.peek( ); if (matches(peek.charAt(0), ch)) { // Does it match? stack.pop( ); int depth = stack.size( ); AttributeSet aset = matchAttrSet[depth % matchAttrSet.length]; doc.setCharacterAttributes(j, 1, aset, false); } else { // Mismatch doc.setCharacterAttributes(j, 1, badAttrSet, false); } } } while (! stack.empty( )) { // Anything left in the stack is a mismatch. String pop = (String)stack.pop( ); int offset = Integer.parseInt(pop.substring(1)); doc.setCharacterAttributes(offset, 1, badAttrSet, false); } } // Unset the foreground color (if any) whenever the user enters text // (if not for this, text entered after a paren would catch the paren's color). public void replaceSelection(String content) { getInputAttributes( ).removeAttribute(StyleConstants.Foreground); super.replaceSelection(content); } // Return true if 'left' and 'right' are matching parens/brackets/braces. public static boolean matches(char left, char right) { if (left == '(') return (right == ')'); if (left == '[') return (right == ']'); if (left == '{') return (right == '}'); return false; } public static void main(String[] args) { JFrame frame = new JFrame("ParenMatcher"); final ParenMatcher matcher = new ParenMatcher( ); matcher.setText("int fact(int n) {\n" +" if (n <= 1) return 1;\n" +" return(n * fact(n-1));\n" +"}\n"); frame.getContentPane( ).add(new JScrollPane(matcher), BorderLayout.CENTER); JButton matchButton = new JButton("match parens"); matchButton.addActionListener(new ActionListener( ) { public void actionPerformed(ActionEvent ae) { matcher.run( ); } }); frame.getContentPane( ).add(matchButton, BorderLayout.SOUTH); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setSize(200, 150); frame.setVisible(true); } } ![]() |
I l@ve RuBoard |
![]() ![]() |