There are many ways to add line numbers to the JEditorPane and JTextPane Swing components. This post looks at two solutions: One providing a custom ParagraphView which paints a child element that contains the line number for that paragraph. The other implements a separate component which is passed as the RowHeaderView of the JScrollPane. Both support scrolling and line wrapping, and in this example the latter includes current line highlighting, as seen in the image below.
The ParagraphView solution is inspired by Stanislav Lapitsky’s old post on the topic, although with some improvements. The basic idea is to paint the line number in the child of the ParagraphView, using the paintChild() method. The advantage is that the relationship between the line and the line number is already established, so alignment is easy. We just have to count which element we’re dealing with to know which number to print. The two methods below show this part. Notice also that only the index 0 of the ParagraphView should be used for the line number, so we don’t count a wrapped line twice. Furthermore, a mono-spaced font helps with padding and alignment.
In order to create this custom ParagraphView, it has to be returned through a ViewFactory, which again has to be provided through an EditorKit. The file below shows the full implementation. Notice how the default ViewFactory is used for all other elements; only the Paragraph Element gets a custom view.
The second solution renders the line numbers in a completely separate component, which can be a JComponent (or even an old style AWT Component). This component is passed in to the setRowHeaderView() method of JScrollPane. That way, the coupling becomes a bit cleaner than overriding multiple classes and methods as with the the ParagraphView solution. However, the down-side is that we must do the alignment with the Editor component manually. Luckily, the Swing Text Component API provides a solid API for this purpose.
The code below is inspired by Rob Camick’s article, but simplifies several aspects of his stand-alone API style class. At the centre is the translation between the Document model and its onscreen view. The viewToModel() method translates an x,y point on the screen to the nearest character offset, while the modelToView() method goes the other way. That way we can align the line number at the same y point as the line in the editor. In Camick’s code, he loops through the text by character offsets rather than paragraphs; the getRowEnd() method helps by finding the end of a line. Camick also points out the the line number should take the font decent of the editor font into account when positioning the line number. The FontMetrics class helps with measuring sizes of fonts.
Finally worth noting is the synchronisation between the editor and the margin component. Since they are separate, this must be handled through event listeners registered with the editor component. The DocumentListener and ComponentListener notifies of edit, movement and resizing events, while the CaretListener signals movement in the cursor position. All of these events force a repaint, but through a scheduled AWT thread, to make sure the editor view has updated first, as seen in the documentChanged() method at the bottom of the code section below.
Here are the full code listings for both examples, as stand-alone applications.