[an error occurred while processing this directive]

Page One
Page Two
The Databank
What Is Swing?
Special Report
IDE Roundup
Swing and the Web
Swing Text
Tech Topics
Friends
Tips and Tricks
The PLAF Papers
Call 911
The Archive
JFC Home
Download Swing
Swing API Docs
Download JDK
JDK Docs
Java Tutorial
 
The Swing Connection Swing Text

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:


The TabStop Class

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.


The TabSet Class

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);    

TabExpander

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.


TabableView

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 and LabelView

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.

Mapping elements to views

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).


Implementing a Ruler

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.

A ruler
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 iconCAUTION: 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.

[an error occurred while processing this directive]