/*
* (c) Copyright 2010-2011 AgileBirds
*
* This file is part of OpenFlexo.
*
* OpenFlexo is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* OpenFlexo 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with OpenFlexo. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.openflexo.jedit;
/*
* TextAreaPainter.java - Paints the text area
* Copyright (C) 1999 Slava Pestov
*
* You may use and modify this package for any purpose. Redistribution is
* permitted, in both source and binary form, provided that this notice
* remains intact in all source distributions of this package.
*/
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.event.MouseEvent;
import java.util.Vector;
import javax.swing.JComponent;
import javax.swing.ToolTipManager;
import javax.swing.text.PlainDocument;
import javax.swing.text.Segment;
import javax.swing.text.TabExpander;
import javax.swing.text.Utilities;
import org.openflexo.jedit.JEditTextArea.CursorPositionListener;
/**
* The text area repaint manager. It performs double buffering and paints lines of text.
*
* @author Slava Pestov
* @version $Id: TextAreaPainter.java,v 1.2 2011/09/12 11:47:09 gpolet Exp $
*/
public class TextAreaPainter extends JComponent implements TabExpander {
/**
* Creates a new repaint manager. This should be not be called directly.
*/
public TextAreaPainter(JEditTextArea textArea, TextAreaDefaults defaults) {
this.textArea = textArea;
setAutoscrolls(true);
setDoubleBuffered(true);
setOpaque(true);
ToolTipManager.sharedInstance().registerComponent(this);
currentLine = new Segment();
currentLineIndex = -1;
setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR));
setFont(textArea.getFont());
setForeground(Color.black);
setBackground(Color.white);
highlights = new Vector<Highlight>();
blockCaret = defaults.blockCaret;
styles = defaults.styles;
cols = defaults.cols;
rows = defaults.rows;
caretColor = defaults.caretColor;
selectionColor = defaults.selectionColor;
lineHighlightColor = defaults.lineHighlightColor;
lineHighlight = defaults.lineHighlight;
bracketHighlightColor = defaults.bracketHighlightColor;
bracketHighlight = defaults.bracketHighlight;
paintInvalid = defaults.paintInvalid;
eolMarkerColor = defaults.eolMarkerColor;
eolMarkers = defaults.eolMarkers;
}
/**
* Returns if this component can be traversed by pressing the Tab key. This returns false.
*/
@Override
public final boolean isManagingFocus() {
return false;
}
/**
* Returns the syntax styles used to paint colorized text. Entry <i>n</i> will be used to paint tokens with id = <i>n</i>.
*
* @see org.gjt.sp.jedit.syntax.Token
*/
public final SyntaxStyle[] getStyles() {
return styles;
}
/**
* Sets the syntax styles used to paint colorized text. Entry <i>n</i> will be used to paint tokens with id = <i>n</i>.
*
* @param styles
* The syntax styles
* @see org.gjt.sp.jedit.syntax.Token
*/
public final void setStyles(SyntaxStyle[] styles) {
this.styles = styles;
repaint();
}
/**
* Returns the caret color.
*/
public final Color getCaretColor() {
return caretColor;
}
/**
* Sets the caret color.
*
* @param caretColor
* The caret color
*/
public final void setCaretColor(Color caretColor) {
this.caretColor = caretColor;
invalidateSelectedLines();
}
/**
* Returns the selection color.
*/
public final Color getSelectionColor() {
return selectionColor;
}
/**
* Sets the selection color.
*
* @param selectionColor
* The selection color
*/
public final void setSelectionColor(Color selectionColor) {
this.selectionColor = selectionColor;
invalidateSelectedLines();
}
/**
* Returns the line highlight color.
*/
public final Color getLineHighlightColor() {
return lineHighlightColor;
}
/**
* Sets the line highlight color.
*
* @param lineHighlightColor
* The line highlight color
*/
public final void setLineHighlightColor(Color lineHighlightColor) {
this.lineHighlightColor = lineHighlightColor;
invalidateSelectedLines();
}
/**
* Returns true if line highlight is enabled, false otherwise.
*/
public final boolean isLineHighlightEnabled() {
return lineHighlight;
}
/**
* Enables or disables current line highlighting.
*
* @param lineHighlight
* True if current line highlight should be enabled, false otherwise
*/
public final void setLineHighlightEnabled(boolean lineHighlight) {
this.lineHighlight = lineHighlight;
invalidateSelectedLines();
}
/**
* Returns the bracket highlight color.
*/
public final Color getBracketHighlightColor() {
return bracketHighlightColor;
}
/**
* Sets the bracket highlight color.
*
* @param bracketHighlightColor
* The bracket highlight color
*/
public final void setBracketHighlightColor(Color bracketHighlightColor) {
this.bracketHighlightColor = bracketHighlightColor;
invalidateLine(textArea.getBracketLine());
}
/**
* Returns true if bracket highlighting is enabled, false otherwise. When bracket highlighting is enabled, the bracket matching the one
* before the caret (if any) is highlighted.
*/
public final boolean isBracketHighlightEnabled() {
return bracketHighlight;
}
/**
* Enables or disables bracket highlighting. When bracket highlighting is enabled, the bracket matching the one before the caret (if
* any) is highlighted.
*
* @param bracketHighlight
* True if bracket highlighting should be enabled, false otherwise
*/
public final void setBracketHighlightEnabled(boolean bracketHighlight) {
this.bracketHighlight = bracketHighlight;
invalidateLine(textArea.getBracketLine());
}
/**
* Returns true if the caret should be drawn as a block, false otherwise.
*/
public final boolean isBlockCaretEnabled() {
return blockCaret;
}
/**
* Sets if the caret should be drawn as a block, false otherwise.
*
* @param blockCaret
* True if the caret should be drawn as a block, false otherwise.
*/
public final void setBlockCaretEnabled(boolean blockCaret) {
this.blockCaret = blockCaret;
invalidateSelectedLines();
}
/**
* Returns the EOL marker color.
*/
public final Color getEOLMarkerColor() {
return eolMarkerColor;
}
/**
* Sets the EOL marker color.
*
* @param eolMarkerColor
* The EOL marker color
*/
public final void setEOLMarkerColor(Color eolMarkerColor) {
this.eolMarkerColor = eolMarkerColor;
repaint();
}
/**
* Returns true if EOL markers are drawn, false otherwise.
*/
public final boolean getEOLMarkersPainted() {
return eolMarkers;
}
/**
* Sets if EOL markers are to be drawn.
*
* @param eolMarkers
* True if EOL markers should be drawn, false otherwise
*/
public final void setEOLMarkersPainted(boolean eolMarkers) {
this.eolMarkers = eolMarkers;
repaint();
}
/**
* Returns true if invalid lines are painted as red tildes (~), false otherwise.
*/
public boolean getInvalidLinesPainted() {
return paintInvalid;
}
/**
* Sets if invalid lines are to be painted as red tildes.
*
* @param paintInvalid
* True if invalid lines should be drawn, false otherwise
*/
public void setInvalidLinesPainted(boolean paintInvalid) {
this.paintInvalid = paintInvalid;
}
/**
* Adds a custom highlight painter.
*
* @param highlight
* The highlight
*/
public void addCustomHighlight(Highlight highlight) {
highlight.init(textArea, highlights.size() > 0 ? highlights.firstElement() : null);
highlights.insertElementAt(highlight, 0);
repaint();
}
/**
* Adds a custom highlight painter.
*
* @param highlight
* The highlight
*/
public void removeCustomHighlight(Highlight highlight) {
highlights.remove(highlight);
repaint();
}
/**
* Adds a custom highlight painter.
*
* @param highlight
* The highlight
*/
@SuppressWarnings("unchecked")
public void removeAllCustomHighlight() {
for (Highlight highlight : (Vector<Highlight>) highlights.clone()) {
removeCustomHighlight(highlight);
}
repaint();
}
/**
* Highlight interface.
*/
public interface Highlight {
/**
* Called after the highlight painter has been added.
*
* @param textArea
* The text area
* @param next
* The painter this one should delegate to
*/
void init(JEditTextArea textArea, Highlight next);
/**
* This should paint the highlight and delgate to the next highlight painter.
*
* @param gfx
* The graphics context
* @param line
* The line number
* @param y
* The y co-ordinate of the line
*/
void paintHighlight(Graphics gfx, int line, int y);
/**
* Returns the tool tip to display at the specified location. If this highlighter doesn't know what to display, it should delegate
* to the next highlight painter.
*
* @param evt
* The mouse event
*/
String getToolTipText(MouseEvent evt);
}
/**
* Returns the tool tip to display at the specified location.
*
* @param evt
* The mouse event
*/
@Override
public String getToolTipText(MouseEvent evt) {
if (highlights.size() > 0) {
return highlights.firstElement().getToolTipText(evt);
} else {
return null;
}
}
/**
* Returns the font metrics used by this component.
*/
public FontMetrics getFontMetrics() {
return fm;
}
/**
* Sets the font for this component. This is overridden to update the cached font metrics and to recalculate which lines are visible.
*
* @param font
* The font
*/
@Override
public void setFont(Font font) {
super.setFont(font);
fm = Toolkit.getDefaultToolkit().getFontMetrics(font);
textArea.recalculateVisibleLines();
}
private int oldCursorX = -1;
private int oldCursorY = -1;
/**
* Repaints the text.
*
* @param g
* The graphics context
*/
@Override
public void paint(Graphics gfx) {
int newCursorX = textArea.getCursorX();
int newCursorY = textArea.getCursorY();
if (newCursorX != oldCursorX || newCursorY != oldCursorY) {
oldCursorX = textArea.getCursorX();
oldCursorY = textArea.getCursorY();
for (CursorPositionListener l : textArea._cursorPositionListeners) {
l.positionChanged(oldCursorX, oldCursorY);
}
}
tabSize = fm.charWidth(' ') * ((Integer) textArea.getDocument().getProperty(PlainDocument.tabSizeAttribute)).intValue();
Rectangle clipRect = gfx.getClipBounds();
gfx.setColor(getBackground());
gfx.fillRect(clipRect.x, clipRect.y, clipRect.width, clipRect.height);
// We don't use yToLine() here because that method doesn't
// return lines past the end of the document
int height = fm.getHeight();
int firstLine = textArea.getFirstLine();
int firstInvalid = firstLine + clipRect.y / height;
// Because the clipRect's height is usually an even multiple
// of the font height, we subtract 1 from it, otherwise one
// too many lines will always be painted.
int lastInvalid = firstLine + (clipRect.y + clipRect.height - 1) / height;
try {
TokenMarker tokenMarker = textArea.getDocument().getTokenMarker();
int x = textArea.getHorizontalOffset();
for (int line = firstInvalid; line <= lastInvalid; line++) {
paintLine(gfx, tokenMarker, line, x);
}
if (tokenMarker != null && tokenMarker.isNextLineRequested()) {
int h = clipRect.y + clipRect.height;
repaint(0, h, getWidth(), getHeight() - h);
}
} catch (Exception e) {
System.err.println("Error repainting line" + " range {" + firstInvalid + "," + lastInvalid + "}:");
e.printStackTrace();
}
}
/**
* Marks a line as needing a repaint.
*
* @param line
* The line to invalidate
*/
public final void invalidateLine(int line) {
repaint(0, textArea.lineToY(line) + fm.getMaxDescent() + fm.getLeading(), getWidth(), fm.getHeight());
}
/**
* Marks a range of lines as needing a repaint.
*
* @param firstLine
* The first line to invalidate
* @param lastLine
* The last line to invalidate
*/
public final void invalidateLineRange(int firstLine, int lastLine) {
repaint(0, textArea.lineToY(firstLine) + fm.getMaxDescent() + fm.getLeading(), getWidth(),
(lastLine - firstLine + 1) * fm.getHeight());
}
/**
* Repaints the lines containing the selection.
*/
public final void invalidateSelectedLines() {
invalidateLineRange(textArea.getSelectionStartLine(), textArea.getSelectionEndLine());
}
/**
* Implementation of TabExpander interface. Returns next tab stop after a specified point.
*
* @param x
* The x co-ordinate
* @param tabOffset
* Ignored
* @return The next tab stop after <i>x</i>
*/
@Override
public float nextTabStop(float x, int tabOffset) {
int offset = textArea.getHorizontalOffset();
int ntabs = tabSize != 0 ? ((int) x - offset) / tabSize : 0;
return (ntabs + 1) * tabSize + offset;
}
/**
* Returns the painter's preferred size.
*/
@Override
public Dimension getPreferredSize() {
Dimension dim = new Dimension();
dim.width = fm.charWidth('w') * cols;
dim.height = fm.getHeight() * rows;
return dim;
}
/**
* Returns the painter's minimum size.
*/
@Override
public Dimension getMinimumSize() {
return getPreferredSize();
}
// package-private members
int currentLineIndex;
Token currentLineTokens;
protected Segment currentLine;
// protected members
protected JEditTextArea textArea;
protected SyntaxStyle[] styles;
protected Color caretColor;
protected Color selectionColor;
protected Color lineHighlightColor;
protected Color bracketHighlightColor;
protected Color eolMarkerColor;
protected boolean blockCaret;
protected boolean lineHighlight;
protected boolean bracketHighlight;
protected boolean paintInvalid;
protected boolean eolMarkers;
protected int cols;
protected int rows;
protected int tabSize;
protected FontMetrics fm;
protected Vector<Highlight> highlights;
protected void paintLine(Graphics gfx, TokenMarker tokenMarker, int line, int x) {
Font defaultFont = getFont();
Color defaultColor = getForeground();
currentLineIndex = line;
int y = textArea.lineToY(line);
if (line < 0 || line >= textArea.getLineCount()) {
if (paintInvalid) {
paintHighlight(gfx, line, y);
styles[Token.INVALID].setGraphicsFlags(gfx, defaultFont);
gfx.drawString("~", 0, y + fm.getHeight());
}
} else if (tokenMarker == null) {
paintPlainLine(gfx, line, defaultFont, defaultColor, x, y);
} else {
paintSyntaxLine(gfx, tokenMarker, line, defaultFont, defaultColor, x, y);
}
}
protected void paintPlainLine(Graphics gfx, int line, Font defaultFont, Color defaultColor, int x, int y) {
paintHighlight(gfx, line, y);
textArea.getLineText(line, currentLine);
gfx.setFont(defaultFont);
gfx.setColor(defaultColor);
y += fm.getHeight();
x = Utilities.drawTabbedText(currentLine, x, y, gfx, this, 0);
if (eolMarkers) {
gfx.setColor(eolMarkerColor);
gfx.drawString(".", x, y);
}
}
protected void paintSyntaxLine(Graphics gfx, TokenMarker tokenMarker, int line, Font defaultFont, Color defaultColor, int x, int y) {
textArea.getLineText(currentLineIndex, currentLine);
currentLineTokens = tokenMarker.markTokens(currentLine, currentLineIndex);
paintHighlight(gfx, line, y);
gfx.setFont(defaultFont);
gfx.setColor(defaultColor);
y += fm.getHeight();
x = SyntaxUtilities.paintSyntaxLine(currentLine, currentLineTokens, styles, this, gfx, x, y);
if (eolMarkers) {
gfx.setColor(eolMarkerColor);
gfx.drawString(".", x, y);
}
}
protected void paintHighlight(Graphics gfx, int line, int y) {
if (line >= textArea.getSelectionStartLine() && line <= textArea.getSelectionEndLine()) {
paintLineHighlight(gfx, line, y);
}
for (Highlight h : highlights) {
h.paintHighlight(gfx, line, y);
}
if (bracketHighlight && line == textArea.getBracketLine()) {
paintBracketHighlight(gfx, line, y);
}
if (line == textArea.getCaretLine()) {
paintCaret(gfx, line, y);
}
}
protected void paintLineHighlight(Graphics gfx, int line, int y) {
int height = fm.getHeight();
y += fm.getLeading() + fm.getMaxDescent();
int selectionStart = textArea.getSelectionStart();
int selectionEnd = textArea.getSelectionEnd();
if (selectionStart == selectionEnd) {
if (lineHighlight) {
gfx.setColor(lineHighlightColor);
gfx.fillRect(0, y, getWidth(), height);
}
} else {
gfx.setColor(selectionColor);
int selectionStartLine = textArea.getSelectionStartLine();
int selectionEndLine = textArea.getSelectionEndLine();
int lineStart = textArea.getLineStartOffset(line);
int x1, x2;
if (textArea.isSelectionRectangular()) {
int lineLen = textArea.getLineLength(line);
x1 = textArea._offsetToX(line, Math.min(lineLen, selectionStart - textArea.getLineStartOffset(selectionStartLine)));
x2 = textArea._offsetToX(line, Math.min(lineLen, selectionEnd - textArea.getLineStartOffset(selectionEndLine)));
if (x1 == x2) {
x2++;
}
} else if (selectionStartLine == selectionEndLine) {
x1 = textArea._offsetToX(line, selectionStart - lineStart);
x2 = textArea._offsetToX(line, selectionEnd - lineStart);
} else if (line == selectionStartLine) {
x1 = textArea._offsetToX(line, selectionStart - lineStart);
x2 = getWidth();
} else if (line == selectionEndLine) {
x1 = 0;
x2 = textArea._offsetToX(line, selectionEnd - lineStart);
} else {
x1 = 0;
x2 = getWidth();
}
// "inlined" min/max()
gfx.fillRect(x1 > x2 ? x2 : x1, y, x1 > x2 ? x1 - x2 : x2 - x1, height);
}
}
protected void paintBracketHighlight(Graphics gfx, int line, int y) {
int position = textArea.getBracketPosition();
if (position == -1) {
return;
}
y += fm.getLeading() + fm.getMaxDescent();
int x = textArea._offsetToX(line, position);
gfx.setColor(bracketHighlightColor);
// Hack!!! Since there is no fast way to get the character
// from the bracket matching routine, we use ( since all
// brackets probably have the same width anyway
gfx.drawRect(x, y, fm.charWidth('(') - 1, fm.getHeight() - 1);
}
protected void paintCaret(Graphics gfx, int line, int y) {
if (textArea.isCaretVisible()) {
int offset = textArea.getCaretPosition() - textArea.getLineStartOffset(line);
int caretX = textArea._offsetToX(line, offset);
int caretWidth = blockCaret || textArea.isOverwriteEnabled() ? fm.charWidth('w') : 1;
y += fm.getLeading() + fm.getMaxDescent();
int height = fm.getHeight();
gfx.setColor(caretColor);
if (textArea.isOverwriteEnabled()) {
gfx.fillRect(caretX, y + height - 1, caretWidth, 1);
} else {
gfx.drawRect(caretX, y, caretWidth - 1, height - 1);
}
}
}
public int getColumns() {
return cols;
}
public void setColumns(int columns) {
this.cols = columns;
}
public int getRows() {
return rows;
}
public void setRows(int rows) {
this.rows = rows;
}
}