JavaAdd Line Numbering in the JEditorPane

Add Line Numbering in the JEditorPane

Developer.com content and product recommendations are editorially independent. We may make money when you click on links to our partners. Learn More.

Line numbering is a useful feature for text editors. It allows the user to estimate and control file size. Most source code editors support this feature. This article illustrates how to add line numbering support.

There are several approaches to add the feature. The simplest one is to measure font height and add a component to the left that paints numbers along the of JEditorPane height. If we want to use different font sizes in the JEditorPane, this approach won’t work. I suggest a little bit more of a complex way, but it doesn’t depend on fonts.

For this example, we will extend the simple StyledEditorKit. To start implementing, we should understand the structure of data, which is represented by a document. The DefaultStyledDocument class has the following three-level tree structure:

Section (Root) element.
--Paragraph element1
----Content element1_1
--Paragraph element2
----Content element2_1

However, the structure of views is more complex because each paragraph view consists of one or more rows, which, in turn, contain text or image views. Thus, the view’s structure is the following:

Section view
--Paragraph view
----Row view1
------Content views (Label view, Image view or Component view)
----Row view2
------Content views (Label view, Image view or Component view)

Creating, layout, and the adjustment of rows are performed by the paragraph’s FlowStrategy. For our feature, we don’t have to extend the FlowStrategy class itself, but we must modify some parameters that define the paragraph’s representation. We have to change the layout algorithm to reserve additional space on the left for row numbers and change the view to paint the numbers. The row width is defined by the FlowSpan parameter, provided by Paragraph, according to available space. In a simple case like ours, the span equals the JEditorPane width minus the left and right paragraph’s indents. We will just increase the left indent to fit the line numbers.

We want to replace the default (provided by SUN) ParagraphView implementation with ours, so we rewrite the ViewFactory that generates views. For that, we had to extend the StyledEditorKit and override the getViewFactory() method. ViewFactory provides a view for different kinds of elements. We should check the element kind and replace the default paragraph with the changed one.

class NumberedEditorKit extends StyledEditorKit {
    public ViewFactory getViewFactory() {
    return new NumberedViewFactory();
    }
}

class NumberedViewFactory implements ViewFactory {
  public View create(Element elem) {
    String kind = elem.getName();
    if (kind != null)
      if (kind.equals(AbstractDocument.ContentElementName)) {
        return new LabelView(elem);
      }
      else if (kind.equals(AbstractDocument.ParagraphElementName)) {
//      return new ParagraphView(elem);
        return new NumberedParagraphView(elem);
      }
      else if (kind.equals(AbstractDocument.SectionElementName)) {
        return new BoxView(elem, View.Y_AXIS);
      }
      else if (kind.equals(StyleConstants.ComponentElementName)) {
        return new ComponentView(elem);
      }
      else if (kind.equals(StyleConstants.IconElementName)) {
        return new IconView(elem);
      }
    // default to text display
    return new LabelView(elem);
  }
}

After that, we can start to modify our paragraph view class. We add a paragraph’s static property NUMBERS_WIDTH to store the width of the space reserved for line numbers. The constant will be used in the overridden setInsets() method. We will add the space to the paragraph’s left indent.

protected void setInsets(short top, short left, short bottom,
                         short right) {super.setInsets(top,(short)
                        (left+NUMBERS_WIDTH),bottom,right);
  }

All preparations have been done and we can start to implement final stage—painting row numbers. We override the paintChild() method of ParagraphView. The last parameter of the method is the number of the child—the required row number. It would be enough if we have only one paragraph, but in our case, we have to add the number of rows in the previous paragraphs. To get this amount, we should recall the structure of views. We get the parent view of our paragraph (Root view) and go through its children (Paragraphs) and accumulate their children count (Row count) until we reach our paragraph. This total amount is the count of rows before a paragraph.

public int getPreviousLineCount() {
  int lineCount = 0;
  View parent = this.getParent();
  int count = parent.getViewCount();
  for (int i = 0; i < count; i++) {
    if (parent.getView(i) == this) {
      break;
    }
    else {
      lineCount += parent.getView(i).getViewCount();
    }
  }
  return lineCount;
}

Thus, the number of the current line is the sum of line counts in all previous paragraphs, plus the number in the current paragraph plus 1 because child numbering is zero-based.

To position the line numbers, we use a rectangle parameter that represents the allocated region to paint into. Our paintChild() method is the following:

public void paintChild(Graphics g, Rectangle r, int n) {
  super.paintChild(g, r, n);
  int previousLineCount = getPreviousLineCount();
  int numberX = r.x - getLeftInset();
  int numberY = r.y + r.height - 5;
  g.drawString(Integer.toString(previousLineCount + n + 1),
              numberX, numberY);
}

After setting the EditorKit to out JEditorPane, the first row number is shown and these numbers are changed as soon as we edit JEditorPane’s content.

Appendix

The following is the full source code of the zooming example:

/**
 * @author Stanislav Lapitsky
 * @version 1.0
 */

import java.awt.*;
import javax.swing.*;
import javax.swing.text.*;

class LNTextPane extends JFrame {
    public LNTextPane() {
        JEditorPane edit = new JEditorPane();
        edit.setEditorKit(new NumberedEditorKit());

        JScrollPane scroll = new JScrollPane(edit);
        getContentPane().add(scroll);
        setSize(300, 300);
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setVisible(true);
    }

    public static void main(String a[]) {
        new LNTextPane();
    }
}

class NumberedEditorKit extends StyledEditorKit {
    public ViewFactory getViewFactory() {
        return new NumberedViewFactory();
    }
}

class NumberedViewFactory implements ViewFactory {
    public View create(Element elem) {
        String kind = elem.getName();
        if (kind != null)
            if (kind.equals(AbstractDocument.ContentElementName)) {
                return new LabelView(elem);
            }
            else if (kind.equals(AbstractDocument.
                             ParagraphElementName)) {
//              return new ParagraphView(elem);
                return new NumberedParagraphView(elem);
            }
            else if (kind.equals(AbstractDocument.
                     SectionElementName)) {
                return new BoxView(elem, View.Y_AXIS);
            }
            else if (kind.equals(StyleConstants.
                     ComponentElementName)) {
                return new ComponentView(elem);
            }
            else if (kind.equals(StyleConstants.IconElementName)) {
                return new IconView(elem);
            }
        // default to text display
        return new LabelView(elem);
    }
}

class NumberedParagraphView extends ParagraphView {
    public static short NUMBERS_WIDTH=25;

    public NumberedParagraphView(Element e) {
        super(e);
        short top = 0;
        short left = 0;
        short bottom = 0;
        short right = 0;
        this.setInsets(top, left, bottom, right);
    }

    protected void setInsets(short top, short left, short bottom,
                             short right) {super.setInsets
                             (top,(short)(left+NUMBERS_WIDTH),
                             bottom,right);
    }

    public void paintChild(Graphics g, Rectangle r, int n) {
        super.paintChild(g, r, n);
        int previousLineCount = getPreviousLineCount();
        int numberX = r.x - getLeftInset();
        int numberY = r.y + r.height - 5;
        g.drawString(Integer.toString(previousLineCount + n + 1),
                                      numberX, numberY);
    }

    public int getPreviousLineCount() {
        int lineCount = 0;
        View parent = this.getParent();
        int count = parent.getViewCount();
        for (int i = 0; i < count; i++) {
            if (parent.getView(i) == this) {
                break;
            }
            else {
                lineCount += parent.getView(i).getViewCount();
            }
        }
        return lineCount;
    }
}

About the Author

Stanislav Lapitsky is an offshore software developer and consultant with more
than 7 years of programming experience. His area of knowledge includes java based
technologies and RDBMS.

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Latest Posts

Related Stories