/*
* 10/28/2004
*
* DefaultToken.java - The default token used in syntax highlighting.
* Copyright (C) 2004 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.rsyntaxtextarea;
import java.awt.Color;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import javax.swing.text.Segment;
import javax.swing.text.TabExpander;
import javax.swing.text.Utilities;
/**
* The default token used in the <code>org.fife.ui.rsyntaxtextarea</code> syntax package. This token type paints itself
* as you would expect, and properly accounts for rendering hints (anti-aliasing and fractional font metrics).
* <p>
*
* The current implementation paints as follows:
* <ul>
* <li>The first tab, if any, is found in the token.</li>
* <li>All characters up to that tab, if it exists, are painted as a group. If no tab was found, all characters in the
* token are painted.</li>
* <li>If a tab was found, its width is calculated and it is painted.</li>
* <li>Repeat until all characters are painted.</li>
* </ul>
* This method allows for rendering hints to be honored, since all possible characters are painted in a group. However,
* adjacent tokens will not have their "touching" characters rendered with rendering hints.
* <p>
*
* A problem with this implementation is that FontMetrics.charsWidth() is still used to calculate the width of a group
* of chars painted. Thus, the group of characters will be painted with the rendering hints specified, but the following
* tab (or group of characters if the current group was the end of a token) will not necessarily be painted at the
* proper x-coordinate (as FontMetrics.charsWidth() returns an <code>int</code> and not a <code>float</code>). The way
* around this would be to calculate the token's width in such a way that a float is returned (Font.getStringBounds()?).
*
* @author Robert Futrell
* @version 0.5
* @see Token
* @see VisibleWhitespaceToken
*/
public class DefaultToken extends Token {
/**
* Creates a "null token." The token itself is not null; rather, it signifies that it is the last token in a linked
* list of tokens and that it is not part of a "multi-line token."
*/
public DefaultToken() {
super();
}
/**
* Constructor.
*
* @param line
* The segment from which to get the token.
* @param beg
* The first character's position in <code>line</code>.
* @param end
* The last character's position in <code>line</code>.
* @param startOffset
* The offset into the document at which this token begins.
* @param type
* A token type listed as "generic" above.
*/
public DefaultToken(final Segment line, final int beg, final int end,
final int startOffset, final int type) {
this(line.array, beg, end, startOffset, type);
}
/**
* Constructor.
*
* @param line
* The segment from which to get the token.
* @param beg
* The first character's position in <code>line</code>.
* @param end
* The last character's position in <code>line</code>.
* @param startOffset
* The offset into the document at which this token begins.
* @param type
* A token type listed as "generic" above.
*/
public DefaultToken(final char[] line, final int beg, final int end,
final int startOffset, final int type) {
super(line, beg, end, startOffset, type);
}
/**
* Determines the offset into this token list (i.e., into the document) that covers pixel location <code>x</code> if
* the token list starts at pixel location <code>x0</code>
* <p>
* . This method will return the document position "closest" to the x-coordinate (i.e., if they click on the
* "right-half" of the <code>w</code> in <code>awe</code>, the caret will be placed in between the <code>w</code>
* and <code>e</code>; similarly, clicking on the left-half places the caret between the <code>a</code> and
* <code>w</code>). This makes it useful for methods such as <code>viewToModel</code> found in
* <code>javax.swing.text.View</code> subclasses.
*
* @param textArea
* The text area from which the token list was derived.
* @param e
* How to expand tabs.
* @param x0
* The pixel x-location that is the beginning of <code>tokenList</code>.
* @param x
* The pixel-position for which you want to get the corresponding offset.
* @return The position (in the document, NOT into the token list!) that covers the pixel location. If
* <code>tokenList</code> is <code>null</code> or has type <code>Token.NULL</code>, then <code>-1</code is
* returned; the caller should recognize this and return the actual end position of the (empty) line.
*/
public int getListOffset(RSyntaxTextArea textArea, TabExpander e,
float x0, float x) {
// If the coordinate in question is before this line's start, quit.
if (x0 >= x)
return offset;
float currX = x0; // x-coordinate of current char.
float nextX = x0; // x-coordinate of next char.
float stableX = x0; // Cached ending x-coord. of last tab or token.
Token token = this;
int last = offset;
FontMetrics fm = null;
while (token != null && token.isPaintable()) {
fm = textArea.getFontMetricsForTokenType(token.type);
char[] text = token.text;
int start = token.textOffset;
int end = start + token.textCount;
for (int i = start; i < end; i++) {
currX = nextX;
if (text[i] == '\t') {
nextX = e.nextTabStop(nextX, 0);
stableX = nextX; // Cache ending x-coord. of tab.
start = i + 1; // Do charsWidth() from next char.
}
else {
nextX = stableX + fm.charsWidth(text, start, i - start + 1);
}
if (x >= currX && x < nextX) {
if ((x - currX) < (nextX - x)) {
return last + i - token.textOffset;
}
return last + i + 1 - token.textOffset;
}
}
stableX = nextX; // Cache ending x-coordinate of token.
last += token.textCount;
token = token.getNextToken();
}
// If we didn't find anything, return the end position of the text.
return last;
}
/**
* Returns the width of a specified number of characters in this token. For example, for the token "while",
* specifying a value of <code>3</code> here returns the width of the "whi" portion of the token.
* <p>
*
* @param numChars
* The number of characters for which to get the width.
* @param textArea
* The text area in which this token is being painted.
* @param e
* How to expand tabs. This value cannot be <code>null</code>.
* @param x0
* The pixel-location at which this token begins. This is needed because of tabs.
* @return The width of the specified number of characters in this token.
* @see #getWidth
*/
public float getWidthUpTo(int numChars, RSyntaxTextArea textArea,
TabExpander e, float x0) {
float width = x0;
FontMetrics fm = textArea.getFontMetricsForTokenType(type);
if (fm != null) {
int w;
int currentStart = textOffset;
int endBefore = textOffset + numChars;
for (int i = currentStart; i < endBefore; i++) {
if (text[i] == '\t') {
// Since TokenMaker implementations usually group all
// adjacent whitespace into a single token, there
// aren't usually any characters to compute a width
// for here, so we check before calling.
w = i - currentStart;
if (w > 0)
width += fm.charsWidth(text, currentStart, w);
currentStart = i + 1;
width = e.nextTabStop(width, 0);
}
}
// Most (non-whitespace) tokens will have characters at this
// point to get the widths for, so we don't check for w>0 (mini-
// optimization).
w = endBefore - currentStart;
width += fm.charsWidth(text, currentStart, w);
}
return width - x0;
}
/**
* Returns the bounding box for the specified document location. The location must be in the specified token list.
*
* @param textArea
* The text area from which the token list was derived.
* @param e
* How to expand tabs.
* @param pos
* The position in the document for which to get the bounding box in the view.
* @param x0
* The pixel x-location that is the beginning of <code>tokenList</code>.
* @param rect
* The rectangle in which we'll be returning the results. This object is reused to keep from frequent
* memory allocations.
* @return The bounding box for the specified position in the model.
*/
public Rectangle listOffsetToView(RSyntaxTextArea textArea, TabExpander e,
int pos, int x0, Rectangle rect) {
int stableX = x0; // Cached ending x-coord. of last tab or token.
Token token = this;
FontMetrics fm = null;
Segment s = new Segment();
while (token != null && token.isPaintable()) {
fm = textArea.getFontMetricsForTokenType(token.type);
if (fm == null) {
return rect; // Don't return null as things'll error.
}
char[] text = token.text;
int start = token.textOffset;
int end = start + token.textCount;
// If this token contains the position for which to get the
// bounding box...
if (token.containsPosition(pos)) {
s.array = token.text;
s.offset = token.textOffset;
s.count = pos - token.offset;
// Must use this (actually fm.charWidth()), and not
// fm.charsWidth() for returned value to match up with where
// text is actually painted on OS X!
int w = Utilities.getTabbedTextWidth(s, fm, stableX, e,
token.offset);
rect.x = stableX + w;
end = token.documentToToken(pos);
if (text[end] == '\t') {
rect.width = fm.charWidth(' ');
}
else {
rect.width = fm.charWidth(text[end]);
}
return rect;
}
// If this token does not contain the position for which to get
// the bounding box...
else {
s.array = token.text;
s.offset = token.textOffset;
s.count = token.textCount;
stableX += Utilities.getTabbedTextWidth(s, fm, stableX, e,
token.offset);
}
token = token.getNextToken();
}
// If we didn't find anything, we're at the end of the line. Return
// a width of 1 (so selection highlights don't extend way past line's
// text). A ConfigurableCaret will know to paint itself with a larger
// width.
rect.x = stableX;
rect.width = 1;
return rect;
}
/**
* Paints this token.
*
* @param g
* The graphics context in which to paint.
* @param x
* The x-coordinate at which to paint.
* @param y
* The y-coordinate at which to paint.
* @param host
* The text area this token is in.
* @param e
* How to expand tabs.
* @param clipStart
* The left boundary of the clip rectangle in which we're painting. This optimizes painting by allowing
* us to not paint when this token is "to the left" of the clip rectangle.
* @return The x-coordinate representing the end of the painted text.
*/
public float paint(Graphics2D g, float x, float y, RSyntaxTextArea host,
TabExpander e, float clipStart) {
int origX = (int) x;
int end = textOffset + textCount;
float nextX = x;
int flushLen = 0;
int flushIndex = textOffset;
Color fg = host.getForegroundForToken(this);
Color bg = host.getBackgroundForTokenType(type);
g.setFont(host.getFontForTokenType(type));
FontMetrics fm = host.getFontMetricsForTokenType(type);
for (int i = textOffset; i < end; i++) {
switch (text[i]) {
case '\t':
nextX = e.nextTabStop(
x + fm.charsWidth(text, flushIndex, flushLen), 0);
if (bg != null) {
paintBackground(x, y, nextX - x, fm.getHeight(),
g, fm.getAscent(), host, bg);
}
if (flushLen > 0) {
g.setColor(fg);
g.drawChars(text, flushIndex, flushLen, (int) x, (int) y);
flushLen = 0;
}
flushIndex = i + 1;
x = nextX;
break;
default:
flushLen += 1;
break;
}
}
nextX = x + fm.charsWidth(text, flushIndex, flushLen);
if (flushLen > 0 && nextX >= clipStart) {
if (bg != null) {
paintBackground(x, y, nextX - x, fm.getHeight(),
g, fm.getAscent(), host, bg);
}
g.setColor(fg);
g.drawChars(text, flushIndex, flushLen, (int) x, (int) y);
}
if (host.getUnderlineForToken(this)) {
g.setColor(fg);
int y2 = (int) (y + 1);
g.drawLine(origX, y2, (int) nextX, y2);
}
return nextX;
}
}