3
\$\begingroup\$

What I wanted to do

As part of a larger project, I needed a simple text editor with line numbers. The editor should also be able to highlight the current line (line number in bold) and define the tab size.

The result looks like this:

enter image description here

My solution

After searching the internet for a while, I found out about the possibility of using a DocumentListener and adding a sepearate text-component with the line-numbers as RowHeader:

I also found this complete solution, but nevertheless implemented the class it on my own - for learning purposes.

My implementation of it looks like this:

import java.awt.Color;
import java.awt.Font;
import java.awt.Dimension;
import javax.swing.JScrollPane;
import javax.swing.JTextPane;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.AbstractDocument;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.DocumentFilter;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;

/**
 * 
 * @author Philipp Wilhelm 
 * Provides a JScrollPane with line-numbers
 */
public class EditorScrollPane extends JScrollPane {

  private static final long serialVersionUID = 1L;

  private JTextPane inputArea;
  private String indentation = "  ";
  private JTextPane lineNumbers;

  /*
   * Here the constructor creates a TextPane as an editor-field and another TextPane for the
   * line-numbers.
   */
  public EditorScrollPane(int width, int height) {
    // Editor-field
    inputArea = new JTextPane();
    inputArea.setPreferredSize(new Dimension(width, height));
    Document doc = inputArea.getDocument();

    // Replacing tabs with two spaces
    ((AbstractDocument) doc).setDocumentFilter(new DocumentFilter() {
      public void replace(FilterBypass fb, int offset, int length, String text, AttributeSet attrs)
          throws BadLocationException {
        super.insertString(fb, offset, text.replace("\t", indentation), attrs);
      }
    });

    // Line-numbers
    lineNumbers = new JTextPane();
    lineNumbers.setBackground(Color.GRAY);
    lineNumbers.setEditable(false);

    // Line-numbers should be right-aligned
    SimpleAttributeSet rightAlign = new SimpleAttributeSet();
    StyleConstants.setAlignment(rightAlign, StyleConstants.ALIGN_RIGHT);
    lineNumbers.setParagraphAttributes(rightAlign, true);

    doc.addDocumentListener(new DocumentListener() {
      @Override
      public void changedUpdate(DocumentEvent e) {
        lineNumbers();
      }

      @Override
      public void insertUpdate(DocumentEvent e) {
        lineNumbers();
      }

      @Override
      public void removeUpdate(DocumentEvent e) {
        lineNumbers();
      }
    });
    // Setting font
    this.setFont(new Font("Monospaced", 12, Font.PLAIN));

    // Sets the main-component in the JScrollPane. this.add(inputArea) wasn't
    // enough in this case
    this.getViewport().add(inputArea);

    // Adds lineNumbers as row header on the left side of the main JTextPane
    this.setRowHeaderView(lineNumbers);
  }

  private void lineNumbers() {
    try {
      String str = inputArea.getText();

      // Plain Style
      SimpleAttributeSet plain = new SimpleAttributeSet();
      StyleConstants.setFontFamily(plain, "Monospaced");
      StyleConstants.setFontSize(plain, 12);

      // Bold style
      SimpleAttributeSet bold = new SimpleAttributeSet();
      StyleConstants.setBold(bold, true);

      // Remove all from document
      Document doc = lineNumbers.getDocument();
      doc.remove(0, doc.getLength());

      // Calculating the number of lines
      int length = str.length() - str.replaceAll("\n", "").length() + 1;

      // Adding line-numbers
      for (int i = 1; i <= length; i++) {

        // Non-bold line-numbers
        if (i < length) {
          doc.insertString(doc.getLength(), i + "\n", plain);

        // Last line-number bold
        } else {
          doc.insertString(doc.getLength(), i + "\n", bold);
        }
      }
    } catch (BadLocationException e) {
      e.printStackTrace();
    }
  }

  /*
   * Setting indentation size in editor-field
   */
  public void setIndentationSize(int size) {
    String cache = indentation;
    indentation = "";
    for (int i = 0; i < size; i++) {
      indentation += " ";
    }
    // Replace all previous indentations (at beginning of lines)
    inputArea.setText(inputArea.getText().replaceAll(cache, indentation));
  }

  /*
   * Overrides the method getText().
   */
  public String getText() {
    return inputArea.getText();
  }

  /*
   * Overrides the method setText().
   */
  public void setText(String str) {
    inputArea.setText(str);
  }
}

Minimal working example

If you want to test the class, you can use the following class to run the code:

// Just for testing purposes

import javax.swing.*;

public class Test {
  public static void main(String[] args) {
    // Frame
    JFrame frame = new JFrame("Editor-Field");
    frame.setExtendedState(JFrame.MAXIMIZED_BOTH);
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

    //Panel
    JPanel panel = new JPanel();
    EditorScrollPane editor = new EditorScrollPane(400,400);
    panel.add(editor);
    frame.add(panel);
    frame.pack();
    frame.setVisible(true);
  }
}

Questions

  • How can this code be improved in general?
  • Can the line-numbering be done in a more efficient way?
  • Does this class miss anything important that you would expect from such a class?
\$\endgroup\$
1
  • 1
    \$\begingroup\$ Lines (paragraphs) may vary in height (which would argue for a line number as part of an extra paragraph margin). You may use StyledDocument as using their attributes; the base class of HTML and RTF (Rich Text Format). Lighter background for line numbers, width based on log10(number of lines). Very nice beginning. \$\endgroup\$
    – Joop Eggen
    Jul 8, 2020 at 14:44

1 Answer 1

2
\$\begingroup\$

Awkward Container

you create a composite component - the child items are

  • JTextPane inputArea
  • JTextPane lineNumbers

as container for these you use a JScrollPane - why is that so? why don't you use a simple container (like a JPanel) instead?

When you use a JScrollPane instead a mere JPanel you bring in more complexity than required. Additionally i would not expect any component to be a scrollPanel except a - well yes, except a JScrollPane itself...

Inconsistent Font Handling

the font size of your LineNumberComponent should be equal to the EditorComponent font size - if they differ, the line numbers start to drift for each line a bit more

additional

you set the font in the container (the JScrollPane) - that makes no sense in my opinion. (I think it's a typo and should be inputArea.setFont(...) )

// Setting font
this.setFont(new Font("Monospaced", 12, Font.PLAIN));

method naming

  • lineNumbers is not a verb but should be - maybe something like adjustLineNumbering?
  • setIndentationSize does not, what a setter should do: set a value - instead rename it into what it really does: adjustIndentation

Line Highlightning

you can get the cursors position by using the getCaretPosition to determine the selected line - even thou you'll have to do some math on your own. have a look at this answer on stackoverflow how you can make parts of a JTextArea in bold.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.