/*
* 02/26/2004
*
* SyntaxScheme.java - The set of colors and tokens used by an RSyntaxTextArea
* to color tokens.
*
* This library is distributed under a modified BSD license. See the included
* RSyntaxTextArea.License.txt file for details.
*/
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.<p>
* You can use this class to programmatically set the fonts and colors used in
* an RSyntaxTextArea, but for more powerful, externalized control, consider
* using {@link Theme}s instead.
*
* @author Robert Futrell
* @version 1.0
* @see Theme
*/
public class SyntaxScheme implements Cloneable, TokenTypes {
private 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[DEFAULT_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) {
this(baseFont, true);
}
/**
* 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.
* @param fontStyles Whether bold and italic should be used in the scheme
* (vs. all tokens using a plain font).
*/
public SyntaxScheme(Font baseFont, boolean fontStyles) {
styles = new Style[DEFAULT_NUM_TOKEN_TYPES];
restoreDefaults(baseFont, fontStyles);
}
/**
* 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.
*/
@Override
public Object clone() {
SyntaxScheme shcs = null;
try {
shcs = (SyntaxScheme)super.clone();
} catch (CloneNotSupportedException cnse) { // Never happens
cnse.printStackTrace();
return null;
}
shcs.styles = new Style[styles.length];
for (int i=0; i<styles.length; 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.
*/
@Override
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);
}
/**
* Returns the specified style.
*
* @param index The index of the style.
* @return The style.
* @see #setStyle(int, Style)
* @see #getStyleCount()
*/
public Style getStyle(int index) {
return styles[index];
}
/**
* Returns the number of styles.
*
* @return The number of styles.
* @see #getStyle(int)
*/
public int getStyleCount() {
return styles.length;
}
/**
* Used by third party implementors e.g. SquirreL SQL. Most applications do
* not need to call this method.
* <p>
* Note that the returned array is not a copy of the style data; editing the
* array will modify the styles used by any <code>RSyntaxTextArea</code>
* using this scheme.
*
* @return The style array.
* @see #setStyles(Style[])
*/
public Style[] getStyles() {
return styles;
}
/**
* 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.
*/
@Override
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.<p>
*
* Consider using the {@link Theme} class for saving and loading RSTA
* styles rather than using this API.
*
* @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 SyntaxSchemeLoader.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.<p>
*
* Consider using the {@link Theme} class for saving and loading RSTA
* styles rather than using this API.
*
* @param string A string generated from {@link #toCommaSeparatedString()}.
* @return A color scheme.
* @see #toCommaSeparatedString()
*/
public static SyntaxScheme loadFromString(String string) {
return loadFromString(string, DEFAULT_NUM_TOKEN_TYPES);
}
/**
* Loads a syntax highlighting color scheme from a string created from
* <code>toCommaSeparatedString</code>. This method is useful for saving
* and restoring color schemes.<p>
*
* Consider using the {@link Theme} class for saving and loading RSTA
* styles rather than using this API.
*
* @param string A string generated from {@link #toCommaSeparatedString()}.
* @param tokenTypeCount The number of token types saved in this string.
* This should be the number of token types saved by your custom
* SyntaxScheme subclass,
* or {@link TokenTypes#DEFAULT_NUM_TOKEN_TYPES} if you used the
* standard implementation (which most people will).
* @return A color scheme.
* @see #loadFromString(String)
* @see #toCommaSeparatedString()
*/
public static SyntaxScheme loadFromString(String string,
int tokenTypeCount) {
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 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);
}
// Use StyleContext to create fonts to get composite fonts for
// Asian glyphs.
StyleContext sc = StyleContext.getDefaultStyleContext();
// 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 = sc.getFont(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) {
restoreDefaults(baseFont, true);
}
/**
* 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.
* @param fontStyles Whether bold and italic should be used in the scheme
* (vs. all tokens using a plain font).
*/
public void restoreDefaults(Font baseFont, boolean fontStyles) {
// Colors used by tokens.
Color comment = new Color(0,128,0);
Color docComment = new Color(164,0,0);
Color markupComment = new Color(0, 96, 0);
Color keyword = Color.BLUE;
Color dataType = new Color(0,128,128);
Color function = new Color(173,128,0);
Color preprocessor = new Color(128,128,128);
Color operator = new Color(128, 64, 64);
Color regex = new Color(0,128,164);
Color variable = new Color(255,153,0);
Color literalNumber = new Color(100,0,200);
Color literalString = new Color(220,0,156);
Color error = new Color(148,148,0);
// (Possible) special font styles for keywords and comments.
if (baseFont==null) {
baseFont = RSyntaxTextArea.getDefaultFont();
}
Font commentFont = baseFont;
Font keywordFont = baseFont;
if (fontStyles) {
// WORKAROUND for Sun JRE bug 6282887 (Asian font bug in 1.4/1.5)
// That bug seems to be hidden now, see 6289072 instead.
StyleContext sc = StyleContext.getDefaultStyleContext();
Font boldFont = sc.getFont(baseFont.getFamily(), Font.BOLD,
baseFont.getSize());
Font italicFont = sc.getFont(baseFont.getFamily(), Font.ITALIC,
baseFont.getSize());
commentFont = italicFont;//baseFont.deriveFont(Font.ITALIC);
keywordFont = boldFont;//baseFont.deriveFont(Font.BOLD);
}
styles[COMMENT_EOL] = new Style(comment, null, commentFont);
styles[COMMENT_MULTILINE] = new Style(comment, null, commentFont);
styles[COMMENT_DOCUMENTATION] = new Style(docComment, null, commentFont);
styles[COMMENT_KEYWORD] = new Style(new Color(255,152,0), null, commentFont);
styles[COMMENT_MARKUP] = new Style(Color.gray, null, commentFont);
styles[RESERVED_WORD] = new Style(keyword, null, keywordFont);
styles[RESERVED_WORD_2] = new Style(keyword, null, keywordFont);
styles[FUNCTION] = new Style(function);
styles[LITERAL_BOOLEAN] = new Style(literalNumber);
styles[LITERAL_NUMBER_DECIMAL_INT] = new Style(literalNumber);
styles[LITERAL_NUMBER_FLOAT] = new Style(literalNumber);
styles[LITERAL_NUMBER_HEXADECIMAL] = new Style(literalNumber);
styles[LITERAL_STRING_DOUBLE_QUOTE] = new Style(literalString);
styles[LITERAL_CHAR] = new Style(literalString);
styles[LITERAL_BACKQUOTE] = new Style(literalString);
styles[DATA_TYPE] = new Style(dataType, null, keywordFont);
styles[VARIABLE] = new Style(variable);
styles[REGEX] = new Style(regex);
styles[ANNOTATION] = new Style(Color.gray);
styles[IDENTIFIER] = new Style(null);
styles[WHITESPACE] = new Style(Color.gray);
styles[SEPARATOR] = new Style(Color.RED);
styles[OPERATOR] = new Style(operator);
styles[PREPROCESSOR] = new Style(preprocessor);
styles[MARKUP_TAG_DELIMITER] = new Style(Color.RED);
styles[MARKUP_TAG_NAME] = new Style(Color.BLUE);
styles[MARKUP_TAG_ATTRIBUTE] = new Style(new Color(63,127,127));
styles[MARKUP_TAG_ATTRIBUTE_VALUE]= new Style(literalString);
styles[MARKUP_COMMENT] = new Style(markupComment, null, commentFont);
styles[MARKUP_DTD] = new Style(function);
styles[MARKUP_PROCESSING_INSTRUCTION] = new Style(preprocessor);
styles[MARKUP_CDATA] = new Style(new Color(0xcc6600));
styles[MARKUP_CDATA_DELIMITER] = new Style(new Color(0x008080));
styles[MARKUP_ENTITY_REFERENCE] = new Style(dataType);
styles[ERROR_IDENTIFIER] = new Style(error);
styles[ERROR_NUMBER_FORMAT] = new Style(error);
styles[ERROR_STRING_DOUBLE] = new Style(error);
styles[ERROR_CHAR] = new Style(error);
// Issue #34: If an application modifies TokenTypes to add new built-in
// token types, we'll get NPEs if not all styles are initialized.
for (int i=0; i<styles.length; i++) {
if (styles[i]==null) {
styles[i] = new Style();
}
}
}
/**
* Sets a style to use when rendering a token type.
*
* @param type The token type.
* @param style The style for the token type.
* @see #getStyle(int)
*/
public void setStyle(int type, Style style) {
styles[type] = style;
}
/**
* Used by third party implementors e.g. SquirreL SQL. Most applications do
* not need to call this method; individual styles can be set via
* {@link #setStyle(int, Style)}.
*
* @param styles The new array of styles to use. Note that this should
* have length of at least
* {@link TokenTypes#DEFAULT_NUM_TOKEN_TYPES}.
* @see #setStyle(int, Style)
* @see #getStyles()
*/
public void setStyles(Style[] styles) {
this.styles = styles;
}
/**
* 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.
* @see #loadFromString(String)
*/
public String toCommaSeparatedString() {
StringBuilder sb = new StringBuilder(VERSION);
sb.append(',');
for (int i=0; i<styles.length; 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 SyntaxSchemeLoader extends DefaultHandler {
private Font baseFont;
private SyntaxScheme scheme;
public SyntaxSchemeLoader(Font baseFont) {
scheme = new SyntaxScheme(baseFont);
}
public static SyntaxScheme load(Font baseFont, InputStream in)
throws IOException {
SyntaxSchemeLoader parser = null;
try {
XMLReader reader = XMLReaderFactory.createXMLReader();
parser = new SyntaxSchemeLoader(baseFont);
parser.baseFont = baseFont;
reader.setContentHandler(parser);
InputSource is = new InputSource(in);
is.setEncoding("UTF-8");
reader.parse(is);
} catch (SAXException se) {
throw new IOException(se.toString());
}
return parser.scheme;
}
@Override
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;
}
}
}
}
}
}