Tabbing
in Text Documents
By Scott Violet
This article gives an overview of how the Swing text package handles
tabs. A brief review of the dclasses and interfaces related to tabs
is followed by a discussion of how ParagraphView and LabelView interact
when these interfaces are used to implement tabs. This material
may be useful to developers who would like to to have a better understanding
of how tabs work in Swing text, as well as to developers who want
to add tab support to custom View implementations. To understand
this article, you should have an understanding of the normal layout
process that Views go through.
This article covers the following major topics:
TabStop,
a class defined in javax.swing.text, encapsulates a single
tab stop. A tab stop is defined by a location from the margin, alignment,
and a leader (a character to use instead of white space).
TabStop is immutable; that is, once it has been created it can
not be changed. Instances of TabStop are usually contained in a
TabSet.
TabSet,
another class defined in javax.swing.text, encapsulates a
set of TabStops. A TabSet is instantiated with an array of TabStops.
Once instantiated, a TabSet cannot change. Besides holding an array
of TabStop's, TabSet adds convenience methods for locating a TabStop
by location.
Example: Creating a TabSet
Here is an example of how to create a TabSet containing
a left justified TabStop at location 72 and a centered TabStop at
location 144:
TabStop[] tabs = new TabStop[2];
tabs[0] = new TabStop(72.0f, TabStop.ALIGN_LEFT,
TabStop.LEAD_NONE);
tabs[1] = new TabStop(144.0f, TabStop.ALIGN_CENTER,
TabStop.LEAD_NONE);
TabSet tabSet = new TabSet(tabs);
Once a TabSet is instantiated, one useful operation
is to locate a TabStop after a specific location. You can use the
method getTabAfter()
to achieve this result.
TabSets are usually set as an attribute of a paragraph
Element. To set a TabSet as an attribute in a paragraph Elements
attributes, you could execute a code sequence such as this:
TabSet tabs = new TabSet(...);
SimpleAttributeSet attributes = new SimpleAttributeSet();
StyleConstants.setTabSet(attributes, tabs);
styledDocument.setParagraphAttributes(offset,
length, attributes, replace);
The TabExpander
interface consists of the single method nextTabStop. It is used
to find the location for a character which you want to place after
a tab.
Two methods define the TabableView
interfaces:
float getTabbedSpan(float x, TabExpander e);
float getPartialSpan(int p0, int p1);
The getTabbedSpan()
method is the equivalent of the View method getPreferredSpan()
with an implied axis along which text is layed out -- usually X_AXIS.
Some Views, such as ParagraphView, check to see whether a View implements
TabableView -- and, if it does, the getTabbedSpan()
method is invoked instead of the getPreferredSpan()
method to determine the View's preferred span. The getTabbedSpan()
method is invoked with a TabExpander. If the receiver encounters
a tab when determining its preferred span, it can then call back
to the TabExpander to determine where to place text after the tab.
As with getPreferredSpan(),
the getPartialSpan()
method is used to determine the preferred span, on the axis along
which text is layed out, of a particular region of the document.
The receiver can assume that a tab will not be located in the region
specified. The getPartialSpan()
method is usually called to help determine where a TabStop is to
be placed for alignments other than TabStop.ALIGN_LEFT.
ParagraphView
is a View that is used to represent paragraph Elements in styled
text. ParagraphView implements TabExpander.
When ParagraphView is laying out its child Views,
it first checks to determine whether the child View implements TabableView.
If the child View does implement TabableView, getTabbedSpan()
is invoked instead of getPreferredSpan().
This provides a way for the child View to get a handle to a TabExpander.
If a child View encounters a tab while it is determining its size,
it can call back to ParagraphView (via the TabExpander interface)
for the next location at which it should place text.
When ParagraphView is messaged with nextTabStop,
it first checks to determinme if there is a TabSet in its attribute.
(If the ParagraphView is not left aligned, nextTabStop returns the
current position offset by 10 points).
If there is no associated TabSet, Swing's default
behavior is to assume that there is a tab every 72 points. If there
is an associated TabSet, it is messaged with getTabAfter()
to determine the next TabStop. If there is no next TabStop, the
current location is offset by 5 points. If there is an associated
TabStop and it is left-aligned, its location is returned.
If the TabStop is not left-aligned, the text content
is searched for the next break character (which will be either a
tab or period, based on the tab type). Then the child Views in the
region between the current tab position and the next break character
position are messaged with either getPartialSpan()
or getPreferredSpan().
LabelView
-- a View that is used to represent leaf Elements in styled text
-- implements the TabableView interface. If getTabbedSpan()
has been invoked once on a LabelView, that LabelView is then able
to correctly position the tabs based on the previously passed in
TabExpander. And since LabelView is almost always contained in a
ParagraphView, styled text is able to correctly deal with tabs.
Let's take a closer look at this process. Figure
1 shows an Element structure containing two paragraphs. The first
paragraph has one style with a tab at Offset 2.
The second paragraph has two styles. The first of
these styles has a tab at Offset 8.
|
Figure 1
An Element structure containing tabs |
Figure 2 shows how the Elements shown in Figure 1
would map to Views. (For illustrative purposes, I have left out
BasicTextUI.RootView and ParagraphView.Row. Also, I am assuming
that no horizontal wrapping has resulted.) The string in parenthesis
gives the name of the Element which the View represents.
|
Figure 2
Mapping Elements to Views
|
When getPreferredSpan(View.X_AXIS)
is invoked on ParagraphView 1, it must forward to its child View
(or Views) -- which, in this case, is LabelView 1. Before ParagraphView
1 forwards getPreferredSpan()
to LabelView 1, it first checks to see if it implements TabableView.
In this case it does, so getTabbedSpan()
is invoked instead of getPreferredSpan().
When LabelView 1 receives getTabbedSpan(),
it iterates through the characters it represents, accumulating the
size. When LabelView 1 hits the tab character at Offset 2, it invokes
nextTabStop on the passed-in TabExpander (in this case, ParagraphView
1). When ParagraphView 1 receives nextTabStop(),
it checks its associated tabs (as outlined above) and returns the
appropriate location. LabelView 1 is then able to return its preferred
span.
In a similar way, LabelView 2 is messaged with getTabbedSpan().
Let's assume that when ParagraphView 2 is messaged with nextTabStop(x,
8), it determines that the next appropriate TabStop is a
centered TabStop. In order for ParagraphView 2 to calculate the
location for a centered tab correctly, it must look ahead in the
text of the document, stopping when a tab is found or when the endOffset
of ParagraphView 2 is encountered (Offset 12).
In this case, no tab is found before the end offset.
So the next step is for ParagraphView 2 to iterate over all its
child Views in between the tab offset passed in (Offset 8) and the
offset just determined (Offset 12), accumulating the return value
by invoking either getPartialSpan()
(if the View implements TabableView) or getPreferredSpan().
In this case, getPartialSpan()
is invoked because both Views in the region implement TabableView.
To illustrate, assume that 50 is accumulated and that the TabSet
is located at Offset 100. Because this is a centered tab, 75 (100
- 50 / 2) is returned (assuming that ParagraphView 2's left margin
is at 0).
One user-interface widget that styled text editors
often provide is a ruler. One useful feature of a ruler is
that it allows the user to inspect the tabs in the current paragraph.
|
Figure 3
A ruler: One common feature of text editors |
In Swing, you can implement a widget similar to the
one shown in Figure 3 by doing just a handful of things. For example,
when the current selection changes, the TabSet under the selection
must be obtained.
To find out when the selection changes, an application
can implement CaretListener, and notify it whenever the selection
changes. The application can obtain the TabSet under the selection
by inspecting the paragraph Element at the selection point. The
following code shows how you can perform this operation:
StyledDocument styledDocument = textPane.getStyledDocument();
Element paragraphElement = styledDocument.getParagraphElement
(textPane.getSelectionStart());
TabSet tabs = StyleConstants.getTabSet
(paragraphElement.getAttributes());
To align the ruler correctly, you must determine
the location of the left margin. To obtain the left margin, you
can use the View method getChildAllocation().
The following pseudocode outlines the process of obtaining a left
margin:
Shape bounds = text.getBounds() - text.getInsets();
View view = text.getUI().getRootView(text);
while (view != null && view.getElement() != paragraphElement
&& bounds != null) {
viewIndex = findChildViewIndex(view,
paragraphElement.getStartOffset());
if (viewIndex != -1) {
bounds = view.getChildAllocation(viewIndex, bounds);
view = view.getView(viewIndex);
}
else {
view = null;
}
}
if (view != null && bounds != null) {
return bounds.getBounds().x;
}
A ruler should allow the user to move around existing
tabs. So there needs to be a way to locate a tab given a particular
location. The following pseudocode shows how this could be done:
int tabIndex = tabs.getTabIndexAfter(location - slop);
if (tabIndex != -1) {
TabStop tab = tabs.getTab(tabIndex);
if (tab.getPosition() >= (location - slop) &&
tab.getPosition() <= (location + slop)) {
return tab;
}
}
TabStop and TabSet cannot be modified. Consequently,
in order to move a TabStop, both a new TabStop and a new TabSet
must be created. Similarly, if a new TabStop is to be added, a new
TabSet must be created.
When you create a new TabSet, be sure that the passed-in
array of TabStops are positioned in ascending order.
Ruler.java pulls the above
ideas together into a working ruler. It allows the user to drag
tabs around by simply clicking on them. If the mouse is pressed
on a location that is not near an existing tab, a new tab is created.
The user can toggle the alignment of the tab by holding the shift
key down while pressing the mouse. To remove a tab, the user can
drag it outside the bounds of the ruler.
You can add a ruler to a JScrollPane that contains
a JTextPane by executing the following code:
JTextPane text = new JTextPane();
Ruler ruler = new Ruler(text);
JScrollPane scrollPane = new JScrollPane(text);
scrollPane.setColumnHeaderView(ruler);
CAUTION:
The LabelView implementation in swing for JDK 1.2 is different
from the LabelView that is in swing for JDK 1.1. These differences
exist to handle drawing, and layout, of internationalized text
that are specific to the 1.2 release of the JDK. The LabelView
in 1.2 does not currently support tabs.
|