The
Element Interface
Use
It to Model Text in Swing Applications
Swing has a interesting interface, called the Element,
which has the power to describe various structural parts of a
document, such as paragraphs, lines of text, or even (in HTML
documents) items in lists. Conceptually, the Element interface
captures some of the spirit of an SGML document. So if you know
SGML, you may already have some understanding of Swing's Element
interface.
This article explains how the Element interface works and
shows how you can make use of it in your Swing programs. The article
may help you get a better understanding of how Swing's Document
interface models its content, as well as how operations on the
Document interface affect the structure of its associated document.
(The Document interface is the model of the JTextComponent
class, from which the JTextField
and JTextPane
subclasses are derived).
By Scott Violet
The interface Element is defined in javax.swing.text.Element
file. This article introduces the Element interface by examining
these major topics:
In Swing, the interface Element defines a structural
piece of a Document, like a paragraph, a line of text, or a list
item in an HTML document. These are the methods that define the
Element interface:
Element getParentElement();
int getElementCount();
int getElement(int index);
boolean isLeaf();
Document getDocument();
String getName();
AttributeSet getAttributes();
int getStartOffset();
int getEndOffset();
Every Element is either a branch or a leaf.
If an element is a branch, the
isLeaf() method returns false.
If an element is a a leaf,isLeaf()
returns true.
Branches can have any number of children. Leaves do
not have children. To determine how many children a branch has,
you can call getElementCount().
To determine the parent of an Element, you can call getParentElement().
Root elements don't have parents, so calling getParentElement()
on a root returns null.
An Element represents a specific region in a Document
that begins with startOffset
and ends just before endOffset.
The start offset of a branch Element is usually the start offset
of its first child. Similarly, the end offset of a branch Element
is usually the end offset of its last child.
Every Element is associated with an AttributeSet
that you can access by calling getAttributes().
In an Element, and AttributeSet is essentially a set of key/value
pairs. These pairs are generally used for markup -- such as determining
the Element's foreground color, font size, and so on. But it is
up to the model, and the developer, to determine what is stored
in the AttributeSet.
You can obtain the root Element (or Elements) of
a Document by calling the methods getDefaultRootElement()
and getRootElements(),
which are defined in the Document interface.
The Document interrface is responsible for translating
a linear view of the characters into Element operations. It is up
to each Document implementation to define what the Element structure
is.
The PlainDocument
class defines an Element structure in which the root node has a
child node for each line of text in the model. Figure
1 shows how two lines of text would be modeled by a PlainDocument:
|
Figure
1
How a PlainDocument models two lines of text
|
Figure 2 shows how how those
same two lines of text might map to actual content:
|
Figure
2
Mapping text lines to actual content
|
As just mentioned, a PlainDocument contains a root
Element, which in turn contains an Element for each line of text.
When text is inserted into a PlainDocument, it creates the Elements
that are needed for an Element to exist for each newline. To illustrate,
let's say you wanted to insert a newline at offset 2 in Figure
2, above. To accomplish this objective, you could use the Document
method insertString(),
using this syntax:
document.insertString(2, "\n", null);
After invoking the insertString()method,
the Element structure would look like the one shown in Figure
3.
|
Figure
3
Inserting a newline
|
As another example, let's say you wanted to insert
the pattern "new\ntext\n" at offset 2 as shown previously
in Figure 2. This operation would
have the result shown in Figure 4.
|
Figure
4
Inserting a pattern
|
In the preceding illustrations, I have changed the
name of the line Elements after the insertion to match the line
numbers. But notice that when I do this, the AttributeSets remain
the same. For example, in Figure 2 , the AttributeSet
of Line 2 matches that of the AttributeSet of Line 4 in Figure
4.
Removing
text from a PlainDocument
Removal of text results in a structure change if
the deletion spans more than one line. Consider a deletion of seven
characters starting at Offset 1 shown previously in Figure
3. In this case, the Element representing Line 2 is completely
removed, as the region it represents is contained in the deleted
region. The Elements representing Lines 1 and 3 are joined, as they
are partially contained in the deleted region. Thus, we have the
result:
The DefaultStyledDocument
class, used for styled text, contains another level of Elements.
This extra level is needed so that each paragraph can contain different
styles of text. In the two paragraphs shown in Figure
6, the first paragraph contains two styles and the second paragraph
contains three styles.
|
Figure
6
Removing text
|
Figure 7 shows how those same
Elements might map to content.
|
Figure
7
Mapping to content
|
As previously mentioned,
DefaultStyledDocument maintains an Element structure such that the
root Element contains a child Element for each paragraph. In turn,
each of these paragraph Elements contains an Element for each style
of text in the paragraph. As an example, let's say you had a document
containing one paragraph, and that this paragraph contained two
styles, as shown in Figure 8.
|
Figure
8
Inserting styled lines into a paragraph
|
If you then wanted to insert a newline at offset
2, you would again use the method insertString(),
as follows:
styledDocument.insertString(2, "\n",
styledDocument.getCharacterElement(0).getAttributes());
This operation would have the result shown in Figure
9.
|
Figure
9
Inserting three styled lines into two paragraphs
|
It's important to note that the AttributeSet passed
to insertString()
matches that of the attributes of Style 1. If the AttributeSet passed
to insertString()
did not match, the result would be the situation shown in Figure
10.
|
Figure
10
The result of mismatched attributes
|
Removing text from a DefaultStyledDocument is similar
to removing text from a PlainDocument. The only difference is the
extra level of Elements. Consider what would happen if you deleted
two characters at Offset 1 from Figure 10, above.
Since the the second Element of Paragraph 1 is completely contained
in the deleted region, it would be removed. Assuming the attributes
of Paragraph 1's first child matched those of Paragraph2's first
child, the results would be those shown in Figure
11.
|
Figure
11
Removing styled text
|
If the attributes did not match, we would get the
results shown in Figure 12.
|
Figure
12
The result of more mismatched attributes
|
The StyledDocument
class provides a method named setCharacterAttributes(),
which allows you to set the attributes on the character Elements
in a given range:
public void setCharacterAttributes
(int offset, int length, AttributeSet s, boolean replace);
Recall that in the diagrams shown in the previous
section, all leaf Elements shown in the drawings were also character
Elements. That means that the setCharacterAttributes()method
could be used to set their attributes.
The setCharacterAttributes()
method takes four arguments . The first and second arguments identify
a region in the Document that is to be changed. The third argument
specifies the new attributes (as an AttributeSet), and the fourth
argument determines if the new attributes should be added to the
existing attributes (a value of false)
or if the character Element should replace its existing attributes
with the new attributes (a value of true).
As an example, let's say you wanted to change the
attributes of the first three characters in Figure
9, shown previously. The first two arguments passed to setCharacterAttributes()
would be 0 and 3. The third argument would be the AttributeSet containing
the new attributes. In the example we are considering, it doesn't
matter what the fourth argument is.
As the start and end offsets of the changed region
(0 and 3) fall on character Element boundaries, no structure change
is needed. That is, only the attributes of the character Element
style 1 will change.
Now let's look at an example that requires a structure
change. Instead of changing the first three characters shown in
Figure 9, let's change the first two characters.
Because the end change offset (2) does not fall on a character Element
boundary, the Element at offset 2 must be split in such a way that
offset 2 is the boundary of two Elements. Invoking setCharacterAttributes()
with a start offset of 0 and length of 2 has the result shown earlier
in Figure 10.
Changing
Paragraph Attributes in a StyledDocument
The StyledDocument class provides a method named
setParagraphAttributes(),
which can be used to change the attributes of a paragraph Element:
public void setParagraphAttributes
(int offset, int length, AttributeSet s, boolean replace);
This method is similar to setCharacterAttributes(),
but it allows you to change the attributes of paragraph Elements.
It is up to the implementation of a StyledDocument to define which
Elements are paragraphs. DefaultStyledDocument interprets paragraph
Elements to be the parent Element of the character Element. Invoking
this method does not result in a structure change; only the attributes
of the paragraph Element change.
Further
Reading
This article has presented a brief overview of how
the Document interface models its content using the Element interface.
For further reading, I recommend looking at the Swing online API
(javadoc) entries for EditorKit
and View.
View is responsible for rendering a particular Element, and EditorKit
is responsible for a ViewFactory
that is able to decide what View should be created based on an Element.
Happy modeling.
|