package com.github.trytocatch.swingplus.text; import java.awt.Color; import java.awt.Dimension; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.Rectangle; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import javax.swing.JComponent; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import javax.swing.plaf.TextUI; import javax.swing.text.AbstractDocument; import javax.swing.text.BadLocationException; import javax.swing.text.Document; import javax.swing.text.Element; import javax.swing.text.JTextComponent; /** * a component to show the line number of JTextComponent, * you can use it in this way: * <blockquote> * <pre> * JTextArea jta = new JTextArea(); * JScrollPane jsp = new JScrollPane(jta); * jsp.setRowHeaderView(new LineLabel(jta)); * </pre> * </blockquote> * @author trytocatch */ public class LineLabel extends JComponent { private static final long serialVersionUID = -2415042750630275725L; private DocumentListener documentListener; private int offset = 3; private Color repetitiveLineColor = Color.lightGray; private int width; protected int rowCount; protected JTextComponent jTextComponent; public LineLabel(JTextComponent jTextComponent) { if (jTextComponent == null) throw new IllegalArgumentException("jTextComponent can't be null"); this.jTextComponent = jTextComponent; setFont(new Font(Font.MONOSPACED, Font.PLAIN, this.jTextComponent .getFont().getSize())); setOpaque(true); setRowCount(this.jTextComponent.getDocument().getDefaultRootElement() .getElementCount()); documentListener = new DocumentListener() { @Override public void removeUpdate(DocumentEvent e) { setRowCount(e.getDocument().getDefaultRootElement() .getElementCount()); repaint(); } @Override public void insertUpdate(DocumentEvent e) { setRowCount(e.getDocument().getDefaultRootElement() .getElementCount()); repaint(); } @Override public void changedUpdate(DocumentEvent e) { setRowCount(e.getDocument().getDefaultRootElement() .getElementCount()); repaint(); } }; this.jTextComponent.getDocument().addDocumentListener(documentListener); this.jTextComponent .addPropertyChangeListener(new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent evt) { if ("font".equals(evt.getPropertyName())) resetFont((Font) evt.getNewValue()); else if ("document".equals(evt.getPropertyName())) { ((Document) evt.getOldValue()) .removeDocumentListener(documentListener); ((Document) evt.getNewValue()) .addDocumentListener(documentListener); } } }); this.jTextComponent.addComponentListener(new ComponentAdapter() { @Override public void componentResized(ComponentEvent e) { setSize(getPreferredSize()); } }); } private void setRowCount(int rowCount) { if (this.rowCount != rowCount) { this.rowCount = rowCount; updateWidth(); } } private void updateWidth() { int widthTemp = offset * 2 + getFontMetrics(getFont()).stringWidth( String.valueOf(this.rowCount)); if (widthTemp != width) { width = widthTemp; revalidate(); } } private void resetFont(Font newFont) { setFont(getFont().deriveFont((float) newFont.getSize())); updateWidth(); } @Override public Dimension getPreferredSize() { return new Dimension(width, jTextComponent.getHeight()); } @Override public void paintComponent(Graphics g) { Graphics g2 = g.create(); Rectangle clipRect = g2.getClipBounds(); if (isOpaque()) { g2.setColor(getBackground()); g2.fillRect(clipRect.x, clipRect.y, clipRect.width, clipRect.height); } g2.setColor(getForeground()); g2.setFont(getFont()); Document document = jTextComponent.getDocument(); Graphics greyG2 = g2.create(); greyG2.setColor(repetitiveLineColor); try { Element rootElement; if (document instanceof AbstractDocument) ((AbstractDocument) document).readLock(); rootElement = document.getDefaultRootElement(); FontMetrics myFontMetrics = getFontMetrics(getFont()); FontMetrics textFontMetrics = jTextComponent .getFontMetrics(jTextComponent.getFont()); TextUI ui = jTextComponent.getUI(); int rowNum = rootElement.getElementIndex(ui.viewToModel( jTextComponent, clipRect.getLocation())); int ascent = textFontMetrics.getAscent(); int rowHeight = textFontMetrics.getHeight(); Element element = rootElement.getElement(rowNum); if (element == null) return; String rowNumStr = ""; int x = 0, y, nextY, originalStartY, maxY; try { originalStartY = ui.modelToView(jTextComponent, element.getStartOffset()).y; maxY = ui.modelToView(jTextComponent, rootElement.getEndOffset() - 1).y; } catch (BadLocationException e1) { return; } maxY = Math.min(maxY, clipRect.y + clipRect.height); y = originalStartY; if (y < clipRect.y) y = clipRect.y - (clipRect.y - y) % rowHeight; nextY = 0; for (; y <= maxY; y += rowHeight) { if (y < nextY) { greyG2.drawString(rowNumStr, x, y + ascent); } else { rowNumStr = String.valueOf(rowNum + 1); x = width - offset - myFontMetrics.stringWidth(rowNumStr); // nextY == 0 means that it's the first time if (nextY != 0 || y == originalStartY) g2.drawString(rowNumStr, x, y + ascent); else y -= rowHeight; rowNum++; if (rowNum >= rowCount) nextY = Integer.MAX_VALUE; else try { nextY = ui.modelToView(jTextComponent, rootElement .getElement(rowNum).getStartOffset()).y; } catch (BadLocationException e) { break; } } } } finally { if (document instanceof AbstractDocument) ((AbstractDocument) document).readUnlock(); g2.dispose(); greyG2.dispose(); } } public int getOffset() { return offset; } public void setOffset(int offset) { this.offset = offset; } public Color getRepetitiveLineColor() { return repetitiveLineColor; } public void setRepetitiveLineColor(Color repetitiveLineColor) { this.repetitiveLineColor = repetitiveLineColor; } }