source: other-projects/rsyntax-textarea/src/java/org/fife/ui/rsyntaxtextarea/SyntaxScheme.java@ 25584

Last change on this file since 25584 was 25584, checked in by davidb, 12 years ago

Initial cut an a text edit area for GLI that supports color syntax highlighting

File size: 19.5 KB
Line 
1/*
2 * 02/26/2004
3 *
4 * SyntaxScheme.java - The set of colors and tokens used by an RSyntaxTextArea
5 * to color tokens.
6 *
7 * This library is distributed under a modified BSD license. See the included
8 * RSyntaxTextArea.License.txt file for details.
9 */
10package org.fife.ui.rsyntaxtextarea;
11
12import java.awt.Color;
13import java.awt.Font;
14import java.awt.Graphics2D;
15import java.io.IOException;
16import java.io.InputStream;
17import java.lang.reflect.Field;
18import javax.swing.text.StyleContext;
19import org.xml.sax.Attributes;
20import org.xml.sax.InputSource;
21import org.xml.sax.SAXException;
22import org.xml.sax.XMLReader;
23import org.xml.sax.helpers.DefaultHandler;
24import org.xml.sax.helpers.XMLReaderFactory;
25
26
27/**
28 * The set of colors and styles used by an <code>RSyntaxTextArea</code> to
29 * color tokens. You can use this class to programmatically set the fonts
30 * and colors used in an RSyntaxTextArea, but for more powerful, externalized
31 * control, consider using {@link Theme}s instead.
32 *
33 * @author Robert Futrell
34 * @version 1.0
35 * @see Theme
36 */
37public class SyntaxScheme implements Cloneable, TokenTypes {
38
39 private Style[] styles;
40
41 private static final String VERSION = "*ver1";
42
43
44 /**
45 * Creates a color scheme that either has all color values set to
46 * a default value or set to <code>null</code>.
47 *
48 * @param useDefaults If <code>true</code>, all color values will
49 * be set to default colors; if <code>false</code>, all colors
50 * will be initially <code>null</code>.
51 */
52 public SyntaxScheme(boolean useDefaults) {
53 styles = new Style[NUM_TOKEN_TYPES];
54 if (useDefaults) {
55 restoreDefaults(null);
56 }
57 }
58
59
60 /**
61 * Creates a default color scheme.
62 *
63 * @param baseFont The base font to use. Keywords will be a bold version
64 * of this font, and comments will be an italicized version of this
65 * font.
66 */
67 public SyntaxScheme(Font baseFont) {
68 this(baseFont, true);
69 }
70
71
72 /**
73 * Creates a default color scheme.
74 *
75 * @param baseFont The base font to use. Keywords will be a bold version
76 * of this font, and comments will be an italicized version of this
77 * font.
78 * @param fontStyles Whether bold and italic should be used in the scheme
79 * (vs. all tokens using a plain font).
80 */
81 public SyntaxScheme(Font baseFont, boolean fontStyles) {
82 styles = new Style[NUM_TOKEN_TYPES];
83 restoreDefaults(baseFont, fontStyles);
84 }
85
86
87 /**
88 * Changes the "base font" for this syntax scheme. This is called by
89 * <code>RSyntaxTextArea</code> when its font changes via
90 * <code>setFont()</code>. This looks for tokens that use a derivative of
91 * the text area's old font (but bolded and/or italicized) and make them
92 * use the new font with those stylings instead. This is desirable because
93 * most programmers prefer a single font to be used in their text editor,
94 * but might want bold (say for keywords) or italics.
95 *
96 * @param old The old font of the text area.
97 * @param font The new font of the text area.
98 */
99 void changeBaseFont(Font old, Font font) {
100 for (int i=0; i<styles.length; i++) {
101 Style style = styles[i];
102 if (style!=null && style.font!=null) {
103 if (style.font.getFamily().equals(old.getFamily()) &&
104 style.font.getSize()==old.getSize()) {
105 int s = style.font.getStyle(); // Keep bold or italic
106 StyleContext sc = StyleContext.getDefaultStyleContext();
107 style.font= sc.getFont(font.getFamily(), s, font.getSize());
108 }
109 }
110 }
111 }
112
113
114 /**
115 * Returns a deep copy of this color scheme.
116 *
117 * @return The copy.
118 */
119 public Object clone() {
120 SyntaxScheme shcs = null;
121 try {
122 shcs = (SyntaxScheme)super.clone();
123 } catch (CloneNotSupportedException cnse) { // Never happens
124 cnse.printStackTrace();
125 return null;
126 }
127 shcs.styles = new Style[NUM_TOKEN_TYPES];
128 for (int i=0; i<NUM_TOKEN_TYPES; i++) {
129 Style s = styles[i];
130 if (s!=null) {
131 shcs.styles[i] = (Style)s.clone();
132 }
133 }
134 return shcs;
135 }
136
137
138 /**
139 * Tests whether this color scheme is the same as another color scheme.
140 *
141 * @param otherScheme The color scheme to compare to.
142 * @return <code>true</code> if this color scheme and
143 * <code>otherScheme</code> are the same scheme;
144 * <code>false</code> otherwise.
145 */
146 public boolean equals(Object otherScheme) {
147
148 // No need for null check; instanceof takes care of this for us,
149 // i.e. "if (!(null instanceof Foo))" evaluates to "true".
150 if (!(otherScheme instanceof SyntaxScheme)) {
151 return false;
152 }
153
154 Style[] otherSchemes = ((SyntaxScheme)otherScheme).styles;
155
156 int length = styles.length;
157 for (int i=0; i<length; i++) {
158 if (styles[i]==null) {
159 if (otherSchemes[i]!=null) {
160 return false;
161 }
162 }
163 else if (!styles[i].equals(otherSchemes[i])) {
164 return false;
165 }
166 }
167 return true;
168
169 }
170
171
172 /**
173 * Returns a hex string representing an RGB color, of the form
174 * <code>"$rrggbb"</code>.
175 *
176 * @param c The color.
177 * @return The string representation of the color.
178 */
179 private static final String getHexString(Color c) {
180 return "$" + Integer.toHexString((c.getRGB() & 0xffffff)+0x1000000).
181 substring(1);
182 }
183
184
185 /**
186 * Returns the specified style.
187 *
188 * @param index The index of the style.
189 * @return The style.
190 * @see #setStyle(int, Style)
191 * @see #getStyleCount()
192 */
193 public Style getStyle(int index) {
194 return styles[index];
195 }
196
197
198 /**
199 * Returns the number of styles.
200 *
201 * @return The number of styles.
202 * @see #getStyle(int)
203 */
204 public int getStyleCount() {
205 return styles.length;
206 }
207
208
209 /**
210 * This is implemented to be consistent with {@link #equals(Object)}.
211 * This is a requirement to keep FindBugs happy.
212 *
213 * @return The hash code for this object.
214 */
215 public int hashCode() {
216 // Keep me fast. Iterating over *all* syntax schemes contained is
217 // probably much slower than a "bad" hash code here.
218 int hashCode = 0;
219 int count = styles.length;
220 for (int i=0; i<count; i++) {
221 if (styles[i]!=null) {
222 hashCode ^= styles[i].hashCode();
223 break;
224 }
225 }
226 return hashCode;
227 }
228
229
230 /**
231 * Loads a syntax scheme from an input stream.
232 *
233 * @param baseFont The font to use as the "base" for the syntax scheme.
234 * If this is <code>null</code>, a default monospaced font is used.
235 * @param in The stream to load from. It is up to the caller to close this
236 * stream when they are done.
237 * @return The syntax scheme.
238 * @throws IOException If an IO error occurs.
239 */
240 public static SyntaxScheme load(Font baseFont, InputStream in)
241 throws IOException {
242 if (baseFont==null) {
243 baseFont = RSyntaxTextArea.getDefaultFont();
244 }
245 return XmlParser.load(baseFont, in);
246 }
247
248
249 /**
250 * Loads a syntax highlighting color scheme from a string created from
251 * <code>toCommaSeparatedString</code>. This method is useful for saving
252 * and restoring color schemes.
253 *
254 * @param string A string generated from {@link #toCommaSeparatedString()}.
255 * @return A color scheme.
256 */
257 public static SyntaxScheme loadFromString(String string) {
258
259 SyntaxScheme scheme = new SyntaxScheme(true);
260
261 try {
262
263 if (string!=null) {
264
265 String[] tokens = string.split(",", -1);
266
267 // Check the version string, use defaults if incompatible
268 if (tokens.length==0 || !VERSION.equals(tokens[0])) {
269 return scheme; // Still set to defaults
270 }
271
272 int tokenTypeCount = NUM_TOKEN_TYPES;
273 int tokenCount = tokenTypeCount*7 + 1; // Version string
274 if (tokens.length!=tokenCount) {
275 throw new Exception(
276 "Not enough tokens in packed color scheme: expected " +
277 tokenCount + ", found " + tokens.length);
278 }
279
280 // Loop through each token style. Format:
281 // "index,(fg|-),(bg|-),(t|f),((font,style,size)|(-,,))"
282 for (int i=0; i<tokenTypeCount; i++) {
283
284 int pos = i*7 + 1;
285 int integer = Integer.parseInt(tokens[pos]); // == i
286 if (integer!=i)
287 throw new Exception("Expected " + i + ", found " +
288 integer);
289
290 Color fg = null; String temp = tokens[pos+1];
291 if (!"-".equals(temp)) { // "-" => keep fg as null
292 fg = stringToColor(temp);
293 }
294 Color bg = null; temp = tokens[pos+2];
295 if (!"-".equals(temp)) { // "-" => keep bg as null
296 bg = stringToColor(temp);
297 }
298
299 // Check for "true" or "false" since we don't want to
300 // accidentally suck in an int representing the next
301 // packed color, and any string != "true" means false.
302 temp = tokens[pos+3];
303 if (!"t".equals(temp) && !"f".equals(temp))
304 throw new Exception("Expected 't' or 'f', found " + temp);
305 boolean underline = "t".equals(temp);
306
307 Font font = null;
308 String family = tokens[pos+4];
309 if (!"-".equals(family)) {
310 font = new Font(family,
311 Integer.parseInt(tokens[pos+5]), // style
312 Integer.parseInt(tokens[pos+6])); // size
313 }
314 scheme.styles[i] = new Style(fg, bg, font, underline);
315
316 }
317
318 }
319
320 } catch (Exception e) {
321 e.printStackTrace();
322 }
323
324 return scheme;
325
326 }
327
328
329 void refreshFontMetrics(Graphics2D g2d) {
330 // It is assumed that any rendering hints are already applied to g2d.
331 for (int i=0; i<styles.length; i++) {
332 Style s = styles[i];
333 if (s!=null) {
334 s.fontMetrics = s.font==null ? null :
335 g2d.getFontMetrics(s.font);
336 }
337 }
338 }
339
340
341 /**
342 * Restores all colors and fonts to their default values.
343 *
344 * @param baseFont The base font to use when creating this scheme. If
345 * this is <code>null</code>, then a default monospaced font is
346 * used.
347 */
348 public void restoreDefaults(Font baseFont) {
349 restoreDefaults(baseFont, true);
350 }
351
352
353 /**
354 * Restores all colors and fonts to their default values.
355 *
356 * @param baseFont The base font to use when creating this scheme. If
357 * this is <code>null</code>, then a default monospaced font is
358 * used.
359 * @param fontStyles Whether bold and italic should be used in the scheme
360 * (vs. all tokens using a plain font).
361 */
362 public void restoreDefaults(Font baseFont, boolean fontStyles) {
363
364 // Colors used by tokens.
365 Color comment = new Color(0,128,0);
366 Color docComment = new Color(164,0,0);
367 Color keyword = Color.BLUE;
368 Color function = new Color(173,128,0);
369 Color preprocessor = new Color(128,64,64);
370 Color regex = new Color(0,128,164);
371 Color variable = new Color(255,153,0);
372 Color literalNumber = new Color(100,0,200);
373 Color literalString = new Color(220,0,156);
374 Color error = new Color(148,148,0);
375
376 // (Possible) special font styles for keywords and comments.
377 if (baseFont==null) {
378 baseFont = RSyntaxTextArea.getDefaultFont();
379 }
380 Font commentFont = baseFont;
381 Font keywordFont = baseFont;
382 if (fontStyles) {
383 // WORKAROUND for Sun JRE bug 6282887 (Asian font bug in 1.4/1.5)
384 StyleContext sc = StyleContext.getDefaultStyleContext();
385 Font boldFont = sc.getFont(baseFont.getFamily(), Font.BOLD,
386 baseFont.getSize());
387 Font italicFont = sc.getFont(baseFont.getFamily(), Font.ITALIC,
388 baseFont.getSize());
389 commentFont = italicFont;//baseFont.deriveFont(Font.ITALIC);
390 keywordFont = boldFont;//baseFont.deriveFont(Font.BOLD);
391 }
392
393 styles[COMMENT_EOL] = new Style(comment, null, commentFont);
394 styles[COMMENT_MULTILINE] = new Style(comment, null, commentFont);
395 styles[COMMENT_DOCUMENTATION] = new Style(docComment, null, commentFont);
396 styles[COMMENT_KEYWORD] = new Style(new Color(255,152,0), null, commentFont);
397 styles[COMMENT_MARKUP] = new Style(Color.gray, null, commentFont);
398 styles[RESERVED_WORD] = new Style(keyword, null, keywordFont);
399 styles[RESERVED_WORD_2] = new Style(keyword, null, keywordFont);
400 styles[FUNCTION] = new Style(function);
401 styles[LITERAL_BOOLEAN] = new Style(literalNumber);
402 styles[LITERAL_NUMBER_DECIMAL_INT] = new Style(literalNumber);
403 styles[LITERAL_NUMBER_FLOAT] = new Style(literalNumber);
404 styles[LITERAL_NUMBER_HEXADECIMAL] = new Style(literalNumber);
405 styles[LITERAL_STRING_DOUBLE_QUOTE] = new Style(literalString);
406 styles[LITERAL_CHAR] = new Style(literalString);
407 styles[LITERAL_BACKQUOTE] = new Style(literalString);
408 styles[DATA_TYPE] = new Style(new Color(0,128,128));
409 styles[VARIABLE] = new Style(variable);
410 styles[REGEX] = new Style(regex);
411 styles[ANNOTATION] = new Style(Color.gray);
412 styles[IDENTIFIER] = new Style(null);
413 styles[WHITESPACE] = new Style(Color.gray);
414 styles[SEPARATOR] = new Style(Color.RED);
415 styles[OPERATOR] = new Style(preprocessor);
416 styles[PREPROCESSOR] = new Style(Color.gray);
417 styles[MARKUP_TAG_DELIMITER] = new Style(Color.RED);
418 styles[MARKUP_TAG_NAME] = new Style(Color.BLUE);
419 styles[MARKUP_TAG_ATTRIBUTE] = new Style(new Color(63,127,127));
420 styles[MARKUP_TAG_ATTRIBUTE_VALUE]= new Style(literalString);
421 styles[MARKUP_PROCESSING_INSTRUCTION] = new Style(preprocessor);
422 styles[MARKUP_CDATA] = new Style(variable);
423 styles[ERROR_IDENTIFIER] = new Style(error);
424 styles[ERROR_NUMBER_FORMAT] = new Style(error);
425 styles[ERROR_STRING_DOUBLE] = new Style(error);
426 styles[ERROR_CHAR] = new Style(error);
427
428 }
429
430
431 /**
432 * Sets a style to use when rendering a token type.
433 *
434 * @param type The token type.
435 * @param style The style for the token type.
436 * @see #getStyle(int)
437 */
438 public void setStyle(int type, Style style) {
439 styles[type] = style;
440 }
441
442
443 /**
444 * Returns the color represented by a string. If the first char in the
445 * string is '<code>$</code>', it is assumed to be in hex, otherwise it is
446 * assumed to be decimal. So, for example, both of these:
447 * <pre>
448 * "$00ff00"
449 * "65280"
450 * </pre>
451 * will return <code>new Color(0, 255, 0)</code>.
452 *
453 * @param s The string to evaluate.
454 * @return The color.
455 */
456 private static final Color stringToColor(String s) {
457 // Check for decimal as well as hex, for backward
458 // compatibility (fix from GwynEvans on forums)
459 char ch = s.charAt(0);
460 return new Color((ch=='$' || ch=='#') ?
461 Integer.parseInt(s.substring(1),16) :
462 Integer.parseInt(s));
463 }
464
465
466 /**
467 * Returns this syntax highlighting scheme as a comma-separated list of
468 * values as follows:
469 * <ul>
470 * <li>If a color is non-null, it is added as a 24-bit integer
471 * of the form <code>((r<<16) | (g<<8) | (b))</code>; if it is
472 * <code>null</code>, it is added as "<i>-,</i>".
473 * <li>The font and style (bold/italic) is added as an integer like so:
474 * "<i>family,</i> <i>style,</i> <i>size</i>".
475 * <li>The entire syntax highlighting scheme is thus one long string of
476 * color schemes of the format "<i>i,[fg],[bg],uline,[style]</i>,
477 * where:
478 * <ul>
479 * <li><code>i</code> is the index of the syntax scheme.
480 * <li><i>fg</i> and <i>bg</i> are the foreground and background
481 * colors for the scheme, and may be null (represented by
482 * <code>-</code>).
483 * <li><code>uline</code> is whether or not the font should be
484 * underlined, and is either <code>t</code> or <code>f</code>.
485 * <li><code>style</code> is the <code>family,style,size</code>
486 * triplet described above.
487 * </ul>
488 * </ul>
489 *
490 * @return A string representing the rgb values of the colors.
491 */
492 public String toCommaSeparatedString() {
493
494 StringBuffer sb = new StringBuffer(VERSION);
495 sb.append(',');
496
497 for (int i=0; i<NUM_TOKEN_TYPES; i++) {
498
499 sb.append(i).append(',');
500
501 Style ss = styles[i];
502 if (ss==null) { // Only true for i==0 (NULL token)
503 sb.append("-,-,f,-,,,");
504 continue;
505 }
506
507 Color c = ss.foreground;
508 sb.append(c!=null ? (getHexString(c) + ",") : "-,");
509 c = ss.background;
510 sb.append(c!=null ? (getHexString(c) + ",") : "-,");
511
512 sb.append(ss.underline ? "t," : "f,");
513
514 Font font = ss.font;
515 if (font!=null) {
516 sb.append(font.getFamily()).append(',').
517 append(font.getStyle()).append(',').
518 append(font.getSize()).append(',');
519 }
520 else {
521 sb.append("-,,,");
522 }
523
524 }
525
526 return sb.substring(0,sb.length()-1); // Take off final ','.
527
528 }
529
530
531 /**
532 * Loads a <code>SyntaxScheme</code> from an XML file.
533 */
534 private static class XmlParser extends DefaultHandler {
535
536 private Font baseFont;
537 private SyntaxScheme scheme;
538
539 public XmlParser(Font baseFont) {
540 scheme = new SyntaxScheme(baseFont);
541 }
542
543 /**
544 * Creates the XML reader to use. Note that in 1.4 JRE's, the reader
545 * class wasn't defined by default, but in 1.5+ it is.
546 *
547 * @return The XML reader to use.
548 */
549 private static XMLReader createReader() throws IOException {
550 XMLReader reader = null;
551 try {
552 reader = XMLReaderFactory.createXMLReader();
553 } catch (SAXException e) {
554 // Happens in JRE 1.4.x; 1.5+ define the reader class properly
555 try {
556 reader = XMLReaderFactory.createXMLReader(
557 "org.apache.crimson.parser.XMLReaderImpl");
558 } catch (SAXException se) {
559 throw new IOException(se.toString());
560 }
561 }
562 return reader;
563 }
564
565 public static SyntaxScheme load(Font baseFont,
566 InputStream in) throws IOException {
567 XMLReader reader = createReader();
568 XmlParser parser = new XmlParser(baseFont);
569 parser.baseFont = baseFont;
570 reader.setContentHandler(parser);
571 InputSource is = new InputSource(in);
572 is.setEncoding("UTF-8");
573 try {
574 reader.parse(is);
575 } catch (SAXException se) {
576 throw new IOException(se.toString());
577 }
578 return parser.scheme;
579 }
580
581 public void startElement(String uri, String localName, String qName,
582 Attributes attrs) {
583
584 if ("style".equals(qName)) {
585
586 String type = attrs.getValue("token");
587 Field field = null;
588 try {
589 field = Token.class.getField(type);
590 } catch (RuntimeException re) {
591 throw re; // FindBugs
592 } catch (Exception e) {
593 System.err.println("Invalid token type: " + type);
594 return;
595 }
596
597 if (field.getType()==int.class) {
598
599 int index = 0;
600 try {
601 index = field.getInt(scheme);
602 } catch (IllegalArgumentException e) {
603 e.printStackTrace();
604 return;
605 } catch (IllegalAccessException e) {
606 e.printStackTrace();
607 return;
608 }
609
610 String fgStr = attrs.getValue("fg");
611 if (fgStr!=null) {
612 Color fg = stringToColor(fgStr);
613 scheme.styles[index].foreground = fg;
614 }
615
616 String bgStr = attrs.getValue("bg");
617 if (bgStr!=null) {
618 Color bg = stringToColor(bgStr);
619 scheme.styles[index].background = bg;
620 }
621
622 boolean styleSpecified = false;
623 boolean bold = false;
624 boolean italic = false;
625 String boldStr = attrs.getValue("bold");
626 if (boldStr!=null) {
627 bold = Boolean.valueOf(boldStr).booleanValue();
628 styleSpecified = true;
629 }
630 String italicStr = attrs.getValue("italic");
631 if (italicStr!=null) {
632 italic = Boolean.valueOf(italicStr).booleanValue();
633 styleSpecified = true;
634 }
635 if (styleSpecified) {
636 int style = 0;
637 if (bold) { style |= Font.BOLD; }
638 if (italic) { style |= Font.ITALIC; }
639 scheme.styles[index].font = baseFont.deriveFont(style);
640 }
641
642 String ulineStr = attrs.getValue("underline");
643 if (ulineStr!=null) {
644 boolean uline= Boolean.valueOf(ulineStr).booleanValue();
645 scheme.styles[index].underline = uline;
646 }
647
648 }
649
650 }
651
652 }
653
654 }
655
656
657}
Note: See TracBrowser for help on using the repository browser.