/*
* 10/28/2004
*
* VisibleWhitespaceToken.java - Token that paints special symbols for its
* whitespace characters (space and tab).
* 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.geom.Rectangle2D;
import javax.swing.text.Segment;
import javax.swing.text.TabExpander;
/**
* This token class paints spaces and tabs with special symbols so the user can see the whitespace in his document.
* Rendering hints are honored.
* <p>
*
* The current implementation paints as follows:
* <ul>
* <li>The first tab or space, if any, is found in the token.</li>
* <li>If a tab was found, all characters up to it are painted as a group.</li>
* <li>If a space was found, all characters up to and including it are painted (it is painted with a special symbol to
* denote it as a space).</li>
* <li>If neither a tab nor a whitespace was found, all characters in the token are painted.</li>
* <li>Repeat until all characters are painted.</li>
* </ul>
* This means that rendering hints are applied to all groups of characters within a token, excluding whitespace and
* tabs.
* <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 DefaultToken
*/
public class VisibleWhitespaceToken extends DefaultToken {
private Rectangle2D.Float dotRect;
/**
* 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 VisibleWhitespaceToken() {
super();
dotRect = new Rectangle2D.Float(0, 0, 1, 1);
}
/**
* 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 VisibleWhitespaceToken(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 VisibleWhitespaceToken(final char[] line, final int beg,
final int end, final int startOffset, final int type) {
super(line, beg, end, startOffset, type);
}
/**
* Paints this token, using special symbols for whitespace characters.
*
* @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 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 final 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);
int ascent = fm.getAscent();
int height = fm.getHeight();
for (int i = textOffset; i < end; i++) {
switch (text[i]) {
case '\t':
// Fill in background.
nextX = x + fm.charsWidth(text, flushIndex, flushLen);
float nextNextX = e.nextTabStop(nextX, 0);
if (bg != null) {
paintBackground(x, y, nextNextX - x, height, g,
ascent, host, bg);
}
g.setColor(fg);
// Paint chars cached before the tab.
if (flushLen > 0) {
g.drawChars(text, flushIndex, flushLen, (int) x, (int) y);
flushLen = 0;
}
flushIndex = i + 1;
/*
* if (host.showIndentGuide()) { g.setColor(Color.GRAY); int y2 = y - ascent; int end2 = y +
* fm.getDescent() - 1; while (y2<end2) { g.drawLine(nextNextX,y2, nextNextX,y2); y2 += 2; }
*/
// Draw an arrow representing the tab.
int halfHeight = height / 2;
int quarterHeight = halfHeight / 2;
int ymid = (int) y - ascent + halfHeight;
g.drawLine((int) nextX, ymid, (int) nextNextX, ymid);
g.drawLine((int) nextNextX, ymid, (int) nextNextX - 4, ymid - quarterHeight);
g.drawLine((int) nextNextX, ymid, (int) nextNextX - 4, ymid + quarterHeight);
x = nextNextX;
break;
case ' ':
// NOTE: There is a little bit of a "fudge factor"
// here when "smooth text" is enabled, as "width"
// below may well not be the width given to the space
// by fm.charsWidth() (it depends on how it places the
// space with respect to the preceding character).
// But, we assume the approximation is close enough for
// our drawing a dot for the space.
// "flushLen+1" ensures text is aligned correctly (or,
// aligned the same as in getWidth()).
nextX = x + fm.charsWidth(text, flushIndex, flushLen + 1);
int width = fm.charWidth(' ');
// Paint background.
if (bg != null) {
paintBackground(x, y, nextX - x, height, g,
ascent, host, bg);
}
g.setColor(fg);
// Paint chars before space.
if (flushLen > 0) {
g.drawChars(text, flushIndex, flushLen, (int) x, (int) y);
flushLen = 0;
}
// Paint a dot representing the space.
dotRect.x = nextX - width / 2.0f; // "2.0f" for FindBugs
dotRect.y = y - ascent + height / 2.0f; // Ditto
g.fill(dotRect);
flushIndex = i + 1;
x = nextX;
break;
case '\f':
// ???
// fall-through for now.
default:
flushLen += 1;
break;
}
}
nextX = x + fm.charsWidth(text, flushIndex, flushLen);
if (flushLen > 0 && nextX >= clipStart) {
if (bg != null) {
paintBackground(x, y, nextX - x, height, g,
ascent, 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;
}
}