/*
* 02/26/2004
*
* SyntaxScheme.java - The set of colors and tokens used by an RSyntaxTextArea
* to color tokens.
* 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.Font;
import java.awt.Graphics2D;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import javax.swing.text.StyleContext;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;
import org.xml.sax.helpers.XMLReaderFactory;
/**
* The set of colors and styles used by an <code>RSyntaxTextArea</code> to color tokens.
*
* @author Robert Futrell
* @version 1.0
*/
public class SyntaxScheme implements Cloneable {
public Style[] styles;
private static final String VERSION = "*ver1";
/**
* Creates a color scheme that either has all color values set to a default value or set to <code>null</code>.
*
* @param useDefaults
* If <code>true</code>, all color values will be set to default colors; if <code>false</code>, all
* colors will be initially <code>null</code>.
*/
public SyntaxScheme(boolean useDefaults) {
styles = new Style[Token.NUM_TOKEN_TYPES];
if (useDefaults) {
restoreDefaults(null);
}
}
/**
* Creates a default color scheme.
*
* @param baseFont
* The base font to use. Keywords will be a bold version of this font, and comments will be an italicized
* version of this font.
*/
public SyntaxScheme(Font baseFont) {
styles = new Style[Token.NUM_TOKEN_TYPES];
restoreDefaults(baseFont);
}
/**
* Changes the "base font" for this syntax scheme. This is called by <code>RSyntaxTextArea</code> when its font
* changes via <code>setFont()</code>. This looks for tokens that use a derivative of the text area's old font (but
* bolded and/or italicized) and make them use the new font with those stylings instead. This is desirable because
* most programmers prefer a single font to be used in their text editor, but might want bold (say for keywords) or
* italics.
*
* @param old
* The old font of the text area.
* @param font
* The new font of the text area.
*/
void changeBaseFont(Font old, Font font) {
for (int i = 0; i < styles.length; i++) {
Style style = styles[i];
if (style != null && style.font != null) {
if (style.font.getFamily().equals(old.getFamily()) &&
style.font.getSize() == old.getSize()) {
int s = style.font.getStyle(); // Keep bold or italic
StyleContext sc = StyleContext.getDefaultStyleContext();
style.font = sc.getFont(font.getFamily(), s, font.getSize());
}
}
}
}
/**
* Returns a deep copy of this color scheme.
*
* @return The copy.
*/
public Object clone() {
SyntaxScheme shcs = null;
try {
shcs = (SyntaxScheme) super.clone();
} catch (CloneNotSupportedException cnse) { // Never happens
cnse.printStackTrace();
return null;
}
shcs.styles = new Style[Token.NUM_TOKEN_TYPES];
for (int i = 0; i < Token.NUM_TOKEN_TYPES; i++) {
Style s = styles[i];
if (s != null) {
shcs.styles[i] = (Style) s.clone();
}
}
return shcs;
}
/**
* Tests whether this color scheme is the same as another color scheme.
*
* @param otherScheme
* The color scheme to compare to.
* @return <code>true</code> if this color scheme and <code>otherScheme</code> are the same scheme;
* <code>false</code> otherwise.
*/
public boolean equals(Object otherScheme) {
// No need for null check; instanceof takes care of this for us,
// i.e. "if (!(null instanceof Foo))" evaluates to "true".
if (!(otherScheme instanceof SyntaxScheme)) {
return false;
}
Style[] otherSchemes = ((SyntaxScheme) otherScheme).styles;
int length = styles.length;
for (int i = 0; i < length; i++) {
if (styles[i] == null) {
if (otherSchemes[i] != null) {
return false;
}
}
else if (!styles[i].equals(otherSchemes[i])) {
return false;
}
}
return true;
}
/**
* Returns a hex string representing an RGB color, of the form <code>"$rrggbb"</code>.
*
* @param c
* The color.
* @return The string representation of the color.
*/
private static final String getHexString(Color c) {
return "$" + Integer.toHexString((c.getRGB() & 0xffffff) + 0x1000000).
substring(1);
}
/**
* This is implemented to be consistent with {@link #equals(Object)}. This is a requirement to keep FindBugs happy.
*
* @return The hash code for this object.
*/
public int hashCode() {
// Keep me fast. Iterating over *all* syntax schemes contained is
// probably much slower than a "bad" hash code here.
int hashCode = 0;
int count = styles.length;
for (int i = 0; i < count; i++) {
if (styles[i] != null) {
hashCode ^= styles[i].hashCode();
break;
}
}
return hashCode;
}
/**
* Loads a syntax scheme from an input stream.
*
* @param baseFont
* The font to use as the "base" for the syntax scheme. If this is <code>null</code>, a default
* monospaced font is used.
* @param in
* The stream to load from. It is up to the caller to close this stream when they are done.
* @return The syntax scheme.
* @throws IOException
* If an IO error occurs.
*/
public static SyntaxScheme load(Font baseFont, InputStream in)
throws IOException {
if (baseFont == null) {
baseFont = RSyntaxTextArea.getDefaultFont();
}
return XmlParser.load(baseFont, in);
}
/**
* Loads a syntax highlighting color scheme from a string created from <code>toCommaSeparatedString</code>. This
* method is useful for saving and restoring color schemes.
*
* @param string
* A string generated from {@link #toCommaSeparatedString()}.
* @return A color scheme.
*/
public static SyntaxScheme loadFromString(String string) {
SyntaxScheme scheme = new SyntaxScheme(true);
try {
if (string != null) {
String[] tokens = string.split(",", -1);
// Check the version string, use defaults if incompatible
if (tokens.length == 0 || !VERSION.equals(tokens[0])) {
return scheme; // Still set to defaults
}
int tokenTypeCount = Token.NUM_TOKEN_TYPES;
int tokenCount = tokenTypeCount * 7 + 1; // Version string
if (tokens.length != tokenCount) {
throw new Exception(
"Not enough tokens in packed color scheme: expected " +
tokenCount + ", found " + tokens.length);
}
// Loop through each token style. Format:
// "index,(fg|-),(bg|-),(t|f),((font,style,size)|(-,,))"
for (int i = 0; i < tokenTypeCount; i++) {
int pos = i * 7 + 1;
int integer = Integer.parseInt(tokens[pos]); // == i
if (integer != i)
throw new Exception("Expected " + i + ", found " +
integer);
Color fg = null;
String temp = tokens[pos + 1];
if (!"-".equals(temp)) { // "-" => keep fg as null
fg = stringToColor(temp);
}
Color bg = null;
temp = tokens[pos + 2];
if (!"-".equals(temp)) { // "-" => keep bg as null
bg = stringToColor(temp);
}
// Check for "true" or "false" since we don't want to
// accidentally suck in an int representing the next
// packed color, and any string != "true" means false.
temp = tokens[pos + 3];
if (!"t".equals(temp) && !"f".equals(temp))
throw new Exception("Expected 't' or 'f', found " + temp);
boolean underline = "t".equals(temp);
Font font = null;
String family = tokens[pos + 4];
if (!"-".equals(family)) {
font = new Font(family,
Integer.parseInt(tokens[pos + 5]), // style
Integer.parseInt(tokens[pos + 6])); // size
}
scheme.styles[i] = new Style(fg, bg, font, underline);
}
}
} catch (Exception e) {
e.printStackTrace();
}
return scheme;
}
void refreshFontMetrics(Graphics2D g2d) {
// It is assumed that any rendering hints are already applied to g2d.
for (int i = 0; i < styles.length; i++) {
Style s = styles[i];
if (s != null) {
s.fontMetrics = s.font == null ? null :
g2d.getFontMetrics(s.font);
}
}
}
/**
* Restores all colors and fonts to their default values.
*
* @param baseFont
* The base font to use when creating this scheme. If this is <code>null</code>, then a default
* monospaced font is used.
*/
public void restoreDefaults(Font baseFont) {
// Colors used by tokens.
Color comment = new Color(0, 128, 0);
Color docComment = new Color(164, 0, 0);
Color keyword = Color.BLUE;
Color function = new Color(173, 128, 0);
Color literalNumber = new Color(100, 0, 200);
Color literalString = new Color(220, 0, 156);
Color error = new Color(148, 148, 0);
// Special fonts.
if (baseFont == null) {
baseFont = RSyntaxTextArea.getDefaultFont();
}
// WORKAROUND for Sun JRE bug 6282887 (Asian font bug in 1.4/1.5)
StyleContext sc = StyleContext.getDefaultStyleContext();
Font boldFont = sc.getFont(baseFont.getFamily(), Font.BOLD,
baseFont.getSize());
Font italicFont = sc.getFont(baseFont.getFamily(), Font.ITALIC,
baseFont.getSize());
Font commentFont = italicFont;// baseFont.deriveFont(Font.ITALIC);
Font keywordFont = boldFont;// baseFont.deriveFont(Font.BOLD);
styles[Token.COMMENT_EOL] = new Style(comment, null, commentFont);
styles[Token.COMMENT_MULTILINE] = new Style(comment, null, commentFont);
styles[Token.COMMENT_DOCUMENTATION] = new Style(docComment, null, commentFont);
styles[Token.RESERVED_WORD] = new Style(keyword, null, keywordFont);
styles[Token.FUNCTION] = new Style(function, null);
styles[Token.LITERAL_BOOLEAN] = new Style(literalNumber, null);
styles[Token.LITERAL_NUMBER_DECIMAL_INT] = new Style(literalNumber, null);
styles[Token.LITERAL_NUMBER_FLOAT] = new Style(literalNumber, null);
styles[Token.LITERAL_NUMBER_HEXADECIMAL] = new Style(literalNumber, null);
styles[Token.LITERAL_STRING_DOUBLE_QUOTE] = new Style(literalString, null);
styles[Token.LITERAL_CHAR] = new Style(literalString, null);
styles[Token.LITERAL_BACKQUOTE] = new Style(literalString, null);
styles[Token.DATA_TYPE] = new Style(new Color(0, 128, 128), null);
styles[Token.VARIABLE] = new Style(new Color(255, 153, 0), null);
styles[Token.IDENTIFIER] = new Style(null, null);
styles[Token.WHITESPACE] = new Style(Color.gray, null);
styles[Token.SEPARATOR] = new Style(Color.RED, null);
styles[Token.OPERATOR] = new Style(new Color(128, 64, 64), null);
styles[Token.PREPROCESSOR] = new Style(new Color(128, 128, 128), null);
styles[Token.MARKUP_TAG_DELIMITER] = new Style(Color.RED, null);
styles[Token.MARKUP_TAG_NAME] = new Style(Color.BLUE, null);
styles[Token.MARKUP_TAG_ATTRIBUTE] = new Style(new Color(63, 127, 127), null);
// styles[Token.ERROR] = null;
styles[Token.ERROR_IDENTIFIER] = new Style(error, null);
styles[Token.ERROR_NUMBER_FORMAT] = new Style(error, null);
styles[Token.ERROR_STRING_DOUBLE] = new Style(error, null);
styles[Token.ERROR_CHAR] = new Style(error, null);
}
/**
* Sets a style to use when rendering a token type.
*
* @param type
* The token type.
* @param style
* The style for the token type.
*/
public void setStyle(int type, Style style) {
styles[type] = style;
}
/**
* Returns the color represented by a string. If the first char in the string is '<code>$</code>', it is assumed to
* be in hex, otherwise it is assumed to be decimal. So, for example, both of these:
*
* <pre>
* "$00ff00"
* "65280"
* </pre>
*
* will return <code>new Color(0, 255, 0)</code>.
*
* @param s
* The string to evaluate.
* @return The color.
*/
private static final Color stringToColor(String s) {
// Check for decimal as well as hex, for backward
// compatibility (fix from GwynEvans on forums)
char ch = s.charAt(0);
return new Color((ch == '$' || ch == '#') ?
Integer.parseInt(s.substring(1), 16) :
Integer.parseInt(s));
}
/**
* Returns this syntax highlighting scheme as a comma-separated list of values as follows:
* <ul>
* <li>If a color is non-null, it is added as a 24-bit integer of the form <code>((r<<16) | (g<<8) | (b))</code>; if
* it is <code>null</code>, it is added as "<i>-,</i>".
* <li>The font and style (bold/italic) is added as an integer like so: "<i>family,</i> <i>style,</i> <i>size</i>".
* <li>The entire syntax highlighting scheme is thus one long string of color schemes of the format
* "<i>i,[fg],[bg],uline,[style]</i>, where:
* <ul>
* <li><code>i</code> is the index of the syntax scheme.
* <li><i>fg</i> and <i>bg</i> are the foreground and background colors for the scheme, and may be null (represented
* by <code>-</code>).
* <li><code>uline</code> is whether or not the font should be underlined, and is either <code>t</code> or
* <code>f</code>.
* <li><code>style</code> is the <code>family,style,size</code> triplet described above.
* </ul>
* </ul>
*
* @return A string representing the rgb values of the colors.
*/
public String toCommaSeparatedString() {
StringBuffer sb = new StringBuffer(VERSION);
sb.append(',');
for (int i = 0; i < Token.NUM_TOKEN_TYPES; i++) {
sb.append(i).append(',');
Style ss = styles[i];
if (ss == null) { // Only true for i==0 (NULL token)
sb.append("-,-,f,-,,,");
continue;
}
Color c = ss.foreground;
sb.append(c != null ? (getHexString(c) + ",") : "-,");
c = ss.background;
sb.append(c != null ? (getHexString(c) + ",") : "-,");
sb.append(ss.underline ? "t," : "f,");
Font font = ss.font;
if (font != null) {
sb.append(font.getFamily()).append(',').
append(font.getStyle()).append(',').
append(font.getSize()).append(',');
}
else {
sb.append("-,,,");
}
}
return sb.substring(0, sb.length() - 1); // Take off final ','.
}
/**
* Loads a <code>SyntaxScheme</code> from an XML file.
*/
private static class XmlParser extends DefaultHandler {
private Font baseFont;
private SyntaxScheme scheme;
public XmlParser(Font baseFont) {
scheme = new SyntaxScheme(baseFont);
}
/**
* Creates the XML reader to use. Note that in 1.4 JRE's, the reader class wasn't defined by default, but in
* 1.5+ it is.
*
* @return The XML reader to use.
*/
private static XMLReader createReader() throws IOException {
XMLReader reader = null;
try {
reader = XMLReaderFactory.createXMLReader();
} catch (SAXException e) {
// Happens in JRE 1.4.x; 1.5+ define the reader class properly
try {
reader = XMLReaderFactory.createXMLReader(
"org.apache.crimson.parser.XMLReaderImpl");
} catch (SAXException se) {
throw new IOException(se.toString());
}
}
return reader;
}
public static SyntaxScheme load(Font baseFont,
InputStream in) throws IOException {
XMLReader reader = createReader();
XmlParser parser = new XmlParser(baseFont);
parser.baseFont = baseFont;
reader.setContentHandler(parser);
InputSource is = new InputSource(in);
is.setEncoding("UTF-8");
try {
reader.parse(is);
} catch (SAXException se) {
throw new IOException(se.toString());
}
return parser.scheme;
}
public void startElement(String uri, String localName, String qName,
Attributes attrs) {
if ("style".equals(qName)) {
String type = attrs.getValue("token");
Field field = null;
try {
field = Token.class.getField(type);
} catch (RuntimeException re) {
throw re; // FindBugs
} catch (Exception e) {
System.err.println("Invalid token type: " + type);
return;
}
if (field.getType() == int.class) {
int index = 0;
try {
index = field.getInt(scheme);
} catch (IllegalArgumentException e) {
e.printStackTrace();
return;
} catch (IllegalAccessException e) {
e.printStackTrace();
return;
}
String fgStr = attrs.getValue("fg");
if (fgStr != null) {
Color fg = stringToColor(fgStr);
scheme.styles[index].foreground = fg;
}
String bgStr = attrs.getValue("bg");
if (bgStr != null) {
Color bg = stringToColor(bgStr);
scheme.styles[index].background = bg;
}
boolean styleSpecified = false;
boolean bold = false;
boolean italic = false;
String boldStr = attrs.getValue("bold");
if (boldStr != null) {
bold = Boolean.valueOf(boldStr).booleanValue();
styleSpecified = true;
}
String italicStr = attrs.getValue("italic");
if (italicStr != null) {
italic = Boolean.valueOf(italicStr).booleanValue();
styleSpecified = true;
}
if (styleSpecified) {
int style = 0;
if (bold) {
style |= Font.BOLD;
}
if (italic) {
style |= Font.ITALIC;
}
scheme.styles[index].font = baseFont.deriveFont(style);
}
String ulineStr = attrs.getValue("underline");
if (ulineStr != null) {
boolean uline = Boolean.valueOf(ulineStr).booleanValue();
scheme.styles[index].underline = uline;
}
}
}
}
}
}