org.fife.ui.rtextarea.LineNumberList Maven / Gradle / Ivy
The newest version!
/*
* 02/11/2009
*
* LineNumberList.java - Renders line numbers in an RTextScrollPane.
*
* This library is distributed under a modified BSD license. See the included
* RSyntaxTextArea.License.txt file for details.
*/
package org.fife.ui.rtextarea;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.Map;
import javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener;
import javax.swing.event.DocumentEvent;
import javax.swing.event.MouseInputListener;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.Element;
import javax.swing.text.View;
import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea;
import org.fife.ui.rsyntaxtextarea.RSyntaxUtilities;
import org.fife.ui.rsyntaxtextarea.folding.Fold;
import org.fife.ui.rsyntaxtextarea.folding.FoldManager;
/**
* Renders line numbers in the gutter.
*
* @author Robert Futrell
* @version 1.0
*/
public class LineNumberList extends AbstractGutterComponent
implements MouseInputListener {
private int currentLine; // The last line the caret was on.
private int lastY = -1; // Used to check if caret changes lines when line wrap is enabled.
private int lastVisibleLine;// Last line index painted.
private int cellHeight; // Height of a line number "cell" when word wrap is off.
private int cellWidth; // The width used for all line number cells.
private int ascent; // The ascent to use when painting line numbers.
private Map,?> aaHints;
private int mouseDragStartOffset;
/**
* Listens for events from the current text area.
*/
private Listener l;
/**
* Used in {@link #paintComponent(Graphics)} to prevent reallocation on
* each paint.
*/
private Insets textAreaInsets;
/**
* Used in {@link #paintComponent(Graphics)} to prevent reallocation on
* each paint.
*/
private Rectangle visibleRect;
/**
* The index at which line numbering should start. The default value is
* 1
, but applications can change this if, for example, they
* are displaying a subset of lines in a file.
*/
private int lineNumberingStartIndex;
/**
* Constructs a new LineNumberList
using default values for
* line number color (gray) and highlighting the current line.
*
* @param textArea The text component for which line numbers will be
* displayed.
*/
public LineNumberList(RTextArea textArea) {
this(textArea, null);
}
/**
* Constructs a new LineNumberList
.
*
* @param textArea The text component for which line numbers will be
* displayed.
* @param numberColor The color to use for the line numbers. If this is
* null
, gray will be used.
*/
public LineNumberList(RTextArea textArea, Color numberColor) {
super(textArea);
if (numberColor!=null) {
setForeground(numberColor);
}
else {
setForeground(Color.GRAY);
}
}
/**
* Overridden to set width of this component correctly when we are first
* displayed (as keying off of the RTextArea gives us (0,0) when it isn't
* yet displayed.
*/
@Override
public void addNotify() {
super.addNotify();
if (textArea!=null) {
l.install(textArea); // Won't double-install
}
updateCellWidths();
updateCellHeights();
}
/**
* Calculates the last line number index painted in this component.
*
* @return The last line number index painted in this component.
*/
private int calculateLastVisibleLineNumber() {
int lastLine = 0;
if (textArea!=null) {
lastLine = textArea.getLineCount()+getLineNumberingStartIndex()-1;
}
return lastLine;
}
/**
* Returns the starting line's line number. The default value is
* 1
.
*
* @return The index
* @see #setLineNumberingStartIndex(int)
*/
public int getLineNumberingStartIndex() {
return lineNumberingStartIndex;
}
/**
* {@inheritDoc}
*/
@Override
public Dimension getPreferredSize() {
int h = textArea!=null ? textArea.getHeight() : 100; // Arbitrary
return new Dimension(cellWidth, h);
}
/**
* Returns the width of the empty border on this component's right-hand
* side (or left-hand side, if the orientation is RTL).
*
* @return The border width.
*/
private int getRhsBorderWidth() {
int w = 4;
if (textArea instanceof RSyntaxTextArea) {
if (((RSyntaxTextArea)textArea).isCodeFoldingEnabled()) {
w = 0;
}
}
return w;
}
/**
* {@inheritDoc}
*/
@Override
void handleDocumentEvent(DocumentEvent e) {
int newLastLine = calculateLastVisibleLineNumber();
if (newLastLine!=lastVisibleLine) {
// Adjust the amount of space the line numbers take up,
// if necessary.
if (newLastLine/10 != lastVisibleLine/10) {
updateCellWidths();
}
lastVisibleLine = newLastLine;
repaint();
}
}
@Override
protected void init() {
super.init();
// Initialize currentLine; otherwise, the current line won't start
// off as highlighted.
currentLine = 0;
setLineNumberingStartIndex(1);
visibleRect = new Rectangle(); // Must be initialized
addMouseListener(this);
addMouseMotionListener(this);
aaHints = RSyntaxUtilities.getDesktopAntiAliasHints();
}
/**
* {@inheritDoc}
*/
@Override
void lineHeightsChanged() {
updateCellHeights();
}
public void mouseClicked(MouseEvent e) {
}
public void mouseDragged(MouseEvent e) {
if (mouseDragStartOffset>-1) {
int pos = textArea.viewToModel(new Point(0, e.getY()));
if (pos>=0) { // Not -1
textArea.setCaretPosition(mouseDragStartOffset);
textArea.moveCaretPosition(pos);
}
}
}
public void mouseEntered(MouseEvent e) {
}
public void mouseExited(MouseEvent e) {
}
public void mouseMoved(MouseEvent e) {
}
public void mousePressed(MouseEvent e) {
if (textArea==null) {
return;
}
if (e.getButton()==MouseEvent.BUTTON1) {
int pos = textArea.viewToModel(new Point(0, e.getY()));
if (pos>=0) { // Not -1
textArea.setCaretPosition(pos);
}
mouseDragStartOffset = pos;
}
else {
mouseDragStartOffset = -1;
}
}
public void mouseReleased(MouseEvent e) {
}
/**
* Paints this component.
*
* @param g The graphics context.
*/
@Override
protected void paintComponent(Graphics g) {
if (textArea==null) {
return;
}
visibleRect = g.getClipBounds(visibleRect);
if (visibleRect==null) { // ???
visibleRect = getVisibleRect();
}
//System.out.println("LineNumberList repainting: " + visibleRect);
if (visibleRect==null) {
return;
}
Color bg = getBackground();
if (getGutter()!=null) { // Should always be true
bg = getGutter().getBackground();
}
g.setColor(bg);
g.fillRect(0,visibleRect.y, cellWidth,visibleRect.height);
g.setFont(getFont());
if (aaHints!=null) {
((Graphics2D)g).addRenderingHints(aaHints);
}
if (textArea.getLineWrap()) {
paintWrappedLineNumbers(g, visibleRect);
return;
}
// Get where to start painting (top of the row), and where to paint
// the line number (drawString expects y==baseline).
// We need to be "scrolled up" just enough for the missing part of
// the first line.
textAreaInsets = textArea.getInsets(textAreaInsets);
if (visibleRect.y=topLine &&
currentLine<=bottomLine) {
g.setColor(textArea.getCurrentLineHighlightColor());
g.fillRect(0,actualTopY+(currentLine-topLine)*cellHeight,
cellWidth,cellHeight);
}
*/
// Paint line numbers
g.setColor(getForeground());
boolean ltr = getComponentOrientation().isLeftToRight();
if (ltr) {
FontMetrics metrics = g.getFontMetrics();
int rhs = getWidth() - RHS_BORDER_WIDTH;
int line = topLine + 1;
while (y at least 1 physical line, so it may be that
// y<0. The computed y-value is the y-value of the top of the first
// (possibly) partially-visible view.
Rectangle visibleEditorRect = ui.getVisibleEditorRect();
Rectangle r = LineNumberList.getChildViewBounds(v, topLine,
visibleEditorRect);
int y = r.y;
final int RHS_BORDER_WIDTH = getRhsBorderWidth();
int rhs;
boolean ltr = getComponentOrientation().isLeftToRight();
if (ltr) {
rhs = width - RHS_BORDER_WIDTH;
}
else { // rtl
rhs = RHS_BORDER_WIDTH;
}
int visibleBottom = visibleRect.y + visibleRect.height;
FontMetrics metrics = g.getFontMetrics();
// Keep painting lines until our y-coordinate is past the visible
// end of the text area.
g.setColor(getForeground());
while (y < visibleBottom) {
r = LineNumberList.getChildViewBounds(v, topLine, visibleEditorRect);
/*
// Highlight the current line's line number, if desired.
if (currentLineHighlighted && topLine==currentLine) {
g.setColor(textArea.getCurrentLineHighlightColor());
g.fillRect(0,y, width,(r.y+r.height)-y);
g.setColor(getForeground());
}
*/
// Paint the line number.
int index = (topLine+1) + getLineNumberingStartIndex() - 1;
String number = Integer.toString(index);
if (ltr) {
int strWidth = metrics.stringWidth(number);
g.drawString(number, rhs-strWidth,y+ascent);
}
else {
int x = RHS_BORDER_WIDTH;
g.drawString(number, x, y+ascent);
}
// The next possible y-coordinate is just after the last line
// painted.
y += r.height;
// Update topLine (we're actually using it for our "current line"
// variable now).
if (fm!=null) {
Fold fold = fm.getFoldForLine(topLine);
if (fold!=null && fold.isCollapsed()) {
topLine += fold.getCollapsedLineCount();
}
}
topLine++;
if (topLine>=lineCount)
break;
}
}
/**
* Called when this component is removed from the view hierarchy.
*/
@Override
public void removeNotify() {
super.removeNotify();
if (textArea!=null) {
l.uninstall(textArea);
}
}
/**
* Repaints a single line in this list.
*
* @param line The line to repaint.
*/
private void repaintLine(int line) {
int y = textArea.getInsets().top;
y += line*cellHeight;
repaint(0,y, cellWidth,cellHeight);
}
/**
* Overridden to ensure line number cell sizes are updated with the
* font size change.
*
* @param font The new font to use for line numbers.
*/
@Override
public void setFont(Font font) {
super.setFont(font);
updateCellWidths();
updateCellHeights();
}
/**
* Sets the starting line's line number. The default value is
* 1
. Applications can call this method to change this value
* if they are displaying a subset of lines in a file, for example.
*
* @param index The new index.
* @see #getLineNumberingStartIndex()
*/
public void setLineNumberingStartIndex(int index) {
if (index!=lineNumberingStartIndex) {
lineNumberingStartIndex = index;
updateCellWidths();
repaint();
}
}
/**
* Sets the text area being displayed.
*
* @param textArea The text area.
*/
@Override
public void setTextArea(RTextArea textArea) {
if (l==null) {
l = new Listener();
}
if (this.textArea!=null) {
l.uninstall(textArea);
}
super.setTextArea(textArea);
lastVisibleLine = calculateLastVisibleLineNumber();
if (textArea!=null) {
l.install(textArea); // Won't double-install
updateCellHeights();
updateCellWidths();
}
}
/**
* Changes the height of the cells in the JList so that they are as tall as
* font. This function should be called whenever the user changes the Font
* of textArea
.
*/
private void updateCellHeights() {
if (textArea!=null) {
cellHeight = textArea.getLineHeight();
ascent = textArea.getMaxAscent();
}
else {
cellHeight = 20; // Arbitrary number.
ascent = 5; // Also arbitrary
}
repaint();
}
/**
* Changes the width of the cells in the JList so you can see every digit
* of each.
*/
void updateCellWidths() {
int oldCellWidth = cellWidth;
cellWidth = getRhsBorderWidth();
// Adjust the amount of space the line numbers take up, if necessary.
if (textArea!=null) {
Font font = getFont();
if (font!=null) {
FontMetrics fontMetrics = getFontMetrics(font);
int count = 0;
int lineCount = textArea.getLineCount() +
getLineNumberingStartIndex() - 1;
do {
lineCount = lineCount/10;
count++;
} while (lineCount >= 10);
cellWidth += fontMetrics.charWidth('9')*(count+1) + 3;
}
}
if (cellWidth!=oldCellWidth) { // Always true
revalidate();
}
}
/**
* Listens for events in the text area we're interested in.
*/
private class Listener implements CaretListener, PropertyChangeListener {
private boolean installed;
public void caretUpdate(CaretEvent e) {
int dot = textArea.getCaretPosition();
// We separate the line wrap/no line wrap cases because word wrap
// can make a single line from the model (document) be on multiple
// lines on the screen (in the view); thus, we have to enhance the
// logic for that case a bit - we check the actual y-coordinate of
// the caret when line wrap is enabled. For the no-line-wrap case,
// getting the line number of the caret suffices. This increases
// efficiency in the no-line-wrap case.
if (textArea.getLineWrap()==false) {
int line = textArea.getDocument().getDefaultRootElement().
getElementIndex(dot);
if (currentLine!=line) {
repaintLine(line);
repaintLine(currentLine);
currentLine = line;
}
}
else { // lineWrap enabled; must check actual y position of caret
try {
int y = textArea.yForLineContaining(dot);
if (y!=lastY) {
lastY = y;
currentLine = textArea.getDocument().
getDefaultRootElement().getElementIndex(dot);
repaint(); // *Could* be optimized...
}
} catch (BadLocationException ble) {
ble.printStackTrace();
}
}
}
public void install(RTextArea textArea) {
if (!installed) {
//System.out.println("Installing");
textArea.addCaretListener(this);
textArea.addPropertyChangeListener(this);
caretUpdate(null); // Force current line highlight repaint
installed = true;
}
}
public void propertyChange(PropertyChangeEvent e) {
String name = e.getPropertyName();
// If they change the current line highlight in any way...
if (RTextArea.HIGHLIGHT_CURRENT_LINE_PROPERTY.equals(name) ||
RTextArea.CURRENT_LINE_HIGHLIGHT_COLOR_PROPERTY.equals(name)) {
repaintLine(currentLine);
}
}
public void uninstall(RTextArea textArea) {
if (installed) {
//System.out.println("Uninstalling");
textArea.removeCaretListener(this);
textArea.removePropertyChangeListener(this);
installed = false;
}
}
}
}