/*
* 02/11/2009
*
* LineNumberList.java - Renders line numbers in an RTextScrollPane.
* Copyright (C) 2009 Robert Futrell
* robert_futrell at users.sourceforge.net
* http://fifesoft.com/rsyntaxtextarea
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
*/
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.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
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;
/**
* Renders line numbers in the gutter.
*
* @author Robert Futrell
* @version 1.0
*/
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 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 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 <code>1</code>, but applications can change
* this if, for example, they are displaying a subset of lines in a file.
*/
private int lineNumberingStartIndex;
private static final int RHS_BORDER_WIDTH = 8;
/**
* Constructs a new <code>LineNumberList</code> 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, Color.GRAY);
}
/**
* Constructs a new <code>LineNumberList</code>.
*
* @param textArea
* The text component for which line numbers will be displayed.
* @param numberColor
* The color to use for the line numbers.
*/
public LineNumberList(RTextArea textArea, Color numberColor) {
super(textArea);
if (numberColor != null) {
setForeground(numberColor);
}
else {
setForeground(Color.GRAY);
}
// 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);
}
/**
* 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.
*/
public void addNotify() {
super.addNotify();
if (textArea != null) {
l.install(textArea); // Won't double-install
}
updateCellWidths();
updateCellHeights();
}
/**
* Returns the starting line's line number. The default value is <code>1</code>.
*
* @return The index
* @see #setLineNumberingStartIndex(int)
*/
public int getLineNumberingStartIndex() {
return lineNumberingStartIndex;
}
/**
* {@inheritDoc}
*/
public Dimension getPreferredSize() {
int h = textArea != null ? textArea.getHeight() : 100; // Arbitrary
return new Dimension(cellWidth, h);
}
/**
* {@inheritDoc}
*/
void handleDocumentEvent(DocumentEvent e) {
int newLineCount = textArea != null ? textArea.getLineCount() : 0;
if (newLineCount != currentLineCount) {
// Adjust the amount of space the line numbers take up,
// if necessary.
if (newLineCount / 10 != currentLineCount / 10) {
updateCellWidths();
}
currentLineCount = newLineCount;
repaint();
}
}
/**
* {@inheritDoc}
*/
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.
*/
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());
Document doc = textArea.getDocument();
Element root = doc.getDefaultRootElement();
if (textArea.getLineWrap()) {
paintWrappedLineNumbers(g, visibleRect);
return;
}
// Get the first and last lines to paint.
int topLine = visibleRect.y / cellHeight;
int bottomLine = Math.min(topLine + visibleRect.height / cellHeight + 1,
root.getElementCount());
// 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" up just enough for the missing part of
// the first line.
int actualTopY = topLine * cellHeight;
textAreaInsets = textArea.getInsets(textAreaInsets);
actualTopY += textAreaInsets.top;
int y = actualTopY + ascent;
// Highlight the current line's line number, if desired.
if (textArea.getHighlightCurrentLine() && currentLine >= 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;
for (int i = topLine + 1; i <= bottomLine; i++) {
int index = i + getLineNumberingStartIndex() - 1;
String number = Integer.toString(index);
int width = metrics.stringWidth(number);
g.drawString(number, rhs - width, y);
y += cellHeight;
}
}
else { // rtl
for (int i = topLine + 1; i <= bottomLine; i++) {
int index = i + getLineNumberingStartIndex() - 1;
String number = Integer.toString(index);
g.drawString(number, RHS_BORDER_WIDTH, y);
y += cellHeight;
}
}
}
/**
* Paints line numbers for text areas with line wrap enabled.
*
* @param g
* The graphics context.
* @param visibleRect
* The visible rectangle of these line numbers.
*/
private void paintWrappedLineNumbers(Graphics g, Rectangle visibleRect) {
// The variables we use are as follows:
// - visibleRect is the "visible" area of the text area; e.g.
// [0,100, 300,100+(lineCount*cellHeight)-1].
// actualTop.y is the topmost-pixel in the first logical line we
// paint. Note that we may well not paint this part of the logical
// line, as it may be broken into many physical lines, with the first
// few physical lines scrolled past. Note also that this is NOT the
// visible rect of this line number list; this line number list has
// visible rect == [0,0, insets.left-1,visibleRect.height-1].
// - offset (<=0) is the y-coordinate at which we begin painting when
// we begin painting with the first logical line. This can be
// negative, signifying that we've scrolled past the actual topmost
// part of this line.
// The algorithm is as follows:
// - Get the starting y-coordinate at which to paint. This may be
// above the first visible y-coordinate as we're in line-wrapping
// mode, but we always paint entire logical lines.
// - Paint that line's line number and highlight, if appropriate.
// Increment y to be just below the are we just painted (i.e., the
// beginning of the next logical line's view area).
// - Get the ending visual position for that line. We can now loop
// back, paint this line, and continue until our y-coordinate is
// past the last visible y-value.
// We avoid using modelToView/viewToModel where possible, as these
// methods trigger a parsing of the line into syntax tokens, which is
// costly. It's cheaper to just grab the child views' bounds.
// Some variables we'll be using.
int width = getWidth();
RTextAreaUI ui = (RTextAreaUI) textArea.getUI();
View v = ui.getRootView(textArea).getView(0);
boolean currentLineHighlighted = textArea.getHighlightCurrentLine();
Document doc = textArea.getDocument();
Element root = doc.getDefaultRootElement();
int lineCount = root.getElementCount();
int topPosition = textArea.viewToModel(
new Point(visibleRect.x, visibleRect.y));
int topLine = root.getElementIndex(topPosition);
// Compute the y at which to begin painting text, taking into account
// that 1 logical line => 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;
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).
topLine++;
if (topLine >= lineCount)
break;
}
}
/**
* Called when this component is removed from the view hierarchy.
*/
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.
*/
public void setFont(Font font) {
super.setFont(font);
updateCellWidths();
updateCellHeights();
}
/**
* Sets the starting line's line number. The default value is <code>1</code>. 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) {
lineNumberingStartIndex = index;
}
/**
* Sets the text area being displayed.
*
* @param textArea
* The text area.
*/
public void setTextArea(RTextArea textArea) {
if (l == null) {
l = new Listener();
}
if (this.textArea != null) {
l.uninstall(textArea);
}
super.setTextArea(textArea);
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 <code>textArea</code>.
*/
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.
*/
private void updateCellWidths() {
int oldCellWidth = cellWidth;
cellWidth = RHS_BORDER_WIDTH;
// 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();
while (lineCount >= 10) {
lineCount = lineCount / 10;
count++;
}
cellWidth += fontMetrics.charWidth('9') * (count + 1) + 5;
}
}
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;
}
}
}
}