Overview of the Table Editing Mechanism

Java > Core SWING advanced programming > 3. TEXT COMPONENTS WITH CUSTOM VIEWS > Customized Document Views

 

Customized Document Views

The document content and the associated attributes in its model specify what the text component should display and how it should display it, but the job of rendering the text is actually performed by various view objects that know how to interpret the document content. The views are responsible for laying out the text on the screen, arranging it into paragraphs, performing word wrapping, supplying margins, and changing color as appropriate. If the standard views do not meet your needs, you can create views of your own that perform custom rendering. To do this, you need to understand how attributes are stored in the document model. In this section, we'll look at how the attributes are represented, and then we'll see an example of a custom view.

How Attributes Are Stored

As you saw in the last chapter, the data in a text component is held in the Document, but that's not all that the Document stores. Holding the data alone is not sufficient, even for the simpler text components. The content of a JTextArea, for example, is divided into lines separated by newline characters, so it is necessary to remember not only the text, but also where the line boundaries are. In the case of JEditorPane and JTextPane, as you saw in the last chapter, the text is arranged into paragraphs and can have arbitrary formatting associated with it. Instead of trying to embed the structural information into the storage used to hold the text, the Document implementations supplied with the Swing text package maintain a parallel structure of elements that organizes the text into lines or paragraphs and provides the storage for the associated attributes. The details of the element structure vary from component to component. Let's look first at the structures used by JTextField and JTextArea.

The Element Structure of the Simple Text Components

The element structure of a text component can be seen by using the dump method of AbstractDocument, which sends a readable representation of it to a given output stream:

 public void dump(PrintStream out); 

If you type the following command:

 java AdvancedSwing.Chapter3.TextFieldElements 

you'll see a text field with a single line of text appear. In the window from which you started the program, you'll see the following output:

 <paragraph>   <content>     [0,33][That's one small step for man...] <bidi root>   <bidi level     bidiLevel=0   >     [0,33][That's one small step for man...] 

The first part of this output (up to the string "<bidi root>") shows the elements that form the logical structure of the text field. The rest of the output is the structure used when there is bi-directional text in the document; bi-directional text is discussed in Chapter 5. A document can have an arbitrary number of different hierarchy structures layered over it, but the standard Document implementations support only two. If you want to add extra hierarchies that map the content in different ways, you can do so, but you must provide a way to store the location of the root of each hierarchy. Usually, you would do this by subclassing PlainDocument or DefaultStyledDocument and overriding the following method:

 public Element[] getRootElements(); 

The PlainDocument and DefaultStyledDocument classes implement this method to return an array with two entries, the first of which points to the usual element structure that we'll discuss in this section, and the second to the bi-directional text information. If you need extra hierarchies, you should return an array that contains the two standard entries and your own private ones. The standard Document implementations also provide the following convenience methods:

 public Element getDefaultRootElement(); public Element getBidiRootElement(); 

which obtain the same hierarchy information directly, without assuming the order of entries in the array returned by getRootElements.

Let's concentrate on the main element structure that is shown in the first part of the program ouput that you have just seen. What this output shows is that a text field consists of a single paragraph, within which there is a single piece of content that represents all the text that was added to the text component, including a trailing newline.

Core Note

The newline was not supplied by the code that created the text field it was added by the Document implementation. If a newline had been included in the text inserted in the component by the setText method, the document would contain two newlines. In fact, you can add more than one line of text to a JTextField and it will display them much like a JTextArea would. It isn't really intended to be used in this way, however, so it is recommended that you restrict yourself to a single line of text.



Elements are hierarchical. A content element is always the lowest level of the hierarchy and the only one that can reference actual character data. A paragraph element represents the next level of the hierarchy; its only job is to contain one or more content elements. In the case of JTextField, the model contains a single paragraph element, which in turn contains a single content element. JTextArea uses a very similar element structure, as you can see by typing the command:

 java AdvancedSwing.Chapter3.TextAreaElements 

This example creates a JTextArea with two lines of text, separated by a newline. Here is what the output looks like in this case:

 <paragraph>   <content>     [0,33][That's one small step for man...]   <content>     [33,61] [One giant leap for mankind.] <bidi root>   <bidi level     bidiLevel=0   >     [0,61][That's one small step for man...            One gia...] 

Ignoring the bi-directional text elements, you can see that this is just a logical extension of the elements structure used for JTextField: The whole document is represented by a single paragraph element, which contains two content elements, one for each line of text. The data in each content element includes its terminating newline. Another way to look at this is to show the hierarchy pictorially, as in Figure 3-1.

Figure 3-1. Element hierarchy for a JTextArea.
graphics/03fig01.gif

Here, you can see more clearly the fact that the paragraph element is the parent of the two content elements and that the content elements map directly to text within the model. There is no overlap between the text in the first content element and that in the second. This element hierarchy is created and maintained by the Document implementation. The particular structure used for JTextField and JTextArea is determined by the PlainDocument class, which is the model used by both of these components. As you'll see later, it is slightly different from the structures used by JEditorPane and JTextPane. However, the idea of a hierarchy of elements is common to both PlainDocument and DefaultStyledDocument, which manages the element structure for the more complex text components.

It is important to realize that the element structure is not constrained by or directly related to the way in which the text will actually appear on the screen. Putting this more clearly, a single content element in a JTextArea corresponds to a single line of text. However, a line of text (in this sense) does not have to be rendered on a single screen line. If the text component is performing wrapping, a single content line may actually be spread over more than one line on the screen. The job of mapping content lines to screen lines is performed by the View objects that you'll see later in this chapter. To see an example of this, try the following command:

 java AdvancedSwing.Chapter3.TextAreaElements2 

This example creates a text area in which word wrapping is enabled but which is too narrow for either line of text to be shown in its entirety in the horizontal space available. Because word wrapping is enabled, the lines wrap at the right edge of the component and continue on the next line, so that four screen lines are used to display the two lines of text (see Figure 3-2). The element structure, however, is exactly the same as that shown in Figure 3-1 two content elements within a single paragraph. Enabling wrapping, then, changes the way in which the element structure is used by the View objects, but does not change the element structure.

Figure 3-2. JTextArea with word wrapping.
graphics/03fig02.gif
Elements and Element Types

The previous section mentioned content and paragraph elements, but these terms are simply convenient tags that reflect the way in which these specific element types are used. In regard to classes and interfaces, all elements are implementations of the Element interface, which is defined by the Swing text package (javax.swing.text). Real elements are all instances of LeafElement or BranchElement, which are inner classes of AbstractDocument that implement the Element interface These two classes are derived from the abstract base class AbstractDocument.AbstractElement. Here is the definition of the Element interface:

 public interface Element {    public Document getDocument();    public Element getParentElement();    public String getName();    public AttributeSet getAttributes();    public int getStartOffset();    public int getEndOffset();    public int getElementIndex(int offset);    public int getElementCount();    public Element getElement(int index);    public boolean isLeaf(); } 

The getDocument method returns a reference to the Document of which the element is a part. Because elements exist in a hierarchy, the getParentElement method can be used to find the parent of the element to which it is applied. In terms of Figure 3-1, the paragraph element is the parent of the two content elements. Invoking getParentElement on either of the content elements would return a reference to the paragraph element, while invoking it on the paragraph element would return null, because it has no parent. The getName method returns a String that describes the type of the element. The values most commonly returned from this method are:

AbstractDocument.ParagraphElementName "paragraph"
AbstractDocument.ContentElementName "content"
AbstractDocument.SectionElementName "section"
AbstractDocument.BidiElementName "bidi level"
StyleConstants.ComponentElementName "component"
StyleConstants.IconElementName "icon"

As you can see, you can use this method to determine whether an element represents a paragraph or the content of a paragraph. Of the other four element types, the section, icon, and component elements are used by JEditorPane and JTextPane, as you'll see in "Elements and JTextPane", and the bidi level element is used to manage bi-directional strings and appears only in the bi-directional hierarchy of elements that you have seen when looking at the element structure of both JTextField and JTextArea. You'll see more about bi-directional strings in Chapter 5.

The getAttributes method returns the attributes associated with an element; attributes are not much used by the simple text components, so we'll postpone discussion of how they fit into the element structure until later.

The getStartOffset and getEndOffset methods return the start and end offsets of the content of the text component with which the element is associated. Because elements are hierarchical, more than one element may contain a particular offset but only one content element (in a given hierarchy) will map to any specified offset. That offset will also be within the start and end offset range of the containing paragraph. If you refer to the element hierarchy for the JTextArea shown in the output from the TextAreaElements example (and represented pictorially in Figure 3-1), you'll see that the start and end offsets for each content element are displayed in square brackets. The first line occupies offsets 0 through 33, including its trailing newline, while the second line has start offset 33 and end offset 61. Notice that the end offset is one larger than the offset of the last character in the element's range, so that the end offset of the first line is the same as the start offset of the second. The start and end offsets of the paragraph element, which are not shown by the AbstractDocument dump method, are 0 and 61, respectively, but the last offset occupied by a real character is 60. Thus, the offset range of the paragraph contains the union of the offset ranges of its child content elements.

The getElementIndex, getElementCount, getElement, and isLeaf methods are used to map between an element hierarchy and the document content. When applied to an element, these methods return information as known by that element. The getElementIndex method returns the ordinal of the child element that contains the given document offset. To see what this means, refer to Figure 3-1. If the call

 getElementIndex(34); 

is made on the paragraph element, it would return the index 1 because it contains two content elements, numbered 0 and 1, in which the element with index 0 covers offsets 0 through 32 (inclusive) and the element with index 1 corresponds to offsets 32 through 60. Applying this call to either of the content elements, however, would return -1, because content elements are at the lowest level of the hierarchy and so do not contain other elements. In terms of classes, a content element is an instance of the AbstractDocument.LeafElement class, while paragraphs are instances of AbstractDocument.BranchElement. A BranchElement is a container that can hold one or more LeafElements, but also appears as a fully fledged element in its own right.

Core Note

Keep in mind the distinction between the two kinds of elements types that we are discussing here. On the one hand, there are two fundamental classes that are used to represent all the elements within a Document (BranchElement and LeafElement). These types are of importance when the element structure is being built, but not when it is being interpreted as the text component is being rendered by View objects. The View objects make use of the second type of element classification the string returned by the getName method, which represents a logical way of distinguishing different types of element Section, paragraph, content, and bidi elements are logically different from the viewpoint of Views, but they are not all implemented by different Java classes.



The getElementCount method returns the number of child elements that a given element has. For LeafElements, like the two content elements in Figure 3-1, this always returns 0. For the paragraph BranchElement in the same diagram, this method would return the value 2.

The getElement method returns the child whose index is supplied as its argument. If applied to a LeafElement, this method always returns null. In Figure 3-1, this method would return a reference to the leftmost content element if called with argument 0 and to the rightmost element when called with argument 1. Other indices do not map to existing child elements and return the value null. The isLeaf method indicates whether the element to which it is applied could not contain child elements, returning true for a LeafElement and false for a BranchElement. Note that isLeaf always returns false for a BranchElement, even if it actually has no child elements when the method is invoked.

Elements and JTextPane

With the basic concepts underlying elements covered, let's now look at how the element structure is built by DefaultStyledDocument. Because this class is used by both JEditorPane and JTextPane, what you'll see in this section applies to both of these components even though, for brevity, we'll refer exclusively to JTextPane throughout. As before, we'll examine the element structure by using a representative example and looking at the results produced by the AbstractDocument dump method. You can run this example using the command:

 java AdvancedSwing.Chapter3.TextPaneElements 

This command produces quite a lot of output in the command window from which it was started. Because the dump call in this example writes to the program's standard output, you can easily capture the results in a file using shell redirection. For example,

 java AdvancedSwing.Chapter3.TextPaneElements > filename 

With the appropriate choice of filename, this works for both UNIX and DOS. The result will look something like this:

 <section>   <paragraph     bold=true     resolver=NamedStyle:MainStyle    {resolver=AttributeSet,name=MainStyle,                   RightIndent=16.0,size=12,family=serif,   LeftIndent=16.0,FirstLineIndent=16.0,nrefs=1}     name=Heading2     foreground=java.awt.Color[r=255,g=0,b=0]     size=16     family=serif     LeftIndent=8.0     FirstLineIndent=0.0   >     <content>       [0,38][Attributes, Styles and Style Contexts]   <paragraph     resolver=NamedStyle:MainStyle {resolver=AttributeSet,name=MainStyle,              RightIndent=16.0,size=12,family=serif,              LeftIndent=16.0,FirstLineIndent=16.0,nrefs=l}   >     <content>       [38,49][The simple ]     <content       name=ConstantWidth       foreground=java.awt.Color[r=0,g=255,b=0]       family=monospaced     >       [49,62][PlainDocument]     <content>       [62,223][ class that you saw in the previous chap     <content       name=ConstantWidth       foreground=java.awt.Color[r=0,g=255,b=0]       family=monospaced     >       [223,237][StyledDocument]     <content>       [237,249][ interface. ]     <content       name=ConstantWidth       foreground=java.awt.Color[r=0,g=255,b=0]       family=monospaced     >       [249,263][StyledDocument]     <content>       [263,286][ is a sub-interface of ]     <content       name=ConstantWidth       foreground=Java.awt.Color[r=0,g=255,b=0]       family=monospaced     >       [286,294][Document]     <content>       [294,475][ that contains methods for manipulating  ]     <content       name=ConstantWidth       foreground=java.awt.Color[r=0,g=255,b=0]       family=monospaced     >       [475,489][StyledDocument]     <content>       [489,497][ called ]     <content       name=ConstantWidth       foreground=java.awt.Color[r=0,g=255,b=0]       family=monospaced     >       [497,518][DefaultStyledDocument]     <content>       [518,557][ that is used as the default model for ]     <content       name=ConstantWidth       foreground=Java.awt.Color[r=0,g=255,b=0]       family=monospaced     >       [557,566][JTextPane]     <content>       [566,639][ and is also the base class from which m ]     <content       name=ConstantWidth       foreground=Java.awt.Color[r=0,g=255,b=0]       family=monospaced     >       [639,651][HTMLDocument]     <content>       [651,733] [ class that handles input in HTML format ]     <content       name=ConstantWidth       foreground=java.awt.Color[r=0,g=255,b=0]       family=monospaced     >       [733,754][DefaultStyledDocument]     <content>       [754,759][ and ]     <content       name=ConstantWidth       foregrounds=java.awt.Color[r=0,g=255,b=0]       family=monospaced     >       [759,768][JTextPane]     <content>       [768,834][ you need to understand how Swing repres ]   <paragraph     resolver=NamedStyle:MainStyle {resolver=AttributeSet,name=MainStyle,              RightIndent=16.0,size=12,family=serif,              LeftIndent=16.0,FirstLineIndent=16.0,nrefs=1}     name=CompPara     SpaceAbove=16.0   >     <component       $ename=component       name=Component       component=javax.swing.JLabel[,0,0,0x0,invalid,hidden,                 alignmentX=0.0,alignmentY=null,                 border=,flags=0,                 maximumSize=,minimumSize=,preferredSize=,     defaultIcon=javax.swing.ImageIcon@685ee635,disabledIcon=,     horizontalAlignment=CENTER,horizontalTextPosition=CENTER,                iconTextGap=4,labelFor=,                text=Displaying text with attributes,   verticalAlignment=CENTER,verticalTextPosition=BOTTOM]     >       [834,835][ ]     <content       name=Component     >       [835,836][ ]   <paragraph     resolver=NamedStyle:MainStyle {resolver=AttributeSet,name=MainStyle,              RightIndent=16.0,size=12,family=serif,   LeftIndent=16.0,FirstLineIndent=16.0,nrefs=1}     name=CompPara     SpaceAbove=16.0   >     <content>       [836,837][ ] <bidi root>   <bidi level     bidiLevel=0   >     [0,837][Attributes, Styles and Style Contexts Th ] 

The first thing to notice is that this document starts with a new element type called section All DefaultStyledDocument element hierarchies are wrapped in a section element, whose only job is to act as a container for paragraph elements. The second difference between this hierarchy and the ones you have already seen is the way in which paragraph elements are used. In the JTextArea, the whole document is contained in a single paragraph element and each line is a content element. When DefaultStyledDocument is used, however, a paragraph corresponds more closely to what you would think of as a paragraph in a word processor such as Microsoft Word a paragraph is a continuous run of text that ends with (and includes) a newline character. In other words, a paragraph element is used in a JTextPane where a content element would have been used with JTextArea.

If that is the case, what are content elements used for? In a DefaultStyledDocument, a single content element maps the longest possible run of text that has the same set of character attributes, which is often smaller than a single line of text. In the simplest case, if you create a JTextPane with one line of text and no attributes, you'll get a single section element that contains a paragraph element wrapped around one content element that maps all the text. As soon as you add character attributes, the content elements are broken up at the boundaries between which the attributes are applied.

The document shown in Figure 3-6 has four paragraphs the heading, the text, the paragraph containing the diagram, and a paragraph at the end of the document containing the newline that was created automatically for us by DefaultStyledDocument. If you refer to the program output shown above, you should be able to find the paragraph elements for each of these. Each element has its own associated attribute set, the content of which is shown when it is nonempty. Consider the heading paragraph, the first one in the document. Here is what the paragraph element looks like:

Figure 3-6. The View and Element hierarchies for JTextField.
graphics/03fig06.gif
 <paragraph     bold=true     resolver=NamedStyle:MainStyle {resolver=AttributeSet,name=MainStyle,              RightIndent=16.0,size=12,family=serif,              LeftIndent=16.0,FirstLineIndent=16.0,nrefs=1}     name=Heading2     foreground=java.awt.Color[r=255,g=0,b=0]     size=16     family=serif     LeftIndent=8.0     FirstLineIndent=0.0   >     <content>       [0,38][Attributes, Styles and Style Contexts] 

The attributes are shown immediately after the paragraph tag. If you refer to Listing 2-2 in Chapter 2, you'll see that these are exactly the attributes that were encoded into the Heading2 style. You'll also see two attributes that we didn't install by name the name attribute, whose meaning is obvious, and the resolver attribute. The resolver attribute was discussed in the previous chapter it points to the attribute set's resolving parent which, in this case, is called MainStyle. As you'll remember, this is the logical style for the document and it is linked to the Heading2 style in this way because it was applied to the paragraph using the setLogicalstyle method. The dump method actually shows the content of the resolving parent attribute set and, as you can see, these attributes correspond exactly to the logical style created in Listing 2-3. This paragraph contains only one content element because the heading doesn't have any explicit character attributes applied to it.

Now let's look at the start of the next paragraph. The element structure that corresponds to part of the first line of this paragraph is shown next.

 <paragraph     resolver=NamedStyle:MainStyle {resolver=AttributeSet,name=MainStyle,              RightIndent=16.0,size=12,family=serif,              LeftIndent=16.0,FirstLineIndent=16.0,nrefs=1}   >     <content>       [38,49][The simple ]     <content       name=ConstantWidth       foreground=java.awt.Color[r=0,g=255,b=0]       family=monospaced     >       [49,62][PlainDocument]     <content>       [62,223][ class that you saw in the previous chap ]     <content       name=ConstantWidth       foreground=java.awt.Color[r=0,g=255,b=0]       family=monospaced     >       [223,237][StyledDocument]     <content>       [237,249][ interface. ]     <content       name=ConstantWidth       foreground=java.awt.Color[r=0,g=255,b=0]       family=monospaced     >       [249,263][StyledDocument] 

First, notice that the paragraph attributes don't contain a name attribute, but they do have a resolver attribute. In fact, if you look back at Listing 2-3, you'll see that this paragraph was created by adding an empty paragraph attribute set and the logical style. An empty attribute set has no attributes at all, not even name and resolver attributes, which explains why there is no name attribute present. The resolver attribute was added when setLogicalStyle was called and, again, you can see that the attributes shown are those of the global logical style created in this example.

Within this paragraph, there are many content elements. In fact, each time the character attributes change, there is a new content element. The first content element corresponds to the words "The simple." It uses the paragraph attributes and has no explicit character attributes applied to it (refer to the content array in Listing 2-3 to see where character attributes are applied), but the next word, "PlainDocument" was rendered in green and with a constant-width font. Because it has different character attributes, it has its own content element, to which the extra attributes that were applied to it in Listing 2-3 are attached. Similarly, the next run of text has no character attributes and so needs yet another content tag, with an empty character attribute set. Both of these elements inherit all the attributes stored in their parent element.

Toward the end of the document you'll see the elements that are involved in representing the diagram, which is a JLabel included in the document using the Component attribute. When the element structure was created, a component element was added to represent the place at which the diagram should be rendered. A component element is just a leaf element whose name has been set to "component" in all other respects, it is the same as a content element. The attributes in this element include the original component attribute that points to the Component and the content associated with it is the space that was added and to which the component attribute was applied. If an Icon had been installed in the document, an icon element would have been used instead.

You'll notice that the component element has an attribute called $ename that has the value "component." This attribute is actually the name of the element it is the value that would be returned by the Element method getName. Elements store their names within their associated attribute set. If you're wondering why the paragraph, content, and section elements don't have a $ename attribute, it's because this attribute is not included in an attribute set by default; BranchElement returns "paragraph" and LeafElement returns "content" from the getName method if the attribute set does not define $ename. A section element is implemented by the SectionElement inner class of DefaultStyledDocument and returns "section" from its getName method. Therefore, these three types do not need to define $ename attribute.

Core Note

SectionElement is a subclass of BranchElement, because it is required to act as a container for ParagraphElements.



Views: Rendering the Document and Its Attributes

The document model contains both the raw text and the structural elements needed to reproduce the content of a text component in the way that the programmer expects. The model does not, however, directly determine how the component appears on the screen. This job is done by view objects, which interpret the element structure to lay out the text in paragraphs and lines and apply colors, fonts, and other effects as specified by the attributes held within the elements. In this section, we'll show you how the standard components map view objects onto the underlying model, and then we'll look at how to create your own views to enhance the rendering of text fields and JTextPane. The techniques that you'll see in this section will be shown in the context of specific text components, but they are generic and can therefore be used with all of the text controls.

The Basic Views

There is a set of standard views used by the Swing text components, the most common of which were shown in Table 1-2 in Chapter 1. These views are created slightly differently by the simpler text components and the more complex ones. Let's start by looking at views in general and at the view structure of JTextField and JTextArea in particular to see how the views map onto the element structure shown in the previous section.

Views and the View Hierarchy

There are basically two types of views those that act as containers and those that simply render text or other content. This makes views analogous to AWT components, some of which are also containers. The similarity also extends to the way in which views are handled because, for example, a view may be asked for its preferred size and will subsequently be given a rectangular area within which to work and within which it must render its content. Unlike AWT, however, there are no separate layout managers that you can assign to "container" views; the view contains the code that does the work that would fall to a layout manager.

Views are linked to elements and, like the element structure, the view structure is hierarchical. In the simplest possible case, one view object would map a single element, but this does not always happen sometimes, a single element has more than one view associated with it. However, no view is ever associated with more than one element. A consequence of this is that the region of text covered by a single basic view (that is, a view that is not a container) only ever has one set of attributes, because an element only has a single (logical) attribute set. For reference, Table 3-1 lists the most important of the standard views in the Swing text package and indicates which is a container and which is a basic view and the text component with which they are associated.

Core Note

In this chapter, the term "basic view" will be used to refer to a view that is not a container.



Table 3-1. Swing Text Views by Type
View Type Text Component
BoxView Container Generic
CompositeView Container Generic
ComponentView Basic JEditorPane/JTextPane
FieldView Basic JTextField
IconView Basic JEditorPane/JTextPane
LabelView Basic JEditorPane/JTextPane
LabelView.LabelFragment Basic JEditorPane/JTextPane
ParagraphView Contaner JEditorPane/JTextPane
ParagraphView.Row Container JEditorPane/JTextPane
PasswordView Basic JPasswordField
PlainView Basic JTextArea
TableView Contaner JEditorPane/JTextPane
TableView.TableRow Container JEditorPane/JTextPane
TableView.TableCell Container JEditorPane/JTextPane
TableView.ProxyCell Basic JEditorPane/JTextPane
WrappedPlainView Contaner JTextArea
WrappedPlainView.WrappedLine Basic JTextArea

Notice that some of the views in Table 3-1 are inner classes of other views. For example, the container view ParagraphView has an inner class called ParagraphView.Row, which is also a container view. These inner class views are used to manage a smaller part of the element to which the main view is mapped. You'll see how this works when we look at specific examples later in this section.

The class hierarchy of the standard views is shown in Figure 3-3. For clarity, this diagram does not show the inner classes. Notice that all the container views are derived from BoxView, which is a subclass of Compositeview.Compositeview implements the methods that are required to host child views with no specific spatial relationship between them, while BoxView organizes its children either horizontally or vertically. ParagraphView is, in turn, a specialized form of BoxView that arranges lines in a vertical configuration to form a paragraph. You'll see examples shortly that show how specific container and child views are used by some of the text components to achieve the expected text layout.

Figure 3-3. The Swing text view class hierarchy.
graphics/03fig03.gif
The View Class

All views are derived from the abstract class View, the methods and constants of which are shown in Table 3-2. As you can see from the constructor, to create a View, you must have an element with which it will be associated. Usually, a particular View subclass will be associated with a specific type of element. Views are created by a ViewFactory, which knows how to create the appropriate Views for the types of elements that are contained within the document that it is working on behalf of.

Table 3-2. Methods and Constants of the View Class
 public View(Element elem) public View getParent() public void setParent(View parent) public Document getDocument() public View(Element elem) public int getStartOffset() public int getEndOffset() public Element getElement() public AttributeSet getAttributes() public Container getContainer() public ViewFactory getViewFactory() public boolean isVisible(); public float getAlignment(int axis) public int getViewCount() public View getView(int n) public abstract float getPreferredSpan(int axis) public float getMinimumSpan(int axis) public float getMaximumSpan(int axis) public int getResizeWeight(int axis) public void setSize(float width, float height) public Shape getChildAllocation(int index, Shape a) public void preferenceChanged(View child, boolean width,      boolean height) public abstract void paint(Graphics g, Shape allocation) public int getNextVisualPositionFrom(int pos,      Position. Bias b, Shape a, int direction,      Position. Bias[] biasRet) throws BadLocationException public abstract Shape modelToView(int pos, Shape a,      Position.Bias b) throws BadLocationException public Shape modelToView(int p0, Position.Bias b0, int p1,      Position.Bias b1, Shape a) throws BadLocationException public View (Element elem) public abstract int viewToModel (float x, float y, Shape a,      Position.Bias [] biasReturn); public void insertUpdate (DocumentEvent e, Shape a,      ViewFactory f) public void removeUpdate (DocumentEvent e, Shape a,      ViewFactory f) public void changedUpdate (DocumentEvent e, Shape a,      ViewFactory f) public View breakview (int axis, int offset, float pos,      float len) public View createFragment (int p0, int p1) public int getBreakWeight (int axis, float pos, float len) public int getResizeWeight (int axis) public static final int BadBreakWeight = 0; public static final int GoodBreakWeight = 1000; public static final int ExcellentBreakWeight = 2000; public static final int ForcedBreakWeight = 3000; public static final int X_AXIS = HORIZONTAL; public static final int Y_AXIS = VERTICAL; 

The setParent and getParent methods respectively set and retrieve the View that lies above the target View in the view hierarchy. The parent of a View is set as the hierarchy is being constructed, which usually happens when content is being added to the document.

The getDocument, getStartOffset, getEndOffset, getElement, getContainer, and getViewFactory methods all return attributes of the View. The start and end offsets will sometimes be the corresponding offsets of the element that the View is mapping, but this need not be the case, because sometimes more than one View will be used to span a single element. The offsets associated with a View are always bounded by the offset range of the element and, furthermore, no more than one non-container View covers a given offset range. The getElement method returns the mapped element, while getcontainer returns the text component that is hosting the view. This can be used, for example, to get back to the JTextField or JTextPane from within a View. You'll see why this is useful when looking at some example View implementations later in this chapter.

The getViewFactory method returns the ViewFactory that created the View. Because only one ViewFactory is used to produce all the Views for a particular component, the default implementation of this method just delegates to its parent on the assumption that the View at the root of the hierarchy will know how to locate the ViewFactory that created it. You'll find out more about this in "View Factories".

The getAttributes method returns the View's associated AttributeSet. These attributes are typically the attributes of the element that the View is mapping and are obtained directly from the element by default. You can, of course, create your own View objects that return a different AttributeSet that may or may not be related to those of the underlying element. You'll see in Chapter 4 that this technique is used by the Views used to render HTML.

The isVisible method returns true if the View is visible and false if it is not. All of the standard Views in the text package just use the default implementation of this method, which always returns true. Some of the Views in the HTML package, however, override this and can claim to be invisible. When a View is not visible, screen space is not allocated for it to draw its content into.

The getAlignment method takes an argument that specifies whether alignment along the x-axis (View.X_AXIS) or the y-axis (View.Y_AXIS) is being requested. As with AWT and Swing components, the alignment value ranges from 0.0 to 1.0 and specifies the relative location of the View's alignment point along the specified axis. A y-axis alignment value of 0.0 places the alignment point at the top of the View and 1.0 places it at the bottom. Similarly, an x-axis alignment of 0.0 aligns the left side of the View with the alignment points of other Views and 1.0 moves the alignment point to the right-hand side. By default, this method returns 0.5 for both axes, which has the effect of centering the View within whichever container it is placed. Often, a container View will be interested only in the alignment in a single direction. For example, when a ParagraphView is laying out a row, it is interested only in the y-axis alignment, to determine how to place its child Views relative to the baseline of each row within the paragraph. Figure 3-4 shows an example in which a ParagraphView is being used to lay out two lines of content, different parts of which have their own specific alignment requirements.

Figure 3-4. A view hierarchy with differing alignments.
graphics/03fig04.gif

The figure shows the ParagraphView, which places each line of content into a ParagraphView.Row subview and arranges them one above the other. Within each row, the pieces of content are placed horizontally. You can think of the ParagraphView as a box that lays out children along the y-axis, while a Paragraphview.Row is a box that lays out subviews along the x-axis. In fact, this is exactly how it works, because both ParagraphView and ParagraphView.Row are derived from BoxView, which can be constructed to manage either a vertical (View.y_AXIS) or a horizontal (View.x_AXIS) configuration. Within each row, there are views that render content. In the case of a JTextPane, these would probably be instances of LabelView. The vertical positioning of these content views is controlled by the value returned by getAlignment(View.Y_AXIS. The view labeled View A has its alignment point at its lower edge and so this edge is placed along the baseline. To achieve this, the getAlignment method would return 1.0. View B, by contrast, has its alignment point part way between its upper and lower edge, nearer the bottom than the top. The alignment value of this view would be something between 0.5 and 1.0, depending on how much of the view appears below the baseline. In Figure 3-4, the y-alignment of View B is probably around 0.8. In the second row, View C is centered on the baseline because its y-axis alignment is 0.5, while View D is aligned with its upper edge on the baseline, because its alignment is 0.0.

In practice, the standard views don't allow their alignments to be easily customized. The LabelView, which renders text for JTextPane, uses the ratio of the total height of the font to its descent to determine the y-alignment. This places the View baseline in the same place as the font baseline, so that the correct amount of space is allocated to draw the descenders for those characters in the font that have them. To change this behavior, you would have to create your own subclass of LabelView and override its getAlignment method.

The getViewCount and getView methods are used by views that act as containers and allow the set of child Views to be accessed. Because these methods exist in all Views, every View can be a container, so theoretically you could code a container View without subclassing CompositeView, the standard container in the text package. You would probably only do this, however, if you need to create a container View that has special requirements that cannot be obtained by subclassing CompositeView. The default View implementations of these methods return 0 and null, respectively.

The next three methods, getPreferredSpan, getMinimumSpan, and getMaximumSpan, are equivalent to the AWT component getPreferredSize, getMinimumSize, and getMaximumSize methods, except that they deal only in one dimension at the time. Each method takes an argument (either View.X_AXIS or View.Y_AXIS) that specifies the direction for which the span is required. The values returned from these methods are used by the parent container View to decide how large it needs to be to accommodate its child views. The functionality provided by layout managers in respect of AWT and Swing components is coded directly into the container Views and so cannot be changed by plugging in a different layout policy. This is not usually an issue, however, because the layout requirements of pieces of a text component are more clearly defined than those of graphical user interface (GUI) components.

Notice that these three methods all return values of type float, not int. The intention is that size measurements will be made in points instead of pixels, so you can render documents in a way that is not dependent on the resolution of your screen or your printer. To make this work, it is sometimes necessary to return fractional point counts. In practice, however, in both JDK 1.1 and Java 2, the text components map points directly to pixels, so it is common to see code that casts the returned value of these methods directly to ints.

The getResizeWeight method is used to indicate whether a View would like to be resized if necessary to fit available space. If this method returns 0, the View will always be rendered at its preferred size and the getMinimumSpan method will return the same value as getPreferredSpan.

The setsize method is invok ed by a View's immediate parent to inform it of the dimensions of the space allocated to it. In response to this, a container View will usually determine the new layout of its children, which will result in their setSize method being called. Noncontainer Views usually do nothing when this method is called because their main function is simply to paint their content, which will happen when the child View's paint method is invoked later.

The getChildAllocation method is implemented only by container views and returns the space allocated to a child view, given by its index number. In a noncontainer View, this method returns null. The ordering of views is dependent on a particular container; for example, ParagraphView orders rows from top to bottom, so that index 0 corresponds to the top row of the paragraph, index 1 to the next row down, and so on. By contrast, ParagraphView.Row builds a horizontal layout in which index 0 is the leftmost component, index 1 the next to the right, and so forth. The second argument to this method and the return value are both of type Shape. Shape is an interface defined by the AWT package that can represent a graphical object with any kind of outline. Given a Shape, you can use the getBounds method to get the smallest Rectangle that completely surrounds the shape. The standard text Views all deal with rectangular areas, so the Shape argument to this method (and to all the other methods that specify a Shape) will always be a Rectangle when the standard Views are in use. The Shape argument of getChildAllocation is actually the region allocated to the View. This value can be used to dynamically compute the area that corresponds to the given child, if this information is not stored within the container View.

The preferenceChanged method of a container View is called by a child View when the width or height of that child might have changed because of, for example, a change of font within the region that the View covers. The width and height values indicate which direction might have changed its preferred span, while the child argument specifies the child making the call. Not all container Views need the information provided by the child argument, so it is often supplied as null.

The paint method is, as its name suggests, where the View renders it contents given a Graphics object to draw into and a Shape that describes the area allocated to the View. Usually, a container View like ParagraphView will implement the paint method by calling the paint method of each of its children. This process may continue if there are several levels of nested Views. Eventually, the paint method of a content-rendering View will be invoked and something will be drawn on the screen (or on the printer). You'll see typical implementations of the paint method in the examples later in this chapter.

The getNextVisualPositionFrom method is typically used when moving the cursor up, down, left, or right. The pos argument is the offset within the document of the current location of the cursor, while direction, which takes one of the values SwingConstants.NORTH, SOUTH, EAST, or WEST, specifies which way the cursor is moving. The return value is the offset within the model that corresponds to the new location of the cursor. Because this method is applied to a particular View that maps only a part of the model, it is possible that the target location is not within the element that the View maps. For example, because a LabelView covers part or all of a single line of text, the result of moving the cursor up (NORTH) or down (SOUTH) will always be to move it out of the current View. If the new location is not within the bounds of the View, -1 is returned. When this happens, the parent View is responsible for redirecting the request to a more appropriate child View. Consider, for example, the situation shown in Figure 3-5. This figure shows the Views for a simple document containing a single paragraph with three lines of text as they would be if the document were rendered within a JTextPane. Each line of text is held within a ParagraphView.Row and, within that, a LabelView. The LabelView is directly associated with the content of the model, whereas the other Views are containers. Suppose the cursor is currently located immediately before the number 2 in the second row and the cursor up key is pressed. To determine where to move, the getNextVisualPositionFrom method will be called on the LabelView that contains the text for the second row. The cursor should move up to the l in the row above. However, LabelView cannot return a meaningful position when asked to move North because, as you can see from Figure 3-5, this is bound to take it outside the area that it covers. Instead, it returns -1. The ParagraphView.Row object for the second row sees this result and also returns -1 to the enclosing ParagraphView. The ParagraphView responds to this by passing the request to the ParagraphView.Row for the top row (the one before the row that it just tried), which does contain the desired location.

Figure 3-5. Determining the next visual position within a View.
graphics/03fig05.gif

Even such simple operations as moving the cursor left or right need not imply that the model position is just incremented or decremented because, for example, the model may contain content that a particular View does not actually display. In cases like this, moving the cursor forward one location might involve increasing the pos value by four if there are three intervening model locations that are not displayed. The two Bias arguments are used in conjunction with the bi-directional text support that will be described in Chapter 5.

The modelToView and viewToModel methods map directly between model positions and View locations. Because the way in which a View renders the model content is View specific, only the View can know how to map from a screen location to the position in the model that supplied the character or other information rendered at that location. For this reason, a viewToModel request is usually processed by the View that actually drew the content at the position given by the x and y values supplied. Similarly, the first form of the modelToView method is delegated to the View at the location corresponding to the pos argument. When the second form of modelToView is used, all Views in the range p0 to p1 are consulted by calling their modelToView (int pos, Shape a, Position. Bias b) method, and a Shape corresponding to the total area covered by that range is created as the union of all the Shapes corresponding to the child Views. This, for example, could be used to obtain a Rectangle covering two lines of a paragraph as the union of the Rectangles of the individual lines. You'll see typical implementations of both modelToView and viewToModel in "Custom Views and Input Fields".

The insertUpdate, removeUpdate, and changedUpdate methods are called when a change takes place within the document that falls within the area mapped by the View. Typically, these methods respond by creating or deleting Views or by changing the attributes associated with an existing View. The arguments to all of these methods are a DocumentEvent that describes the change to the document, a Shape that specifies the space allocated to the receiving View, and the View-Factory that is being used to create the Views for the text component displaying the document. This factory is used to create new Views if necessary.

The breakView and getBreakWeight methods are used to organize runs of text (and other content if necessary) into units that can fit on the current line. As you'll see later, paragraphs are built from rows whose width is bounded by the width of the space allocated to the text component. The preferred width of the text within a particular Element may bear no relation to the space available to render it. If the content of the Element is too wide to fit within the space available on the current row, it must be split at an appropriate point and some of the content moved to the next row. The breakView method has the job of creating a View that maps as much of the Element as will fit in what remains of the current row; layout then continues on the next row with the balance of the Element. If the Element is large, this splitting process may continue over several rows. The getBreakWeight method determines how favorable it is to break the View that it is applied to at the offset given by the pos argument. Some Views (such as IconView and ComponentView) cannot be broken, so return the value BadBreakweight. Text views prefer to be broken at white space boundaries and return ExcellentBreakWeight if the proposed location coincides with white space and GoodBreakWeight otherwise. Some Views require a mandatory break after they have been rendered and return ForcedBreakweight to indicate this. Splitting usually occurs along the x-axis, but could (at least theoretically) happen along the y-axis if a text component were being rendered in an environment that had a notion of page size, in which case it would be necessary to split a ParagraphView so that it was wholly contained within the available space on the current page. Although the framework exists to support this, the current implementation of ParagraphView does not split itself on the y-axis. If you need this functionality, you will have to create a custom view derived from ParagraphView. An example of a custom ParagraphView is shown later in this chapter.

While splitting a View into fragments is most often required because of space constraints, another reason for doing so is the presence of bi-directional text. When an Element contains text that flows in both directions, it is mapped by a set of Views that each render only in one direction. The createFragment method is used to create a View fragment that covers a unidirectional chunk of content.

View Factories

You've already seen that there are several different types of Views that render different types of content. In fact, the same content type can be managed by a different View type within different text components. For example, plain text in a JTextField is mapped by a FieldView, by a PasswordView in JPasswordField, by a PlainView or a WrappedPlainView in JTextArea, and by a LabelView in JTextPane. The allocation of the appropriate view for a particular type of content is performed by a ViewFactory; Different text components use different implementations of the ViewFactory to create the correct type of View for their particular circumstances.

ViewFactory is, in fact, an interface with just one method:

 public interface ViewFactory {    public View create(Element elem); } 

The create method is supposed to instantiate a View object that can map some or all of the Element given as its argument. Some ViewFactory implementations (such as that for JTextField) only have a single View type (FieldView) and so always return an object of that type. More complex components use various different View types and typically look at the name of the Element (returned by its getName method) to decide which subclass of View to use. In a JTextPane, for example, a content Element is mapped by a LabelView, while a component Element is mapped by a ComponentView. The create method is usually an if statement that compares the element's name to a fixed set of valid names to decide which type of View to return. It is equally valid, however, to use the attributes associated with the Element (or any other criterion) as well as (or instead of) the name to determine the View type. This approach is taken by JEditorPane when it is displaying HTML, as you'll see in Chapter 4.

Where does the ViewFactory come from? There are two ways in which a text component can obtain a ViewFactory: from its EditorKit or from its UI delegate. When the View hierarchy for a component is being built, the model Changed method of the component's UI delegate is called. Unless you write a custom delegate that overrides it, this method is provided by the class javax.swing.plaf.basic.BasicTextUI, which gets the ViewFactory from the getViewFactory method of an inner class called RootView that is always at the top of the View hierarchy for every text component. The getViewFactory method of BasicTextUI.RootView first calls the getEditorKit method of the JTextComponent that the Views are to be associated with, and then calls the editor kit's getViewFactory method. If this method returns a factory, it is used to create all the Views for that component. If, instead, it returns null, the UI delegate will provide the ViewFactory by supplying a suitable create method.

In terms of the standard text components, the three simpler ones, JTextField, JPasswordField, and JTextArea, all use the DefaultEditorKit, which does not supply a ViewFactory (its getViewFactory method returns null), so the ViewFactory for each of these components is implemented in their UI delegate class. By contrast, JTextPane uses StyledEditorKit (or a custom subclass of StyledEditorKit), which does provide a ViewFactory that knows how to map elements from DefaultStyledDocument. JEditorPane can use any editor kit, but that editor kit must supply a view factory, because BasicEditorPaneUI does not override the BasicTextUI create method, which returns null when asked for a View for any kind of element. The relationship between the standard Swing text components and their view factories is summarized in Table 3-3.

Table 3-3. Text Components and View Factories
Component Source of View Factory
JTextField BasicTextFieldlU
JPasswordField BasicPasswordFieldUI
JTextArea BasicTextAreaUI
JTextPane From editor kit
JEditorPane From editor kit

Because a ViewFactory creates a View based on the type of each Element that it finds in the document, it is clear that there must be a close connection between the ViewFactory and the Document class that is plugged into the text component. The factories in the UI delegates for the simple text components can, for example, return only View objects for the Element types created by PlainDocument, which is the Document model used by these components. If you create a subclass of PlainDocument that you want to use with any of these components, you must either restrict yourself to using the same element types as PlainDocument or you will have to extend the ViewFactory in the UI delegate class so that it can provide Views for your specialized element types. You'll see how to create a custom ViewFactory and a custom View for JTextField in "Custom Views and Input Fields".

The View Hierarchies of JTextField and JTextArea

The discussion so far has been a little academic, so let's make it more concrete by looking at specific examples. We'll start by creating a class that can be used to display the hierarchy of Views within a component. Once we've got this, we'll use it to look at the internals of some of the five text components in the Swing package, starting with JTextField.

Looking at the View Hierarchy

As you've already seen, all the Views that map a single component form a hierarchy, rooted from a single View called the root view. You can obtain a reference to this View by calling the getRootView method of BasicTextUI and then using the getViewCount and getview methods that you saw in Table 3-3 to find all the children, grandchildren, and later descendants of the root view. Using these methods, it is a simple matter to create a method that will recursively descend the complete view hierarchy for an instance of any text component and display a representation of all the Views that have been created, much as we were able to do for the Element structure within the document model earlier in this chapter. You can see an implementation of this in Listing 3-1.

Listing 3-1 A Text Pane with Automatic Scrolling
 package AdvancedSwing.Chapter3; import javax.swing.*; import javax.swing.text.*; import java.io.*; public class ViewDisplayer {    public static void displayViews(JTextComponent comp,                                    PrintStream out) {       View rootView = comp.getUI().getRootView(comp);       displayView(rootView, 0, comp.getDocument(), out);    }    public static void displayView(View view, int indent,                                  Document doc,                                  PrintStream out) {       String name = view.getClass().getName();       for (int i = 0; i < indent; i++) {          out.print("\t");       }       int start = view.getStartOffset();       int end = view.getEndOffset();       out.println(name + "; offsets [" + start + ", " +                   end + "]");       int viewCount = view.getViewCount();       if (viewCount == 0) {          int length = Math.min(32, end - start);          try {             String txt = doc.getText(start, length);             for (int i = 0; i < indent + 1; i++) {                out.print("\t");             }             out.println("[" + txt + "]");          } catch (BadLocationException e) {          }       } else {          for (int i = 0; i < viewCount; i++) {             displayView(view.getView(i), indent + 1,                         doc, out);          }       }    } } 

The ViewDisplayer class contains two static methods, either of which could be used to look at some or all of the View structure within a text component. The first of these methods, displayViews, is the more useful of the two. It takes as arguments a reference to a text component and a PrintStream, to which it sends its output. This method uses the JComponent getUI method to locate the text component's UI delegate (which will always be a subclass of javax.swing.plaf.TextUI) and then calls its getRootView method to locate the root of the hierarchy. It uses the second public method, displayView, to print a representation of the root View and its child Views. This method is recursive; Each time it calls itself, it increments the indent argument by one, so that the hierarchical structure of the Views that it encounters can be clearly seen. So that you can easily relate the various Views to the parts of the document model that they map, the displayView method displays not only all the View objects that it finds, but also up to the first 32 characters of the Element within the document model that each View maps.

Views and JTextField

Let's now use the ViewDisplayer class to examine the Views that are used by the simplest text component of all, the one-line JTextField. Earlier in this chapter, you saw how simple the Element structure of this component is. For ease of comparison with the View hierarchy, here are the Elements that are created for a JTextField that contains the text "That's one small step for man ":

 <paragraph>   <content>     [0,33][That's one small step for man...] 

Core Note

For clarity, we're not going to show the elements attached to the "Bidi" root in this section because they do not relate directly to the View hierarchy in any of the examples that you'll see here. Bi-directional text is discussed in Chapter 5



To see the View hierarchy for this text component, use the command:

 java AdvancedSwing.Chapter3.TextFieldViews 

The output from this command should look like this:

 javax.swing.plaf.basic.BasicTextUI$RootView; offsets [0, 33]   javax.swing.text.FieldView; offsets [0, 33]          [That's one small step for man...] 

The first thing to notice about this is that it is very simple: there is a single root view, implemented by the BasicTextUI.RootView class that maps the whole text field. Contained within the root view is a single FieldView, which also maps the whole text field. You'll see that the offset range for the FieldView exactly matches that of the content and paragraph elements seen in the Document representation. In this case, the ViewFactory for the JTextField returned a FieldView when its create method was called with the paragraph element as its argument. Because a JTextField is only supposed to hold a single line of text, you would see this exact structure in any JTextField, no matter how large its content and whether all the content could appear on the screen at the same time. In fact, if the width of the JTextField is constrained so that it cannot display all the characters in the model, the FieldView arranges for the content to appear to scroll left and right as the user types or drags the mouse inside the text field's visible area. Because this is just a matter of presentation, the model is not aware that any of this is happening. If you're interested, you can see some of the details of the scrolling mechanism in "Custom Views and Input Fields".

The mapping between the simple element hierarchy of JTextField and the corresponding View hierarchy is shown pictorially in Figure 3-6.

An obvious question that arises from Figure 3-6 is why there is no View object corresponding to the content element on the left side of the diagram. Simply put, there isn't a View object for this element because it isn't needed the paragraph element maps all the text, so the View object for the paragraph can render everything. There is slightly more to it than this, however, as you'll see when looking at the View structure for JTextArea.

JTextArea: Nonwrapping and Wrapping Views

The View structure for a JTextArea is very similar to that of JTextField, but there are two slightly different cases to consider. Let's look at the simple case first. As you saw earlier in this chapter, the element structure of a JTextArea that contains two lines of text looks like this:

 <paragraph>   <content>     [0,33] [That's one small step for man ]   <content>     [33,61] [One giant leap for mankind.] 

If you type the command

 java AdvancedSwing.Chapter3.TextAreaViews 

you'll see the view hierarchy that corresponds to this model structure:

 javax.swing.plaf.basic.BasicTextUI$RootView; offsets [0, 61]    javax.swing.text.PlainView; offsets [0, 61]          [That's one small step for man ] 

As you can see, as with the JTextField, there is only one View covering all the content, despite the fact that there are two content elements and the fact that the text covers two lines. Here, as before, the View maps the paragraph element, not the content element, and it is responsible for displaying all the data. This JTextArea does not have line wrapping turned on, so if you narrow the window so that the text no longer fits, it will just be clipped to the right. If you do this within 30 seconds of running the example, the View structure will be printed again and, not surprisingly, you'll see that it doesn't change as a result of the text area being narrowed. This situation changes, however, in the next example in which line wrapping is enabled. You can run this example with the following command:

 java AdvancedSwing.Chapter3.TextAreaViews2 

This text area is exactly the same as the previous one, except that it has line wrapping turned on. The element structure is not affected by this change, but the View hierarchy changes:

 javax.swing.plaf.basic.BasicTextUI$RootView; offsets [0, 61]    javax.swing.text.WrappedPlainView; offsets [0, 61]       javax.swing.text.WrappedPlainView$WrappedLine;                                               offsets [0, 33]             [That's one small step for man ]       javax.swing.text.WrappedPlainView$WrappedLine;                                              offsets [33, 61]             [One giant leap for mankind.] 

As before, the paragraph element maps to a WrappedPlainView, but this time each content element has a corresponding WrappedPlainView.WrappedLine object. WrappedLine is an inner class of WrappedPlainView; its job is to render one line of text, wrapping it at the right margin of the text component if necessary. If you narrow the window within 30 seconds of starting this example, you'll see that the text does indeed wrap (as shown in Figure 3-2), but the View structure does not change. Therefore, the WrappedLine View may need to draw text that covers more than one line on the screen. The View and Element structures for this JTextArea are shown in Figure 3-7.

Figure 3-7. The View and Element hierarchies for JTextArea with line wrapping.
graphics/03fig07.gif

How does this more complex View hierarchy get built? So far, all we've said about building the View hierarchy is that the create method of the ViewFactory is called to manufacture Views corresponding to elements in the model. However, you've just seen this work in two different ways. In the case of the JTextField and the nonwrapping JTextArea, only one View is created, despite the fact that there are two content elements and, in fact, these Views correspond to the paragraph element that wraps all the model content, rather than the content elements themselves. By contrast, when line wrapping is turned on, the View structure changes there is still a top-level View that is associated with the paragraph element, but now there are two more, one for each content element. What is it that causes this difference in the View structure? To see how this happens, let's look at how the second of these structures is built.

To build the View structure, the getViewFactory method of the RootView is called (from the UI delegate's modelChanged method) to get the ViewFactory and then the factory's create method is called, passing it the root element of the document hierarchy as its argument. The View returned by this call is installed as the only child of the RootView of the component and no more create calls are made from the modelChanged method. In the case of a JTextArea with line wrapping enabled, the initial create call results in the creation of a WrappedPlainView object that maps the paragraph element given to it. When this View is connected to the RootView, its setParent method is called to establish a backward reference from the WrappedPlainView to the RootView. As you can see from the class hierarchy diagram in Figure 3-3, WrappedPlainView is derived from BoxView, which is a subclass of CompositeView. The setParent method of WrappedPlainView is actually inherited from CompositeView. As well as saving the reference to its parent (by passing it to the setParent method of View), this method also invokes a method called loadChildren, which is defined as follows:

 protected void loadChildren (ViewFactory f); 

Although there is a default implementation of this method in CompositeView, WrappedPlainView overrides it. The WrappedPlainView implementation loops over all the child elements of the paragraph element that it is associated with and creates a WrappedLine View for each of them. Each of these Views is installed as a child of the WrappedPlainView and is associated with a single content element, producing the structure shown in Figure 3-7. It is important to note that, although the basis for this mechanism, namely the loadChildren method, is part of the CompositeView class, the exact details of the implementation are a part of WrappedPlainView. Hence, other container Views can use different policies to create and install child Views as necessary. Furthermore, because this functionality is part of CompositeView and all container views are derived from CompositeView, every container has the opportunity to create the necessary child views when its setParent method is called and can do so simply by overriding the loadChildren method of CompositeView. This same mechanism is exploited by the Views used by JTextPane to create the more complex hierarchies that you'll see later in this chapter.

What about the simpler structure that was created for both JTextField and JTextArea when line wrapping was turned off? In these two cases, the process is initially identical the create method of the component's ViewFactory is called and passed the root element of the model. In the case of JTextField, this results in the creation of a FieldView object, which becomes the child of the RootView (see Figure 3-6). As before, the connection between the FieldView and RootView is made by calling the setParent method of FieldView. Unlike PlainView, however, Fieldview is not derived from CompositeView; in fact, the setParent method of FieldView is inherited from View and no method equivalent to loadChildren is called from the view setParent method. As a result, no child Views are created for the FieldView and the hierarchy remains as shown in Figure 3-6. The nonwrapping JTextArea case is almost identical. Here, the create method of the JTextArea ViewFactory creates a PlainView if line wrapping is not enabled (instead of a WrappedPlainView). PlainView is actually the superclass of FieldView and it behaves in the same way so, again, no children are created. The final View hierarchy for a JTextArea without line wrapping is the same as that shown in Figure 3-6, except that a PlainView replaces the FieldView.

The Views Used with JEditorPane and JTextPane

Because JEditorPane and JTextPane support a wider range of facilities than the components we looked at in the previous section, you won't be surprised to discover that they use a more complex View structure to provide this extra functionality. The View structure of JEditorPane is, in fact, completely customizable because you can plug any editor kit into it and you can also supply your own Document class, the attributes that JEditorPane can support are completely open-ended. As a result, the editor kits used with JEditorPane will almost certainly need to define their own custom Views that either extend the standard Views to provide extra features or implement completely new functionality. The Swing package comes with support for documents encoded in either Hypertext Markup Language (HTML) or Rich Text Format (RTF); HTML use its own implementation of Document and both types supply their own editor kit. At the time of writing, the HTML editor kit, which is discussed in the next chapter, uses several custom Views, but the RTF editor kit does not.

JTextPane is more constrained than JEditorPane, because you can only install a Document that implements the StyledDocument interface and an editor kit that extends StyledEditorKit. This doesn't, of course, mean that you can't have a JTextPane configured in such a way as to use custom attributes or new View objects in fact, you'll see two examples of this in the next section. It does mean, however, that customized JTextPanes are more likely to use the standard views than customized JEditorPanes. In this section, you'll see the View structure of JTextPane and how it differs from that of JTextArea and JTextField. We'll use this View structure to demonstrate general principles that also apply to JEditorPane, so that you'll be in a better position to properly use the Swing HTML support and, more importantly, to extend it if necessary.

A Typical JTextPane View Hierarchy

As we did in the previous section, we'll explain the JTextPane View structure by showing you the Views that correspond to an example that you saw earlier in this chapter. If you type the command

 java AdvancedSwing.Chapter3.TextPaneViews 

and wait for a short time, you'll see the View structure that corresponds to the example shown in Figure 3-6. Because this is a fairly complex example, the View structure is relatively large, so it would be a good idea to redirect the output to a file, especially if you're working in an environment (such as DOS) that doesn't provide much scrolling in the shell window. Although there is quite a lot of output, if you look carefully at it you'll find that it is pretty repetitive and that there are only a few basic constructs that are used many times over. Here, for example, is a snapshot of the start of the View structure:

 javax.swing.plaf.basic.BasicTextUI$RootView; offsets [0, 837]    javax.swing.text.BoxView; offsets [0, 837]         javax.swing.text.ParagraphView; offsets [0, 38]               javax.swing.text.ParagraphView$Row; offsets [0, 38]                      javax.swing.text.LabelView; offsets [0, 38]                             [Attributes, Styles and Style Con]         javax.swing.text.ParagraphView; offsets [38, 834]              javax.swing.text.ParagraphView$Row; offsets [38, 114]                      javax.swing.text.LabelView; offsets [38, 49]                             [The simple ]                      javax.swing.text.LabelView; offsets [49, 62]                             [PlainDocument]                      javax.swing.text.LabelView$LabelFragment;                                                offsets [62, 114]                             [ class that you saw in the previ] 

If you examine the rest of the output, you'll find that it consists of repeated passages that all look very much like this (with the exception of the first two lines, which are not repeated). The last part is slightly different, because there is an embedded component in the document we'll look at that later. Here is the element structure that corresponds to this part of the View hierarchy:

 <section>   <paragraph     bold=true     resolver=NamedStyle:MainStyle {resolver=AttributeSet,name=MainStyle,              RightIndent=16.0,size=12,family=serif,              LeftIndent=16.0,FirstLineIndent=16.0,nrefs=l}     name=Heading2     foreground=java.awt.Color[r=255,g=0,b=0]     size=16     family=serif     LeftIndent=8.0     FirstLineIndent=0.0   >     <content>       [0,38][Attributes, Styles and Style Contexts]   <paragraph     resolver=NamedStyle:MainStyle {resolver=AttributeSet,               name=MainStyle,               RightIndent=16.0,size=12,family=serif,               LeftIndent=16.0,FirstLineIndent=16.0,nrefs=l}   >     <content>       [38,49] [The simple ]     <content       name=ConstantWidth       foreground=java.awt.Color[r=0,g=255,b=0]       family=monospaced     >       [49,62] [PlainDocument]     <content>       [62,223] [ class that you saw in the previous chap ] 

As usual, the View hierarchy starts with a RootView. The first real View is, in this case, a BoxView. As you can see by looking at the offsets, the BoxView covers all the document content and, in fact, is created because of the Section element that always wraps the element structure created by DefaultStyledDocument. The job of a BoxView is to arrange its children in a straight line, either horizontally or vertically depending on the axis passed as an argument to its constructor:

 public BoxView (int axis);    // View.X_AXIS or View.Y_AXIS 

When created to map a Section element, BoxView arranges its children vertically, making sure that their alignment points are lined up on a vertical straight line. This behavior is, in fact, just like that of the Swing Box container when created with constructor BoxLayout.y_axis, and it results in all the paragraphs being laid out one above the other. The element structure next shows two paragraphs covering offsets 0 through 37, and 38 through 833; in terms of Figure 3-6, these correspond to the heading and the main body of the text, which ends just before the embedded diagram. Because the text attributes vary over the span of these two paragraphs, many different elements are used to represent their content. As you know, each paragraph has its own paragraph element, within which there is a content element for each run of text that shares the same attributes. Thus, for example, the paragraph element for the heading has a single content element, while the extract of the element structure for the main body text shown above has three, because the word "PlainDocument" is shown in red and in a monospaced font, whereas the rest of the text in this range, some before and some after this word, uses the attributes installed at the paragraph level.

As we said earlier, under normal circumstances, each element in the element hierarchy will tend to map to one View. To see how this works for JTextPane, look first at the View structure created for the first paragraph. The paragraph element causes the creation of a single ParagraphView object that maps the entire paragraph element. ParagraphView is a subclass of BoxView that lays out its children vertically; every paragraph in a JTextPane is represented by a single ParagraphView. This ParagraphView has one child of type ParagraphView.Row that also covers the whole paragraph because, in this case, the paragraph fits on a single screen line. This View, in turn, has a LabelView that again maps all of the paragraph's content in this case, the text "Attributes, Styles, and Style Contexts." This is not quite a one-to-one mapping between elements and Views! What exactly is going on here?

As we said before, a ParagraphView maps a complete paragraph and is capable, using features inherited from BoxView, of arranging its child Views one above the other. Because of this, it would be natural for each child of a ParagraphView to correspond to a single line of text as seen on the screen. However, the content elements that contain the text to be displayed do not, in general, correspond to single lines of text. In fact, the actual mapping between screen lines and element content is variable and depends on several factors, including the width of the area allocated to the text component, the font in use, and any indents assigned at the paragraph level. Because of this, there is not a one-to-one mapping between content elements and Views in JTextPane. To simplify its job, Paragraphview doesn't attempt to deal directly with Views such as LabelView that render text (or other content). Instead, it creates a set of child Views of type ParagraphView.Row, each of which maps a single line of text on the screen. Because it knows that each of these child Views exactly spans a single line, its only responsibility, after creating its children, is to make sure that they are lined up one above the other in the screen space allocated to the paragraph. You'll see how the child Views are created in "Paragraph Size, Line Layout, and Wrapping".

Even though the first paragraph has only one line of text, it still needs the overhead of a ParagraphView.Row View, because ParagraphView deals only with children of this type. In fact, if you resized the window and made it narrow enough so that the heading would no linger fit on one line, the ParagraphView would create as many ParagraphView.Row Views as it needed to manage all the lines of heading text in the new arrangement. In the case shown above, the ParagraphView.Row View has a single LabelView child that maps all the text in the corresponding content element. LabelView's job is to draw the actual text on the screen, using the correct font and the correct foreground color. It is also obliged to be able to determine the width of the text that it maps, and to translate from an offset within the part of the document that it is rendering to the location on the screen at which the text at that offset is being drawn, and vice versa.

The second paragraph has the same basic structure as the heading. Again, the paragraph element maps to a single ParagraphView, which has one ParagraphView.Row child for each actual line of text. Because this paragraph spans several lines, this particular ParagraphView has several ParagraphView.Row children; the extract above shows only the first ParagraphView.Row but, in fact, with the window width initially assigned to it and the fonts installed on my system, this paragraph actually has 10 ParagraphView.rows allocated to it. Looking only at the first line of text (in Figure 3-6), the change of font and color part way through dictates that there will be three content elements involved in this row so, because one View can only map at most one element, you would probably expect to find that the ParagraphView.Row has three LabelView children. If you look at the extract of the View hierarchy shown earlier, you will, in fact, find that this ParagraphView.Row has three children, the first two of which are LabelViews. The last child is, however, not the same it is an instance of an inner class of LabelView called LabelView. LabelFragment.

Why is the last child of a different type? As we said earlier, ParagraphView.Row can only manage a single line of text. In fact, ParagraphView.Row is derived from BoxView but, unlike ParagraphView, it arranges its children horizontally (so is created with constructor argument View.X_AXIS). As a result, it must have child Views that cover parts of the document that begin and end within the same screen line. In this case, the text that would be mapped by the third LabelView would not all fit on the line being managed by the Row, so a particular type of View, called LabelView.LabelFragment was created. This View is almost identical to LabelView, but doesn't map all of the elements that it is associated with.

Paragraph Size, Line Layout, and Wrapping

To understand exactly how and why the LabelView.LabelFragment was created, let's take a more detailed look at how ParagraphView and ParagraphView.Row create and manage their child Views. When a ParagraphView is first created, it has an associated paragraph element. At this stage, it doesn't know how much screen space will be allocated to it and, in fact, it will probably be asked for its preferred size before it is given an actual screen allocation to work with. Because working out its preferred size involves being able to measure the width of the text (and other objects) that it contains, it needs to create child Views (such as LabelView) that can do this job by working directly with the content elements in the model. ParagraphView itself, being a relatively simple container object, does not know how to perform measurement of text, so it must delegate this operation to its children.

As you know from our earlier description of JTextArea, shortly after a View is created, its setParent method is called. You also know that, for a container View derived from CompositeView, this results in the invocation of a method called loadChildren and that this method is the natural place to create a child's Views, if it requires any. This is, in fact, where ParagraphView creates a new set of Views that it will ultimately manage and, from what you have seen so far, you might expect these to be ParagraphView.Row views. However, this is not what happens. Instead, the loadChildren method loops over all the child elements of the paragraph element that the ParagraphView maps and creates the appropriate View for each element, using the text component's associated ViewFactory. In the case of the second paragraph of the example that we are looking at in this section, all the child elements are content elements. For JTextPane, the ViewFactory is implemented by the plugged-in editor kit, which, in the case of this example, is StyledEditorKit. When requested to return a View that maps a content element, it creates a LabelView.

Core Note

Throughout this section, we are assuming that a standard StyledEditorKit is plugged into the JTextPane. As noted earlier, any editor kit connected to a JTextPane must be derived from StyledEditorKit. It is, of course, possible to subclass StyledEditorKit and implement a custom ViewFactory that returns completely different custom, view objects. If you are working with a component that uses a customized editor kit and you need to enhance its View objects, you will need to look at its create method to see how it maps element types to Views, and then add your enhancements by subclassing those Views and reimplementing the create method in your own subclass of the editor factory. You'll see an example of this in "A Customized Paragraph View".



Having created a set of LabelView objects, ParagraphView stores them all in a Vector that it refers to as its layout pool. This pool of Views is ordered in the same way as the content elements that they map but they do not appear as the direct children of the ParagraphView, because the getViewCount and getView methods do not allow them to be seen from outside the ParagraphView object. Shortly after this, the text component will probably be asked for some or all of its minumum, maximum, and preferred sizes, depending on the layout manager being used by the container in which the component is mounted. The appropriate sizes for a text component depend, of course, on how its Views render the component's content, so this request is passed to the View hierarchy. In this example, the BoxView at the top of the hierarchy will be asked, using getPreferredSpan, getMinimumSpan, and getMaximumSpan, for the horizontal and vertical spans that it requires. The BoxView computes its requirements by delegating this request to each of the ParagraphViews that it contains and using the results to compute its own requirement. For example, the vertical span for a BoxView that manages its children along the y-axis can be obtained by summing the vertical spans of each child. The preferred horizontal span of the same BoxView can be obtained by taking the maximum of the preferred spans of all its children and there is a similar algorithm in each direction for the minimum and maximum sizes.

So, how does a ParagraphView compute its own preferred, maximum, and minimum sizes? This is actually something of a chicken-and-egg problem because there is more than one way to lay out the text in a paragraph when line wrapping is enabled. If you were given some text to typeset, you would start at the top left of the space allocated and work across the top row, placing the words until you ran out of space and then move down to the next line and continue until you had no more words left. This, of course, presumes that you know how wide the paragraph is and, from this, you can arrive at a value for the height of the paragraph. However, if you don't know how wide your allocated space is, there are many ways in which you could lay out the words that it contains, ranging from placing everything on a single line to placing one word per line. What actually happens is that, when ParagraphView is asked for its preferred span along the x-axis, it goes through each child in its layout pool and asks it how much horizontal space it needs (again using getPreferredSpan, and so forth), and then adds the results together. To this, it adds any paragraph insets that have been set and returns the result. This corresponds to laying the entire paragraph out on a single line of the screen. Along the y-axis, only the sum of the top and bottom paragraph insets, if any, are returned. Often, this will mean that the paragraph's preferred height will be advertised as 0. The preferred size returned by a JTextPane is, therefore, not of much practical use.

Core Note

The basic algorithm used by ParagraphView is also used by the simpler text components. However, both JTextField and JTextArea can be created with a specific number of rows and columns. These requests can, using the selected font, be transformed into a corresponding preferred size. If the number of rows and columns has been specified, the size calculated in this way is passed to the setSize method of RootView and then the getPreferredSpan method is called for each direction. This gives the Views for these components some clue as to how large a space they will have available. This is not possible for JTextPane or JEditorPane because there is no way to set a desired number of rows or columns for these components.



When the preferred size has been returned, the text component will be allocated some space in its container. Eventually, the setSize method of the RootView will be called with the exact dimensions of the text component. Now, it is necessary to divide this space between the Views that want to use it. This task is done, step-by-step, by the various view objects in the hierarchy. First, RootView calls the setSize method of the top-level View in the hierarchy below it, which will be a BoxView in this case. The BoxView at the top of the hierarchy uses the size allocated to it to create allocations for each of its child Views, which will all be ParagraphViews, and then calls the setSize method of each with its actual size allocation.

Each ParagraphView now knows how much screen space it has to work with and has to allocate space for each line of text that it contains. As described earlier, it does this by allocating an inner class ParagraphView.row object for each line of text. At this stage, the ParagraphView object knows the range of offsets that mark out the text (or other content) that it covers, but it does not know how many screen rows this will cover. In fact, this will not be known until all the rows have been individually laid out. What actually happens is that the ParagraphView creates a single ParagraphView.Row and tries to fit as much as it can into it. When the ParagraphView.Row is full, another one is created and so on, until all the content has been mapped.

Now let's look at how each ParagraphView.Row object is filled. At the outset, the ParagraphView.Row knows how wide the screen area that it can work with is and the start and end offsets of the content that it must map. In general, of course, any given ParagraphView.Row will only map the beginning of this offset range and return the rest for the next ParagraphView.Row. First, the ParagraphView.Row leaves space for the left inset of the paragraph that it is contained in, and then it gets the View for the element that it is mapping from the layout pool by using the offset of the element as the key. To determine how much space this View needs, one of two methods is called. If the View supports embedded tabs, its getTabbedSpan method is called, otherwise getPreferredSpan is used. Both of these methods have access to the content that they are rendering, because a View is created with an associated element. If the width required to render the View is less than that remaining in the row, the View is just added directly to the ParagraphView.Row and its width deducted from the available space. Then, the getBreakWeight method of the view is called and, if this returns ForcedBreakWeight (or any larger value), the ParagraphView.Row is considered filled and a new Row will be started for the View associated with the next element.

In general, however, a point will be reached at which there is insufficient space for the current View in the row being built. At this point, the View will already have been added to the ParagraphView.Row. To make maximum use of the space available, the View must be split so that as much as possible of the element that the View is mapping can be rendered in the current row. This situation is shown in Figure 3-8.

Figure 3-8. Laying out Views in a paragraph row.
graphics/03fig08.gif

Here, the first two elements have each been mapped by LabelView objects. At this point, the ParagraphView.Row is only partly full. However, as you can see by looking back at the element structure of this document, the next element contains 161 characters from offset 62 in the model. Not surprisingly, with the font size in use this won't fit in the space left to the ParagraphView.Row. However, the fact that the View wouldn't fit wasn't known when it was created and added to the layout pool, so the LabelView mapping this text already exists it is shown at the bottom of Figure 3-8. As described earlier, all views have a method called breakView that is used to create a smaller View that maps part of the same element but will fit in a given smaller space, declared as follows:

 public View breakView(int axis, int offset, float pos, float len) 

The parameters passed to this method are:

axis The axis along which the break is to occur. In this case, where a ParagraphView.Row is being populated, this will have the value View.X_AXIS.
offset The starting offset within the document of the View fragment being created. When ParagraphView is the container that is responsible for the splitting, this is always the start offset of the View being split.
pos The distance from the start of the ParagraphView.Row at which the new View will be placed. In this example, this value will be the sum of the left indentation of the paragraph plus the length of the two Views already in place.
len The amount of space into which the broken View must fit. This is the amount of space left in the current ParagraphView.Row, less the padding required for the right indentation of the paragraph, if any.

The breakview method is supposed to return another View that starts at the given offset and that will fit in the space given by len. Ideally, this View should occupy as much as possible of the remaining space in the ParagraphView.Row. In the case of LabelView, the breakView method uses a convenience method called Utilities.getBreakLocation that calculates the optimal location within the text at which to break. This method takes into account the current font and expands tabs, and then tries to break between word boundaries. The pos argument to breakView is useful when expanding tabs, because it gives the starting distance from the left of the ParagraphView.Row at which the text being rendered will start, which can be a factor in the algorithm used to calculate tab locations. The provisions for managing tabs within the Swing text components are described in "Handling Tabs".

Once the optimal offset has been determined, a LabelView.LabelFragment object is created, mapping the same element as the original, from the start offset of that View through to the offset corresponding to the location in the text at which the break should occur. This fragment is then used to fill out the Paragraphview.Row, but the View being replaced (the one that was too wide for the remaining space) remains in the layout pool. You can see the final layout of the Paragraphview.Row in Figure 3-9.

Figure 3-9. Using a View fragment to fill a row.
graphics/03fig09.gif

That takes care of the first line of text what happens now? After the last View in the ParagraphView.Row has been created, the layout code in ParagraphView gets the offset of the start of the next ParagraphView.Row by calling getEndOffset on the last View of the ParagraphView.Row just completed. This gives the correct starting offset for the first View in the new ParagraphView.Row, whether the last View was fragmented. As with the previous Paragraphview.Row, the offset is used to find a child View from the layout pool. If the last View was split, this will find the same View that was processed at the end of the last Row. In response to this situation, the ParagraphView creates another fragment from the View in the layout pool. This time, the fragment is trimmed at the front with respect to the original View that is, its start offset is different from that of the original View, but its end offset is the same. This View fragment (in this example, a LabeView.LabelFragment) is then added as the first item of the new ParagraphView.Row. At this point, the same process as detailed above is followed. In the example that we're looking at here, however, with the default window size created by the example code that we're using, there is going to be an immediate problem. Let's look at what has happened so far.

In the first ParagraphView. Row of this paragraph, there are three Views:

  • A LabelView covering offsets 38 through 48 (inclusive).

  • A LabelView covering offsets 48 through 61.

  • A LabelView. LabelFragment for offsets 62 through 121.

This last fragment maps part of the element from offsets 62 through 222, replacing the LabelView for those offsets that were originally created and placed in the layout pool. At the start of the second ParagraphView.Row, the initial offset is 122, which is the end offset of the previous View.

Core Note

Recall that the end offset stored in Views and elements is one more than the last offset actually covered by the object. However, the end offsets in the previous list and in the descriptions in this section refer to the actual last offset and so are one less than the value returned by the getEndOffset method.



This offset is used to access the layout pool, which will result in the LabelView for offsets 62 to 222 being returned again. Because the starting offset for the next View does not match that of the one in the layout pool, a new fragment is created covering offsets 62 to 222 and is added to the ParagraphView.Row. The next step is to measure the width of this view to determine how much space is left in the ParagraphView.Row. Unfortunately, the 160 characters left in this View won't fit in the width allocated to the text component, so this View, which is already a fragment, needs to be fragmented again and the resulting fragment is placed in the ParagraphView.Row instead of the original fragment. The original fragment is now no longer referenced and will be garbage collected some time later.

Now we move on to the third row. On my system, the View fragment in the second row would cover offsets 62 to 121, so the starting offset for the third row is 122. Again, this offset is used to index the layout pool and again the View for offsets 62 through 222 is returned. Once more, this View is fragmented to cover offsets 123 to 222 and placed in the new ParagraphView.Row but again it is too wide for the screen, so a second fragmentation occurs, resulting in a fragment for offsets 123 to 207 and the ParagraphView.Row is filled. For the fourth ParagraphView.Row, the start offset is 208, which is, of course, still in the bounds of the same view in the layout pool. Again, a fragment is created for offsets 208 to 222 and placed in the fourth ParagraphView.Row. This time, the remaining characters do not fill up the screen, so no further fragmentation is needed. The end offset of this View becomes the start offset for the next operation. Now, offset 223 is presented to the layout pool. This is beyond the offset of the View that we have been using for the last three rows and so a new View is extracted and the process begins again and continues until all the elements in the document have been mapped. You can see the complete result by examining the output of the TextPaneViews example that we have been using in this section.

A Few Minor Details

The description that you've just read is accurate but, for simplicity, a few small details were left out. Let's clear them up now. There are two things that we didn't fully describe:

  • How the view to be fragmented is chosen.

  • What happens if the view to be fragmented doesn't support fragmentation.

Core Note

With few exceptions, the code used in the Swing releases for Java Developer's Kit (JDK) 1.1 is identical to that used with Java 2. However, the text components sometimes have Java 2-specific code. Because there isn't room in this book to discuss both implementations, some of the low-level details that are described here are specific to the JDK 1.1 implementation. Although the details differ slightly for Java 2, the basic principles remain the same. In fact, the View hierarchies, which are the important part, are identical in JDK 1.1 and Java 2 only the precise details of text measurement and the way in which the View fragments are created changes for Java 2. This is also true of the discussion of tab handling in the next section.



Let's look at these two in order. In the last section, we implied that the View to be fragmented was always the last one added to the ParagraphView.Row and, in fact, this happened to always be the case, but it need not have been so. In fact, when a ParagraphView.Row is full, ParagraphView walks down all the Views in that ParagraphView.Row and calls its getBreakWeight method. As described earlier, larger values returned from this method indicate that it is more favorable to break that View. A View that cannot be fragmented would return a number no larger than BadBreakWeight. At the end of the ParagraphView.Row, the View with the largest return value from getBreakWeight is selected and then its breakView method is called to determine where in that View the break will actually be made. In the example used in this section, all the Views are LabelViews or LabelView fragments and they all return the same value from getBreakWeight. As a result, the last one encountered is chosen to be fragmented.

What about a View that cannot be fragmented? In most cases, no attempt will be made to fragment such a View, because it should return BadBreakWeight from getBreakWeight and another View should be chosen instead. However, if the View completely fills the ParagraphView.Row, or if all the Views in the ParagraphView.Row return BadBreakWeight and this is the last View in the ParagraphView.Row, it will be the only candidate for fragmentation and its breakView method will be called. A View that cannot be fragmented returns itself (that is, this) from breakView. As a result, no fragmentation takes place and the original View is placed in the ParagraphView.Row. This will mean that the ParagraphView.Row will be wider than the space allocated to the text component and so the component's content will be drawn outside its allocated space. Such a case is, of course, rare. It is only likely to occur if you include a Component or an Icon in the text flow, because these objects have fixed widths and they cannot be split over more than one ParagraphView.Row. This will not corrupt the area outside the component, however, because the Graphics object used to draw it is clipped at the component's boundaries.

Handling Tabs

As far as the Swing text components are concerned, there are two aspects to tab handling:

  1. How the tab positions are specified.

  2. How the tabbing is implemented by the Views.

In this section, we'll look first at the two possible ways to specify tab positions, and then we'll describe how the standard Views interpret the tabbing information.

Specifying Tab Positions

The simplest way to specify tab positions is not to bother, in which case the tabbing mechanism implemented by the Views used by JTextPane provides tabs at 72-pixel intervals. The alternative is to use a TabSet object, which allows you complete control over where tab stops are placed and what happens when a TAB character is encountered. A TabSet is a collection of TabStop objects, each of which specifies a single tab position. The association between a TabSet and its TabStops is made when the constructor is invoked:

 public void TabSet(TabStop[] tabStops) 

Once a TabSet has been created, it is immutable you cannot change the tabbing information in any way. For the tabbing to operate properly, the TabStop objects must be passed to the constructor in left-to-right order. At the time of writing, the constructor does not perform any sorting if the tab stops are not correctly ordered. TabSet is a simple class that has only a small number of methods:

 public TabStop getTab(int index) public TabStop getTabAfter(float location) public int getTabCount() public int getTabIndex(TabStop stop) public int getTabIndexAfter(float location) public String toString() 

These methods should be self-explanatory. The most useful of them is getTabAfter, which can be used to find the next TabStop given the current location of the cursor or the current insertion point while expanding tabs during View creation. Given a TabStop, the actual location of the corresponding tab can be obtained using its getPosition method.

The Tabstop class is a little more complex than TabSet. Here are its constructors and its significant methods:

 public TabStop(float position) public TabStop(float position, int align, int leader) public int getAlignment() public int getLeader() public float getPosition() 

The simplest form of TabStop just specifies a tab location in the form of an offset from the left side of the text component's visible area. As noted earlier, when creating a TabSet, the locations associated with the TabStops in the array supplied to the TabSet constructor must be in ascending order. You can, if you wish, supply two extra attributes for a TabStop alignment and leader. The possible values for the leader attribute are shown in Table 3-4.

Table 3-4. TabStop Leader Values
LEAD_NONE The space between the tab and the tab stop is left empty.
LEAD_DOTS Fill the space from the tab location to the tab stop with dots.
LEAD_HYPHENS Fill the space from the tab location to the tab stop with hyphens.
LEAD_UNDERLINE Underline the space from the tab location to the tab stop
LEAD_THICKLINE Draw a thick underline beneath the space from the tab location to the tab stop.
LEAD_EQUALS Fill the space from the tab location to the tab stop with equals signs.

If you don't specify a value for leader, LEAD_NONE is assumed. The other values are useful for creating certain types of tabular display. For example, if you want to build something that looks like a contents page, you might set a single tab stop near to the right side of the page and then set the leader value to LEAD_DOTS. Then, if the text were installed like this:

 Preface [TAB]i Chapter 1[TAB]1 Chapter 2[TAB]21 

and so on, then the resulting display might be something like this:

 Preface.............i Chapter 1...........1 Chapter 2...........21 

Core Note

This feature looks very useful but, at the time of writing, the leader value, which was initially intended to help support the rendering of RTF files, is not actually implemented. As a result, whatever leader value you supply, the effect is the same as if you had used LEAD_NONE.



The possible values for alignment are listed in Table 3-5.

Table 3-5. TabStop Alignment Values
ALIGN_LEFT Text following the tab starts at the tab stop location.
ALIGN_RIGHT Text after the tab, up to the following tab, or the next newline, is aligned to end at the location supplied in this TabStop.
ALIGN_CENTER Text after the tab, up to the following tab, or the next newline, is arranged to be centered on the location of this TabStop.
ALIGN_DECIMAL Text after the tab, up to the next tab, newline, or decimal separator, is aligned to the end location specified in this TabStop. If the text after the tab consists of or contains a number (or other text) that contains a decimal separator, the effect of using this alignment value is to place all the decimal points in a vertical straight line.
ALIGN_BAR Currently treated as ALIGN_LEFT.

At the time of writing, ALIGN_BAR, which has a specific meaning for RTF files, is treated as ALIGN_LEFT. If you don't specify an alignment value when constructing a TabStop, ALIGN_LEFT is assumed.

If you want to specify tabs using a TabSet, you must create a TabSet object and apply it to a paragraph or a range of paragraphs as a paragraph attribute or as part of a logical style. To simplify this task, the StyleConstants class provides the convenience method setTabSet:

 public static void setTabSet(MutableAttributeSet attrs,                              TabSet tabs) 

Here's an example that provides tab positions 72, 160, and 220 points from the left side of a text component:

 TabStop[] tabStops = new TabStop[] {   new TabStop(72.0f),   new TabStop(160.0f),   new TabStop(220.0f) }; TabSet tabSet = new TabSet(tabStops); StyleConstants.setTabSet(attrs, tabSet);. 
Views and Tab Expansion

Tab expansion is triggered by the presence of TAB characters in a text component's content. Even though content is usually interpreted and rendered by basic Views like LabelView, tabs are actually expanded only by an object that implements the TabExpander interface. In the Swing text package, tab expansion is not performed directly by the methods that are usually used to draw text such as Utilities.drawTabbedText (which you'll see later in this chapter). Instead, these methods are passed a reference to a TabExpander, which is used to perform the expansion as required. The TabExpander interface has only one method:

 public interface TabExpander {    public float nextTabStop(float x, int tabOffset); } 

The return value from this method is the location of the next tab stop. The first argument is the x position along the line at which the TAB character was encountered. The second argument is the offset within the document model of the TAB character. How the next tab stop is determined depends entirely on the TabExpander implementation; this interface is implemented by three classes in the standard text package, as summarized in Table 3-6.

Table 3-6. Views That Implement Tab Expansion
PlainView Used by JTextArea when line wrapping is not enabled. Provides tabs separated by a fixed distance determined by the PlainDocument.tabSizeAttribute property of the document being viewed. By default, this property is set to 8 and is measured in units of the letter "in" in the font used by the text component. If the letter "m" in the font is 9 pixels wide and the PlainDocument.tabSizeAttribute property is set to 10, then tab stops occur at 90-pixel intervals starting from the left edge of the text component. TabSets cannot, of course, be used with JTextArea, so this implementation does not handle them.
WrappedPlainView Used by JTextArea when line wrapping is enabled. Uses the same implementation as PlainView.
ParagraphView This is a complete implementation for JTextPane and JEditorPane that uses the TabSet property of the paragraph that it is mapping if it is set. If this property is not set, tabs are deemed to occur at 72-point intervals.

Basic views, such as LabelView, delegate tab expansion to their containing ParagraphView. Because JTextField and JPasswordField both use Views derived from Plainview, tab expansion in these components is the same as in JTextArea. If tabs occur in text being rendered by a View that does not support tab expansion, the TAB character is replaced by a space.

Handling Embedded Icons and Components

Icons and Components in documents are mapped by two dedicated views IconView and ComponentView, both of which are relatively simple. The main job of IconView is to arrange for the Icon to draw itself into the area allocated to it within the text component. Because the IconView inherits from its base class (View) a paint method that receives the width and height of the allocated area and a Graphics object to draw with, it is a simple matter to map this call to a call on the Icon's paintIcon method. The only other important job that the IconView performs is to return the preferred span of the object in each direction, by asking the Icon how much space it needs.

ComponentView is much the same as IconView except that it manages an arbitrary Component (almost certainly a JComponent) instead of an Icon. Most of the view methods that ComponentView directly implements are delegated directly to the Component. Table 3-7 summarizes the implementation of these methods.

Table 3-7. Implementation of View Methods by ComponentView
Paint Sizes the Component to the area allocated to the ComponentView and makes it visible if it is not. This will make the Component paint itself. If it was already visible, the Component will get its own paint call automatically.
getPreferredSpan getMinimumSpan getMaximumSpan These three methods use the getPreferredSize, getMinimumSize, and getMaximumSize methods of the Component and return the width or height depending on the axis argument.
getAlignment Delegates to the Component methods getAlignmentx or getAlignmentY depending on the axis argument.
setSize Changes the Component's size to match the supplied width and height. This is always done in the AWT event thread, even if this method is invoked in a different thread.
SetParent This method adds the Component to the text component that it is a part of, establishing the AWT parent/child Container/Component relationship. If the Component has already been added to a Container, it is first removed from that Container. These operations are always performed in the AWT event thread.

A Customized Paragraph View

After a lengthy and somewhat academic discussion of Views, it's time to put what we've learned to some practical use by creating a couple of Views that you can use to enhance your applications. The first example we're going to look at is a custom ParagraphView that you can use with JTextPane to highlight text that needs to stand out from its surroundings. If you use only the standard features of JTextPane, your options for highlighting text are limited to changing the font, changing the color, or using underlining. Here, we'll show you how to make a paragraph stand out by changing its background color and adding a border of your choosing around the paragraph. In fact, with the implementation that you'll see here, you'll be able to use these effects separately or together, according to your requirements. Before we start looking at how to implement this feature, let's look at what it looks like when applied to the JTextPane example you saw in the last chapter (Figure 2-3). That example consisted of a heading and a single paragraph of text to which limited formatting had been applied at the paragraph and character level to emphasize specific words. If you type the command

 java AdvancedSwing.Chapter3.ExtendedParagraphExample 

you'll see the same text, but now the main text is surrounded by an etched border and the background is filled with gray, as shown in Figure 3-10.

Figure 3-10. A custom ParagraphView.
graphics/03fig10.gif

While you've got this example running, resize the window in various ways and observe what happens to the paragraph highlighting: Whether you make the window narrower or wider, taller or shorter, the border and the color fill reshape themselves so that they always neatly fit around the text. Notice also that the colored area is indented away from both sides of the component and that there is a space between the border and the text that it wraps. As you'll see, all of this is provided by the View that we're going to create and it can all be controlled by applying the appropriate attributes to the paragraph.

Anatomy of the Extended ParagraphView

Before looking at the new View, let's review the structure of the existing ParagraphView and how the attributes that you can assign at the paragraph level are interpreted. Figure 3-11 shows a standard ParagraphView. The main function of ParagraphView is to organize the text that it contains into rows and to draw them onto the surface of the text component. The shape of the paragraph is controlled in part by the width of the component, but there are a few attributes that you can use to modify the behavior slightly.

Figure 3-11. ParagraphView and its attributes.
graphics/03fig11.gif

The SpaceAbove and SpaceBelow attributes determine how much room is left above and below the paragraph and, similarly, the LeftIndent and RightIndent attributes control the left and right margins. Finally, the first line of text in the paragraph may have extra indentation, specified using FirstLineIndent. You've seen these attributes in use in the examples shown earlier in this chapter. In effect, these attributes create an inset area around the paragraph text that is under the management of the ParagraphView but is not available for use by the paragraph's child Views. This is, in itself, very much like having a border around the paragraph contents. Because the ParagraphView is able to create this blank area, you can deduce without even looking at its source code that it should be fairly simple to make it draw a border around itself. In fact, the only real problem to be solved as far as rendering the modified View is concerned is actually drawing the border, because ParagraphView is already able to ensure that the text that it contains does not stretch all the way to the edges of its allocated space. If this were not true, we would have a much harder job. In fact, as you'll see when we look at the implementation later in this section, the ability to specify insets around a paragraph is inherited by ParagraphView from CompositeView.

Given that we can arrange to draw a border around the paragraph, there are two ways in which this could be done. Figure 3-12 shows one possible choice. In this diagram, we've added a border around the outside of the area allocated to the paragraph and also painted the paragraphs background (another feature that will be added by our custom View). With this implementation, the paragraph insets are interpreted as the distance of the paragraph text from the inside of the border.

Figure 3-12. A ParagraphView with a border: choice 1.
graphics/03fig12.gif

This is, of course, a perfectly acceptable way to provide this feature, but it isn't particularly pleasing. The main problem with this approach is that the paragraph background is filled in all the way to the edge of the text component there's no margin any more. Under some circumstances, this may be what you want, but because this implementation gives you no choice, it's not as good as the alternative, which is shown in Figure 3-13.

Figure 3-13. A ParagraphView with a border: choice 2.
graphics/03fig13.gif

In this case, the paragraph insets are used to create a margin around the outside of both the border and the painted area. If you want, you can remove these insets by setting the associated paragraph attributes to zero and you'll get back to something like Figure 3-12. In most cases, though, you'll probably want insets around the paragraph so that it is flush with the rest of the content. Given that the paragraph insets are now applied around the outside of the border, why does Figure 3-13 show a blank area between the border and the text? This blank space is a feature of the border and it is totally under your control. The border shown in Figure 3-13 is actually a compound border, composed of a lined border on the outside and an empty border on the inside. The empty border creates the space between the edge of the paragraph and the text. As you'll see, you can install any kind of border around our extended paragraph, so the exact spacing, if any, and the relationship between the border and the spaced area, is entirely up to you. As a result, you could, if you wanted, arrange for the text to be flush against the lined part of the border and add extra space outside. The external space is still part of the paragraph, so it would have its background filled if a filled background is specified.

Incidentally, it's worth noting that you can't use the theoretical implementation shown in Figure 3-12 to produce the effect shown in Figure 3-13 by adding an empty border on the outside of the lined border, because the filled area extends to the edges of the paragraph which, in Figure 3-12, extend to the limits of the component, so the background fill would reach outside the border area. Although it could be considered a matter of taste, we'll implement the alternative shown in Figure 3-13.

Specifying the Border and the Background Color

Our new paragraph is going to be able do two things that existing ones don't:

  1. Optionally fill its background with a specified color.

  2. Optionally draw an arbitrary border around itself.

By now, it should be evident that both of these things should be specified as paragraph attributes. If you look back to Table 2-1 in the last chapter, you'll see that there is already an attribute defined for background color, but it is a character attribute. Because we want to apply a background color at the paragraph level, we'll need to create a new paragraph-level attribute.

We will also need a new attribute to specify the border that we want to be drawn around the paragraph. The existing attributes are all defined in the StyleConstants class; because we can't add newm attributes to this class, we do the next best thing and create a derived class called ExtendedStyleConstants to define our new attributes. Listing 3-2 shows the implementation of this class.

Core Note

We could cheat and use the existing character-level Background attribute as a paragraph attribute, because none of the Views in the standard Swing text package fill their background. This would be a mistake, however, because you might want to implement a View that could change the background of a range of characters. If you applied this attribute as a character attribute to a run of characters in a paragraph that also used the Background attribute at the paragraph level, the character attribute would override the one at the paragraph level for the length of that character run. This usually would not be a problem. For example, if you had a paragraph with the Background attribute set to yellow within which there was a character run with Background set to black, the black background would override the yellow paragraph background for that part of the paragraph. Is this a problem? Not if you're only going to use solid colors, because you could say that, logically, the yellow background is painted first and gets over-painted with black. So what does it matter that the yellow background was lost? This reasoning is correct in JDK 1.1. However, in Java 2, the java2D package gives you the possibility of using colors that are not solid in other words, you could conceive of the possibility of a background applied to the character run that allowed the paragraph background color to be partly visible through gaps. This isn't possible with the current definition of the StyleConstants setBackground method, which only accepts a solid color, but nothing prevents you from adding another method, in a class like ExtendedStyleConstants, that would overload setBackground to accept a Java2D textured background as the Background attribute and then implementing the View support for it In this case, you would certainly want the paragraph background color to be defined by a different attribute.



Listing 3-2 Declaring a New Attribute
 package AdvancedSwing.Chapter3; import javax.swing.text.*; import javax.swing.border.Border; import java.awt.Color; public class ExtendedStyleConstants {    public ExtendedStyleConstants(String name) {       this.name = name;    }    public String toString() {       return name;    }    /**     * The border to be used for a paragraph.     * Type is javax.swing.border.Border     */    public static final Object ParagraphBorder =               ExtendedParagraphConstants.ParagraphBorder;    /**     * The background to be used for a paragraph.     * Type is java.awt.Color     */    public static final Object ParagraphBackground =              ExtendedParagraphConstants.ParagraphBackground;    /* Adds the border attribute */    public static void setParagraphBorder                             (MutableAttributeSet a, Border b) {       a.addAttribute(ParagraphBorder, b);    }    /* Gets the border attribute */    public static Border getParagraphBorder(AttributeSet a) {       return (Border)a.getAttribute(ParagraphBorder);    }    /* Adds the paragraph background attribute */    public static void setParagraphBackground                             (MutableAttributeSet a, Color c) {       a.addAttribute(ParagraphBackground, c);    }    /* Gets the paragraph background attribute */    public static Color getParagraphBackground(AttributeSet a) {       return (Color)a.getAttribute(ParagraphBackground);    }    /* A typesafe collection of extended paragraph attributes */    public static class ExtendedParagraphConstants                           extends ExtendedStyleConstants       implements AttributeSet.ParagraphAttribute {       /**        * The paragraph border attribute.        */       public static final Object ParagraphBorder =                           new ExtendedParagraphConstants(                           "ParagraphBorder");      /**      * The paragraph background attribute.      */     public static final Object ParagraphBackground =                         new ExtendedParagraphConstants(                         "ParagraphBackground");      private ExtendedParagraphConstants(String name) {         super(name);      }    }    protected String name; // Name of an attribute } 

The ExtendedStyleConstants class, like StyleConstants, is a base class within which we can define new attributes in a type-safe way. An ExtendedStyleConstants attribute has a name that can be used to recognize it when you dump the model of a document to which it is applied; like those of all the standard attributes, this name has no other direct use within the Swing text framework. The border attribute is a constant instance of the inner class ExtendedParagraphConstants, accessible via the name ExtendedStyleConstants. ParagraphBorder, by analogy to all the other Swing text attributes. The ExtendedParagraphConstants class extends ExtendedStyleConstants and implements the interface AttributeSet.ParagraphAttributes. The first of these two circumstances makes ExtendedParagraphConstants recognizable (to any code that wishes to check) as an instance of ExtendedStyleConstants and the second indicates that it is an attribute that applies at the paragraph level. The ParagraphBackground attribute is declared in the same way.

Core Note

This roundabout way of declaring attributes looks a little convoluted and slightly cumbersome. The fact is that you don't actually have to go to all this trouble to create new attributes, because nothing in the Swing text package checks that the attributes that are used are actually of the required generic types, unless you use the styleConstants convenience methods, which require the specific type to be correct There is no explicit check, for example, that attributes applied at the paragraph level implement AttributeSet.ParagraphAttributes. Nevertheless, for consistency, we follow the same pattern used by styleConstants to create these attributes.



As well as defining the attributes, methods that allow them to be added to or retrieved from an attribute set are required. Like the similar methods in StyleConstants, these are not strictly necessary; they do, however, make code that uses these attributes slightly more readable as well as providing a measure of type safety. The implementation of all four methods is trivial adding an attribute maps directly to the MutableAttributeSet addAttribute method, while the accessor methods use getAttribute. If an accessor method is used to retrieve the paragraph border or background color when it hasn't been set, null is returned. Because applying either of these attributes to a paragraph is optional (and, moreover, using one does not imply using the other), it is not an error for either getParagraphBackground or getParagraphBorder to return null.

A New ViewFactory

Having defined a couple of new attributes, the next problem is to somehow arrange for them to affect the way in which the paragraph to which they are applied is rendered. To change the way in which paragraphs are drawn, you need to use a custom View that understands the new attributes and arranges for an instance of it to be created for every paragraph in the document. As you know, the mapping between element types in the model and views is determined by the text components viewFactory, which is determined either by the plugged-in editor kit or the component's UI delegate. In the case of JTextPane, the editor kit supplies the viewFactory, so to arrange for a new View to be used to map paragraph elements, we have to create a new editor kit with a suitable ViewFactory.

JTextPane requires that its editor kit be derived from StyledEditorKit. Because of this, our custom editor kit will be created by extending StyledEditorKit. Because the only enhancement we need to make is the viewFactory, we need only to override the getviewFactory method, which is called from the JTextPanes UI delegate. The implementation of the new editor kit, called ExtendedStyledEditorKit, is shown in Listing 3-3.

Listing 3-3 A Customized Editor Kit
 package AdvancedSwing.Chapter3; import javax.swing.*; import javax.swing.text.*; public class ExtendedStyledEditorKit extends                                         StyledEditorKit {    public Object clone() {       return new ExtendedStyledEditorKit();    }    public ViewFactory getviewFactory() {       return defaultFactory;    }    /* The extended view factory */    static class ExtendedStyledViewFactory implements                                               ViewFactory {       public View create(Element elem) {          String elementName = elem.getName();          if (elementName != null) {             if (elementName.equals(                       AbstractDocument.ParagraphElementName)) {                   return new ExtendedParagraphview(elem);               }            }            // Delegate others to StyledEditorKit             return styledEditorKitFactory.create(elem);        }    }    private static final ViewFactory StyledEditorKitFactory =                    (new StyledEditorKit()).getViewFactory();    private static final ViewFactory defaultFactory =                    new ExtendedStyledViewFactory(); } 

The new ViewFactory is declared as a static inner class of our custom editor kit and a single instance is created when the class is first loaded. The getViewFactory method simply returns a reference to this shared instance. The ViewFactory only has a single method, which creates the Views for each type of element in the associated document. In the create method, we look at the element name to determine the type of View to return. If the element represents a paragraph, the appropriate thing to return is an instance of our new View class, ExtendedParagraphview, the implementation of which you'll see later. For all the other element types, we want to delegate to the ViewFactory for StyledEditorKit rather than copy all its code into our own create method. To delegate to StyledEditorKit's ViewFactory, we get a reference to it in our static initializer. However, because the getViewFactory method is not static, we need to create a StyledEditorKit object to invoke it. The overhead of this is small and is incurred only once in the lifetime of any application that uses our editor kit. Moreover, the StyledEditorKit that we create in the static initializer will be garbage collected some time after the initializer completes.

Before looking at the implementation of ExtendedParagraphview, it's worth considering whether an alternative ViewFactory implementation is possible. As it stands now, every paragraph in the document has an ExtendedParagraphview associated with it, whether it uses either of our new attributes. As an alternative, perhaps we could have the create method look for these attributes and return an ExtendedParagraphview for a paragraph in which they are present and an ordinary Paragraphview otherwise. The problem with this is what should be done if the paragraph border or paragraph background attributes are added after the View structure has been created. When this happens, it would be necessary to replace the Paragraphview by an ExtendedParagraphview. However, if applying these attributes is the only change made to the document (that is, the actual document content does not change at the same time), the View structure is not automatically rebuilt and, as a result, the affected paragraph would continue to be rendered by a Paragraphview. Extra code would need to be added to react to certain attribute changes by installing an ExtendedParagraphview, complicating the implementation.

Implementing the New Paragraphview

The last piece we need to implement is the new Paragraphview. Because this class has to do everything that the existing View does, we create it by extending Paragraphview. We need to add two features:

  • Filling the background with the paragraph background color if the paragraph has the ParagraphBackground attribute applied to it.

  • Drawing a border around the paragraph if the ParagraphBorder attribute has been used.

As we said earlier, and as Figure 3-13 shows, the background fill will be applied to the region inside the paragraph insets. To determine the size of the area to be filled, we need to get the insets that have been applied to the paragraph. These insets are actually held by Compositeview, one of the superclasses of ExtendedParagraphview, which provides the methods getLeftInset, getRightInset, getTopInset, and getBottomlnset to allow access to them. The inset values are set when the View is created by calling the protected method setPropertiesFromAttributes, which obtains any values that it needs from the paragraph attributes attached to the element that the View maps and caches them internally for faster access. This method is subsequently called only when something happens that would invalidate the cached values, such as the application of changed attributes. Because ExtendedParagraphview has two extra attributes, it makes sense to override setPropertiesFromAttributes to cache the values of these two attributes at the same time as the paragraph insets are being set.

The other method of ParagraphView that we'll need to override is paint. The paint method of a container View usually does not actually paint anything; like the paint method of the AWT Container class, its normal job is just to arrange for its child Views to draw themselves by invoking their paint methods. In the case of ParagraphView, its usual function is to loop over all the Row views that it contains and have them draw themselves directly onto their assigned part of the area assigned to the paragraph. This results in each line of text being drawn on the screen, from the top of the paragraph to the bottom. The ExtendedParagraphView paint method still needs to do this, of course, but it also needs to fill the appropriate rectangular area of the paragraph with the background color if it is set and also draw the paragraph border, if there is one. After doing this, it can delegate the job of drawing the paragraph content to the usual ParagraphView paint method.

Because our View is such a simple one, we don't need to override any other methods. You can see the implementation of the ExtendedParagraphView class in Listing 3-4.

Listing 3-4 A Custom ParagraphView
 package AdvancedSwing.Chapter3; import javax.swing.*; import javax.swing.text.*; import javax.swing.border.*; import java.awt.*; public class ExtendedParagraphView extends ParagraphView {    public ExtendedParagraphView(Element elem) {       super(elem);    }    // Override ParagraphView methods    protected void setPropertiesFromAttributes() {       AttributeSet attr = getAttributes();       if (attr != null) {          super.setPropertiesFromAttributes();          paraInsets = new Insets(getTopInset(),                       getLeftInset(), getBottomInset(),                       getRightInset());        border =                 ExtendedStyleConstants.getParagraphBorder(attr);        bgColor =            ExtendedStyleConstants.getParagraphBackground(attr);        if (bgColor != null && border == null) {            // Provide a small margin if the background            // is being filled and there is no border            border = smallBorder;        }            if (border != null) {               Insets borderInsets = border.getBorderInsets(                                     getContainer());               setInsets((short)(paraInsets.top +                  borderInsets.top),(short)(paraInsets.left +                  borderInsets.left),(short)(paraInsets.bottom +                 borderInsets.bottom),(short)(paraInsets.right +                  borderInsets.right));            }        } } public void paint(Graphics g, Shape a) {    Container comp = getContainer();    Rectangle alloc = new Rectangle(a.getBounds());    alloc.x += paraInsets.left;    alloc.y += paraInsets.top;    alloc.width -= paraInsets.left + paraInsets.right;    alloc.height -= paraInsets.top + paraInsets.bottom;    if (bgColor != null) {       Color origColor = g.getColor();       g.setColor(bgColor);       g.fillRect(alloc.x, alloc.y, alloc.width,                  alloc.height);       g.setColor(origColor);    }    if (border != null) {       // Paint the border       border.paintBorder(comp, g, alloc.x, alloc.y,                          alloc.width, alloc.height);    }    super.paint(g, a); // Note: pass ORIGINAL allocation }     // Attribute cache     protected Color bgColor;                   // Background color, or null for transparent.     protected Border border; // Border, or null for no border     protected Insets paraInsets; // Original paragraph insets     protected static final Border smallBorder =                     BorderFactory.createEmptyBorder(2, 2, 2, 2); } 
Setting the Paragraph Insets

The first of the two overridden methods well look at is setPropertiesFromAttributes, which is called when the View is created and whenever any change to the attributes occurs. The first thing that this method does is obtain the paragraph attributes that apply to it by calling the getAttributes method. This method is implemented by the View class (see Table 3-2) and just returns the attributes associated with the paragraph element that the ExtendedParagraphView maps. Of course, if this method were called within a View that was associated with a content element, it would return the attributes associated with the character run that the element represents, not those assigned to the surrounding paragraph. After obtaining the paragraph's attribute set, it then invokes the setPropertiesFromAttributes method on its superclass, which allows the ParagraphView to extract and cache the following paragraph attributes:

  • SpaceAbove

  • SpaceBelow

  • LeftIndent

  • RightIndent

  • FirstLineIndent

  • Alignment

  • LineSpacing

The first four of these attributes determine the space to be left around the filled area and the outside of the border, if it exists. The ParagraphView obtains and saves these values by calling the Compositeview setParagraphInsets method, which stores the insets in private instance members of the Compositeview superclass of the ExtendedParagraphView object. Although these attributes are held in private members, you can still get access to them using the Compositeview getTopInset, getBottomInset (and so forth) methods, so it is a simple matter to work out how much space to leave around the outside when filling the paragraph background in the paint method. Unfortunately, if we are going to use a paragraph border, life is not so simple. The issue with this is that the border will take up space inside the area allocated for the paragraph insets. The code that lays out the text of the paragraph by creating Row objects computes the space available by subtracting the sum of the left and right insets (obtained using the CompositeView getLeftInset and getRightInset methods) from the horizontal space allocation. If there is a border present, however, the left and right border insets needs to be taken account of to avoid drawing text over the border. The simplest way to obtain this effect is to add the border insets to the initial paragraph insets, so that code that calls methods like getLeftInset and getRightInset will retrieve values that take all the unusable space into account. This is exactly what the setPropertiesFromAttributes method in Listing 3-4 does. After obtaining the paragraph insets, it uses the ExtendedstyleConstants convenience methods that were shown in Listing 3-2 to retrieve the paragraph background and paragraph border attributes. If a paragraph border has been defined for this paragraph, its size is obtained by calling its getBorderInsets method and new paragraph insets are set for the paragraph using the CompositeView setInsets method, adding together the existing paragraph insets and those of the border. This step ensures that the text will be properly laid out inside the border area.

There are two points of interest in this code that should be carefully noted. First, note that we save a copy of the original paragraph insets in the instance variable paraInsets. This is necessary because the paint method needs to fill the background and place the border with respect to the original paragraph insets, which can no longer be obtained from the CompositeView methods that were used earlier. These insets could be obtained from the paragraph attributes, but it is faster to cache them in this way. The second point of interest is a small matter of presentation. If the paragraph is configured with background fill but no border, the text within the paragraph would be drawn from the left edge of the filled area, which looks ugly. To improve matters, if a background fill is specified but no border, we add a small border inset to move the text away from the edges of the filled area. This doesn't cause an empty border to be drawn; instead it works by giving the code that lays out the Rows a restricted space to work in that does not include a small region just inside the filled area. The effect is, of course, the same as if a small empty border had been requested. If a paragraph border is used, we don't supply any extra margin because the programmer is expected to create a border that includes any margins that the programmer deems fit, as you'll see shortly in the example code that demonstrates ExtendedParagraphview.

Painting the Paragraph

Once the paragraph insets have been dealt with, the paint method is simple to implement. The Shape argument describes the area allocated to the paragraph. The bounding rectangle of this area is retrieved by using the getBounds method and, because we are going to change the position and size of this rectangle, we create a new one to avoid any side effects that might be caused by changing the original.

Core Note

The JDK documentation does not specify whether the shape getBounds method returns a Rectangle that can be mutated. Because Shape is an interface that could be implemented by many classes, it isn't safe to assume that you get a private Rectangle each time you call getBounds. Even if you find this to be the case for one implementation of Shape, it may not be true for another. Hence, the safest approach is to create your own Rectangle.



Having obtained the bounding rectangle of the paragraph, the next step is to use the original paragraph insets that were saved in the setPropertiesFromAttributes and use them to adjust the bounding rectangle so that it bounds the space to be filled with background color and which will mark the outer boundaries of the border. If a paragraph background color has been specified, the Graphics fillRect method is called to fill the area inside the paragraph and border insets. Similarly, if a paragraph border has been configured, it is drawn by calling its paintBorder method and passing it a description of the outer boundary of the paragraph area, a Graphics object to draw with, and a reference to the text component, obtained using the View getContainer method. Some borders use this argument to obtain properties (such as the colors and fonts) from the component that they are drawing on so that you don't need to specify border drawing colors every time you create a border. With JTextPane, however, this is not always a useful property, because the background color of the JTextPane is not always the one relevant to the border. Indeed, in this case, it would not always be of much use, because if the border were being drawn on a filled area, it would need to use colors that contrast with the fill color, not with the background color of the JTextPane. In cases like this, you should create the border with suitable colors rather than allow it to choose its colors when it is drawn.

Having filled the background and drawn the border, the last step is to call the superclass paint method, which does the usual work of rendering the paragraphs text. Note that the Shape passed to this method is the original Shape, which corresponds to the complete paragraph space allocation and is not restricted to the inside of the border. This is the correct thing to do, because the ParagraphView paint method takes into account the paragraph insets (which now include the border) when drawing and so will not draw over the border or outside the filled area.

Using the ExtendedParagraphView

At the beginning of this section, you saw an example that shows you how a paragraph rendered by the ExtendedParagraphview looks (see Figure 3-10). This example was based on the example code that was developed earlier in this chapter, so we're not going to repeat all the code here (and you can find it in the sample code on the CD-ROM that accompanies this book). In this example, we create a new style that incorporates both a background fill and a paragraph border, like this:

 // Create and add the extended para styles Style paraStyle = sc.addStyle(paraStyleName, null); Color bgColor = Color.gray; ExtendedStyleConstants.setParagraphBackground(paraStyle,                                               bgColor); ExtendedStyleConstants.setParagraphBorder(paraStyle,          BorderFactory.createCompoundBorder(          BorderFactory.createEmptyBorder(2, 2, 2, 2),          BorderFactory.createCompoundBorder(          BorderFactory.createEtchedBorder(          bgColor.brighter(), bgColor.darker()),          BorderFactory.createEmptyBorder(4, 4, 4, 4)))); 

This code uses the setParagraphBackground and setParagraphBorder convenience methods of ExtendedStyleConstants to add the relevant attributes to a new style. In this case, the paragraph will be filled in gray. The border is slightly more complex. Fundamentally, this is an etched border that is drawn using colors that are slightly lighter and darker than the background fill applied to the paragraph. As noted earlier, many of the borders in the Swing package (including the etched border) can deduce suitable drawing colors when they are painting themselves by referring directly to the component that they are being drawn on. This is, of course, an example in which this doesn't work because the JTextPane has a white background and a black foreground, whereas the filled area is gray. For this reason, the etched border is given specific colors to use.

The configured border is, in fact, two compound borders nested one inside the other. On the outside, there is a 2-pixel wide empty border that leaves a small gap between the etch and the boundary of the colored area. This is followed by the etched border. Finally, inside the etched area, we add another empty border, which, in this case, leaves a 4-pixel space between the drawn part of the border and the text that it surrounds. If this had not been added, the left side of text would be abutted to the border. The border insets obtained in the setPropertiesFromAttributes method are those of the compound border, which is the sum of the insets of all three borders.

The only other important change needed to make the extended paragraph attributes usable is to change the editor kit that the JTextPane uses by adding the following code (highlighted in bold) in the main method:

 // Create the StyleContext, the document and the pane final StyleContext sc = new StyleContext(); final DefaultStyledDocument doc =                                new DefaultStyledDocument(sc); final JTextPane pane = new JTextPane(doc); pane.setEditorKit(new ExtendedStyledEditorKit( )); 

With this change in place, our modified viewFactory will be used and will create an ExtendedParagraphview object for every paragraph in the document, instead of a Paragraphview. Finally, the border and background fill are applied to the main text by specifying the name that corresponds to the paragraph style show previously in the Paragraph object for the main text:

 public static final Paragraph[] content = new Paragraph[] {    new Paragraph(heading2StyleName, new Run[] {         new Run(null, "Attributes, Styles and Style Contexts")    }) ,    new Paragraph(paraStyleName, new Run[] {         new Run(null, "The simple "),         new Run(charStyleName, "PlainDocument"), 

 

 



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

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