org.fife.ui.rsyntaxtextarea.SyntaxView Maven / Gradle / Ivy
/*
* 02/24/2004
*
* SyntaxView.java - The View object used by RSyntaxTextArea when word wrap is
* disabled.
*
* This library is distributed under a modified BSD license. See the included
* LICENSE file for details.
*/
package org.fife.ui.rsyntaxtextarea;
import java.awt.*;
import javax.swing.event.*;
import javax.swing.text.*;
import org.fife.ui.rsyntaxtextarea.folding.Fold;
import org.fife.ui.rsyntaxtextarea.folding.FoldManager;
/**
* The javax.swing.text.View
object used by {@link RSyntaxTextArea}
* when word wrap is disabled. It implements syntax highlighting for
* programming languages using the colors and font styles specified by the
* RSyntaxTextArea
.
*
* You don't really have to do anything to use this class, as
* {@link RSyntaxTextAreaUI} automatically sets the text area's view to be
* an instance of this class if word wrap is disabled.
*
* The tokens that specify how to paint the syntax-highlighted text are gleaned
* from the text area's {@link RSyntaxDocument}.
*
* @author Robert Futrell
* @version 0.3
*/
public class SyntaxView extends View implements TabExpander,
TokenOrientedView, RSTAView {
/**
* The default font used by the text area. If this changes we need to
* recalculate the longest line.
*/
private Font font;
/**
* Font metrics for the current font.
*/
private FontMetrics metrics;
/**
* The current longest line. This is used to calculate the preferred width
* of the view. Since the calculation is potentially expensive, we try to
* avoid it by stashing which line is currently the longest.
*/
private Element longLine;
private float longLineWidth;
private int tabSize;
private int tabBase;
/**
* Cached for each paint() call so each drawLine() call has access to it.
*/
private RSyntaxTextArea host;
/**
* Cached values to speed up the painting a tad.
*/
private int lineHeight = 0;
private int ascent;
private int clipStart;
private int clipEnd;
/**
* Temporary token used when we need to "modify" tokens for rendering
* purposes. Since tokens returned from RSyntaxDocuments are treated as
* immutable, we use this temporary token to do that work.
*/
private TokenImpl tempToken;
/**
* Constructs a new SyntaxView
wrapped around an element.
*
* @param elem The element representing the text to display.
*/
public SyntaxView(Element elem) {
super(elem);
tempToken = new TokenImpl();
}
/**
* Iterate over the lines represented by the child elements
* of the element this view represents, looking for the line
* that is the longest. The longLine variable is updated to
* represent the longest line contained. The font variable
* is updated to indicate the font used to calculate the
* longest line.
*/
void calculateLongestLine() {
Component c = getContainer();
font = c.getFont();
metrics = c.getFontMetrics(font);
tabSize = getTabSize() * metrics.charWidth(' ');
Element lines = getElement();
int n = lines.getElementCount();
for (int i=0; i longLineWidth) {
longLineWidth = w;
longLine = line;
}
}
}
/**
* Gives notification from the document that attributes were changed
* in a location that this view is responsible for.
*
* @param changes the change information from the associated document
* @param a the current allocation of the view
* @param f the factory to use to rebuild if the view has children
* @see View#changedUpdate
*/
@Override
public void changedUpdate(DocumentEvent changes, Shape a, ViewFactory f) {
updateDamage(changes, a, f);
}
/**
* Repaint the given line range.
*
* @param line0 The starting line number to repaint. This must
* be a valid line number in the model.
* @param line1 The ending line number to repaint. This must
* be a valid line number in the model.
* @param a The region allocated for the view to render into.
* @param host The component hosting the view (used to call repaint).
*/
protected void damageLineRange(int line0, int line1, Shape a,
Component host) {
if (a != null) {
Rectangle area0 = lineToRect(a, line0);
Rectangle area1 = lineToRect(a, line1);
if ((area0 != null) && (area1 != null)) {
Rectangle dmg = area0.union(area1); // damage.
host.repaint(dmg.x, dmg.y, dmg.width, dmg.height);
}
else {
host.repaint();
}
}
}
/**
* Draws the passed-in text using syntax highlighting for the current
* language. It is assumed that the entire line is either not in a
* selected region, or painting with a selection-foreground color is turned
* off.
*
* @param painter The painter to render the tokens.
* @param token The list of tokens to draw.
* @param g The graphics context in which to draw.
* @param x The x-coordinate at which to draw.
* @param y The y-coordinate at which to draw.
* @return The x-coordinate representing the end of the painted text.
*/
private float drawLine(TokenPainter painter, Token token, Graphics2D g,
float x, float y, int line) {
float nextX = x; // The x-value at the end of our text.
boolean paintBG = host.getPaintTokenBackgrounds(line, y);
while (token!=null && token.isPaintable() && nextXtoken.getOffset()) {
tempToken.copyFrom(token);
tempToken.textCount = selStart - tempToken.getOffset();
nextX = painter.paint(tempToken,g,nextX,y,host, this, clipStart);
tempToken.textCount = token.length();
tempToken.makeStartAt(selStart);
// Clone required since token and tempToken must be
// different tokens for else statement below
token = new TokenImpl(tempToken);
}
int tokenLen = token.length();
int selCount = Math.min(tokenLen, selEnd-token.getOffset());
if (selCount==tokenLen) {
nextX = painter.paintSelected(token, g, nextX,y, host,
this, clipStart, useSTC);
}
else {
tempToken.copyFrom(token);
tempToken.textCount = selCount;
nextX = painter.paintSelected(tempToken, g, nextX,y, host,
this, clipStart, useSTC);
tempToken.textCount = token.length();
tempToken.makeStartAt(token.getOffset() + selCount);
token = tempToken;
nextX = painter.paint(token, g, nextX,y, host, this,
clipStart);
}
}
// Selection ends in this token
else if (token.containsPosition(selEnd)) {
tempToken.copyFrom(token);
tempToken.textCount = selEnd - tempToken.getOffset();
nextX = painter.paintSelected(tempToken, g, nextX,y, host, this,
clipStart, useSTC);
tempToken.textCount = token.length();
tempToken.makeStartAt(selEnd);
token = tempToken;
nextX = painter.paint(token, g, nextX,y, host, this, clipStart);
}
// This token is entirely selected
else if (token.getOffset()>=selStart &&
token.getEndOffset()<=selEnd) {
nextX = painter.paintSelected(token, g, nextX,y, host, this,
clipStart, useSTC);
}
// This token is entirely unselected
else {
nextX = painter.paint(token, g, nextX,y, host, this, clipStart);
}
token = token.getNextToken();
}
// NOTE: We should re-use code from Token (paintBackground()) here,
// but don't because I'm just too lazy.
if (host.getEOLMarkersVisible()) {
g.setColor(host.getForegroundForTokenType(Token.WHITESPACE));
g.setFont(host.getFontForTokenType(Token.WHITESPACE));
g.drawString("\u00B6", nextX, y);
}
// Return the x-coordinate at the end of the painted text.
return nextX;
}
/**
* Calculates the width of the line represented by the given element.
*
* @param lineNumber The line number of the specified line in the document.
* @return The width of the line.
*/
private float getLineWidth(int lineNumber) {
Token tokenList = ((RSyntaxDocument)getDocument()).
getTokenListForLine(lineNumber);
return RSyntaxUtilities.getTokenListWidth(tokenList,
(RSyntaxTextArea)getContainer(),
this);
}
/**
* Provides a way to determine the next visually represented model
* location that one might place a caret. Some views may not be visible,
* they might not be in the same order found in the model, or they just
* might not allow access to some of the locations in the model.
*
* @param pos the position to convert >= 0
* @param a the allocated region to render into
* @param direction the direction from the current position that can
* be thought of as the arrow keys typically found on a keyboard.
* This may be SwingConstants.WEST, SwingConstants.EAST,
* SwingConstants.NORTH, or SwingConstants.SOUTH.
* @return the location within the model that best represents the next
* location visual position.
* @exception BadLocationException If the offset specified is invalid.
* @exception IllegalArgumentException for an invalid direction
*/
@Override
public int getNextVisualPositionFrom(int pos, Position.Bias b, Shape a,
int direction, Position.Bias[] biasRet)
throws BadLocationException {
return RSyntaxUtilities.getNextVisualPositionFrom(pos, b, a,
direction, biasRet, this);
}
/**
* Determines the preferred span for this view along an
* axis.
*
* @param axis may be either View.X_AXIS or View.Y_AXIS
* @return the span the view would like to be rendered into >= 0.
* Typically the view is told to render into the span
* that is returned, although there is no guarantee.
* The parent may choose to resize or break the view.
* @exception IllegalArgumentException for an invalid axis
*/
@Override
public float getPreferredSpan(int axis) {
updateMetrics();
switch (axis) {
case View.X_AXIS:
float span = longLineWidth + getRhsCorrection(); // fudge factor
if (host.getEOLMarkersVisible()) {
span += metrics.charWidth('\u00B6');
}
return span;
case View.Y_AXIS:
// We update lineHeight here as when this method is first
// called, lineHeight isn't initialized. If we don't do it
// here, we get no vertical scrollbar (as lineHeight==0).
lineHeight = host!=null ? host.getLineHeight() : lineHeight;
// return getElement().getElementCount() * lineHeight;
int visibleLineCount = getElement().getElementCount();
if (host.isCodeFoldingEnabled()) {
visibleLineCount -= host.getFoldManager().getHiddenLineCount();
}
return visibleLineCount * (float) lineHeight;
default:
throw new IllegalArgumentException("Invalid axis: " + axis);
}
}
/**
* Workaround for JTextComponents allowing the caret to be rendered
* entirely off-screen if the entire "previous" character fit entirely.
*
* @return The amount of space to add to the x-axis preferred span.
*/
private int getRhsCorrection() {
int rhsCorrection = 10;
if (host!=null) {
rhsCorrection = host.getRightHandSideCorrection();
}
return rhsCorrection;
}
/**
* Returns the tab size set for the document, defaulting to 5.
*
* @return The tab size.
*/
private int getTabSize() {
Integer i = (Integer)getDocument().getProperty(
PlainDocument.tabSizeAttribute);
int size = (i != null) ? i : 5;
return size;
}
/**
* Returns a token list for the physical line above the physical
* line containing the specified offset into the document. Note that for
* this plain (non-wrapped) view, this is simply the token list for the
* logical line above the line containing offset
, since lines
* are not wrapped.
*
* @param offset The offset in question.
* @return A token list for the physical (and in this view, logical) line
* before this one. If offset
is in the first line in
* the document, null
is returned.
*/
@Override
public Token getTokenListForPhysicalLineAbove(int offset) {
RSyntaxDocument document = (RSyntaxDocument)getDocument();
Element map = document.getDefaultRootElement();
int line = map.getElementIndex(offset);
FoldManager fm = host.getFoldManager();
if (fm==null) {
line--;
if (line>=0) {
return document.getTokenListForLine(line);
}
}
else {
line = fm.getVisibleLineAbove(line);
if (line>=0) {
return document.getTokenListForLine(line);
}
}
// int line = map.getElementIndex(offset) - 1;
// if (line>=0)
// return document.getTokenListForLine(line);
return null;
}
/**
* Returns a token list for the physical line below the physical
* line containing the specified offset into the document. Note that for
* this plain (non-wrapped) view, this is simply the token list for the
* logical line below the line containing offset
, since lines
* are not wrapped.
*
* @param offset The offset in question.
* @return A token list for the physical (and in this view, logical) line
* after this one. If offset
is in the last physical
* line in the document, null
is returned.
*/
@Override
public Token getTokenListForPhysicalLineBelow(int offset) {
RSyntaxDocument document = (RSyntaxDocument)getDocument();
Element map = document.getDefaultRootElement();
int lineCount = map.getElementCount();
int line = map.getElementIndex(offset);
if (!host.isCodeFoldingEnabled()) {
if (line=0 && line
*
* This is implemented to subtract the width of the second character, as
* this view's modelToView
actually returns the width of the
* character instead of "1" or "0" like the View implementations in
* javax.swing.text
. Thus, if we don't override this method,
* the View
implementation will return one character's width
* too much for its consumers (implementations of
* javax.swing.text.Highlighter
).
*
* @param p0 the position of the first character (>=0)
* @param b0 The bias of the first character position, toward the previous
* character or the next character represented by the offset, in
* case the position is a boundary of two views; b0
* will have one of these values:
*
* -
Position.Bias.Forward
* -
Position.Bias.Backward
*
* @param p1 the position of the last character (>=0)
* @param b1 the bias for the second character position, defined
* one of the legal values shown above
* @param a the area of the view, which encompasses the requested region
* @return the bounding box which is a union of the region specified
* by the first and last character positions
* @exception BadLocationException if the given position does
* not represent a valid location in the associated document
* @exception IllegalArgumentException if b0
or
* b1
are not one of the
* legal Position.Bias
values listed above
* @see View#viewToModel
*/
@Override
public Shape modelToView(int p0, Position.Bias b0,
int p1, Position.Bias b1,
Shape a) throws BadLocationException {
Shape s0 = modelToView(p0, a, b0);
Shape s1;
if (p1 ==getEndOffset()) {
try {
s1 = modelToView(p1, a, b1);
} catch (BadLocationException ble) {
s1 = null;
}
if (s1 == null) {
// Assume extends left to right.
Rectangle alloc = (a instanceof Rectangle) ? (Rectangle)a :
a.getBounds();
s1 = new Rectangle(alloc.x + alloc.width - 1, alloc.y,
1, alloc.height);
}
}
else {
s1 = modelToView(p1, a, b1);
}
Rectangle r0 = s0 instanceof Rectangle ? (Rectangle)s0 : s0.getBounds();
Rectangle r1 = s1 instanceof Rectangle ? (Rectangle)s1 : s1.getBounds();
if (r0.y != r1.y) {
// If it spans lines, force it to be the width of the view.
Rectangle alloc = (a instanceof Rectangle) ? (Rectangle)a :
a.getBounds();
r0.x = alloc.x;
r0.width = alloc.width;
}
r0.add(r1);
// The next line is the only difference between this method and
// View's implementation. We're subtracting the width of the second
// character. This is because this method is used by Highlighter
// implementations to get the area to "highlight", and if we don't do
// this, one character too many is highlighted thanks to our
// modelToView() implementation returning the actual width of the
// character requested!
if (p1>p0) {
r0.width -= r1.width;
}
return r0;
}
/**
* Returns the next tab stop position after a given reference position.
* This implementation does not support things like centering so it
* ignores the tabOffset argument.
*
* @param x the current position >= 0
* @param tabOffset the position within the text stream
* that the tab occurred at >= 0.
* @return the tab stop, measured in points >= 0
*/
@Override
public float nextTabStop(float x, int tabOffset) {
if (tabSize == 0) {
return x;
}
int ntabs = (((int)x) - tabBase) / tabSize;
return tabBase + ((ntabs + 1f) * tabSize);
}
/**
* Actually paints the text area. Only lines that have been damaged are
* repainted.
*
* @param g The graphics context with which to paint.
* @param a The allocated region in which to render.
*/
@Override
public void paint(Graphics g, Shape a) {
RSyntaxDocument document = (RSyntaxDocument)getDocument();
Rectangle alloc = a.getBounds();
tabBase = alloc.x;
host = (RSyntaxTextArea)getContainer();
Rectangle clip = g.getClipBounds();
// An attempt to speed things up for files with long lines. Note that
// this will actually slow things down a bit for the common case of
// regular-length lines, but it doesn't make a perceivable difference.
clipStart = clip.x;
clipEnd = clipStart + clip.width;
lineHeight = host.getLineHeight();
ascent = host.getMaxAscent();//metrics.getAscent();
int heightAbove = clip.y - alloc.y;
int linesAbove = Math.max(0, heightAbove / lineHeight);
FoldManager fm = host.getFoldManager();
linesAbove += fm.getHiddenLineCountAbove(linesAbove, true);
Rectangle lineArea = lineToRect(a, linesAbove);
int y = lineArea.y + ascent;
int x = lineArea.x;
Element map = getElement();
int lineCount = map.getElementCount();
// Whether token styles should always be painted, even in selections
int selStart = host.getSelectionStart();
int selEnd = host.getSelectionEnd();
RSyntaxTextAreaHighlighter h =
(RSyntaxTextAreaHighlighter)host.getHighlighter();
Graphics2D g2d = (Graphics2D)g;
Token token;
//System.err.println("Painting lines: " + linesAbove + " to " + (endLine-1));
TokenPainter painter = host.getTokenPainter();
int line = linesAbove;
//int count = 0;
while (y=selEnd ||
endOffset alloc.y + alloc.height) {
return host.getLastVisibleOffset();
}
// They're asking about a position within the coverage of this view
// vertically. So, we figure out which line the point corresponds to.
// If the line is greater than the number of lines contained, then
// simply use the last line as it represents the last possible place
// we can position to.
else {
Element map = doc.getDefaultRootElement();
lineHeight = host.getLineHeight();
int lineIndex = Math.abs((y - alloc.y) / lineHeight);//metrics.getHeight() );
FoldManager fm = host.getFoldManager();
//System.out.print("--- " + lineIndex);
lineIndex += fm.getHiddenLineCountAbove(lineIndex, true);
//System.out.println(" => " + lineIndex);
if (lineIndex >= map.getElementCount()) {
return host.getLastVisibleOffset();
}
Element line = map.getElement(lineIndex);
// If the point is to the left of the line...
if (x < alloc.x) {
return line.getStartOffset();
}
else if (x > alloc.x + alloc.width) {
return line.getEndOffset() - 1;
}
else {
// Determine the offset into the text
int p0 = line.getStartOffset();
Token tokenList = doc.getTokenListForLine(lineIndex);
tabBase = alloc.x;
int offs = tokenList.getListOffset(
(RSyntaxTextArea)getContainer(),
this, tabBase, x);
return offs!=-1 ? offs : p0;
}
} // End of else.
}
@Override
public int yForLine(Rectangle alloc, int line) throws BadLocationException {
//Rectangle lineArea = lineToRect(alloc, lineIndex);
updateMetrics();
if (metrics != null) {
// NOTE: lineHeight is not initially set here, leading to the
// current line not being highlighted when a document is first
// opened. So, we set it here just in case.
lineHeight = host!=null ? host.getLineHeight() : lineHeight;
if (host != null) {
FoldManager fm = host.getFoldManager();
if (!fm.isLineHidden(line)) {
line -= fm.getHiddenLineCountAbove(line);
return alloc.y + line * lineHeight;
}
}
}
return -1;
}
@Override
public int yForLineContaining(Rectangle alloc, int offs)
throws BadLocationException {
Element map = getElement();
int line = map.getElementIndex(offs);
return yForLine(alloc, line);
}
}