001: /*=============================================================================
002: * Copyright Texas Instruments 2002. All Rights Reserved.
003: *
004: * This program is free software; you can redistribute it and/or modify
005: * it under the terms of the GNU General Public License as published by
006: * the Free Software Foundation; either version 2 of the License, or
007: * (at your option) any later version.
008: *
009: * This program is distributed in the hope that it will be useful,
010: * but WITHOUT ANY WARRANTY; without even the implied warranty of
011: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
012: * GNU General Public License for more details.
013: *
014: * You should have received a copy of the GNU General Public License
015: * along with this program; if not, write to the Free Software
016: * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
017: */
018:
019: package ti.swing.console;
020:
021: import java.awt.*;
022: import java.awt.event.*;
023: import java.util.Vector;
024: import java.util.Hashtable;
025: import java.util.Iterator;
026: import java.util.LinkedList;
027:
028: /**
029: * An input handler which provides support for color, by use of escaped
030: * sequences of characters. The escape character, and the format of the
031: * escaped character sequences, is not well defined, so the static methods
032: * {@link #fgColor}, {@link #bgColor}, and {@link #hyperlink} should be
033: * used, for example:
034: * <pre>
035: * // to make text with blue background, green foreground:
036: * out.println(
037: * ColorInputHandler.fgColor( Color.green,
038: * ColorInputHandler.bgColor( Color.blue, "Some Text" ) ) );
039: *
040: * // to make a hyperlink:
041: * out.println(
042: * ColorInputHandler.hyperlink( new Runnable() {
043: * public void run() {
044: * // do something here!
045: * }
046: * }, "Some Text" ) );
047: * </pre>
048: * Attributes can be combined and nested as needed. Attributes created by
049: * {@link #fgColor} and {@link #bgColor} are constant, and so the resulting
050: * string can be reused, written to disk, read from disk, etc. Attributes
051: * created by {@link #hyperlink} are dynamic, and can only be used once for
052: * each time they are created by calling {@link #hyperlink}.
053: *
054: * @author Rob Clark
055: * @version 0.1
056: */
057: public class ColorInputHandler extends InputAdapter {
058: static final char ESCAPE_CHAR = 0x241b; // symbol for UNICODE escape, should be safe to use
059:
060: static final char FGCOLOR_ATTR_OPEN = 1;
061: static final char FGCOLOR_ATTR_CLOSE = 2;
062: static final char BGCOLOR_ATTR_OPEN = 3;
063: static final char BGCOLOR_ATTR_CLOSE = 4;
064: static final char HYPERLINK_ATTR_OPEN = 5;
065: static final char HYPERLINK_ATTR_CLOSE = 6;
066:
067: /**
068: * Table of runnables. This is sorta cheezy, but an entry in the table is
069: * populated when {@link #hyperlink} is called, and unpopulated when an
070: * instance of this class encounters the corresponding open tag in the
071: * input stream. This means a hyperlink can only be written to a single
072: * console. It also means that there can only be a finite number of
073: * hyperlinks created but not yet scaned by an instance of this class. It
074: * is possible that the implementation changes in the future if needed,
075: * which is why the format of the attributes is not publicly defined.
076: */
077: private static Runnable[] runnables = new Runnable[64];
078:
079: /* Attribute formats:
080: * fgColor: ESCAPE_CHAR + FGCOLOR_ATTR_OPEN + r + g + b + <STR> + ESCAPE_CHAR + FGCOLOR_ATTR_CLOSE
081: * bgColor: ESCAPE_CHAR + BGCOLOR_ATTR_OPEN + r + g + b + <STR> + ESCAPE_CHAR + BGCOLOR_ATTR_CLOSE
082: * hyperlink: ESCAPE_CHAR + HYPERLINK_ATTR_OPEN + runnableIdx + <STR> + ESCAPE_CHAR + HYPERLINK_ATTR_CLOSE
083: */
084:
085: /**
086: * The console we are part of.
087: */
088: private Console console;
089:
090: /**
091: * The state machine:
092: */
093: private State state;
094:
095: /**
096: * Class Constructor.
097: *
098: * @param console the console we are adding this input handler to
099: */
100: public ColorInputHandler(Console console) {
101: super (console.getInputHandler());
102:
103: this .console = console;
104: this .state = NULL_STATE.reset();
105:
106: HyperlinkMouseHandler hmh = new HyperlinkMouseHandler();
107: console.addMouseListener(hmh);
108: console.addMouseMotionListener(hmh);
109: }
110:
111: /*=======================================================================*/
112:
113: /**
114: * Create a string with a foreground color attribute applied.
115: *
116: * @param c the color to apply
117: * @param str the string to apply it to
118: * @return a string with embedded control characters
119: */
120: public static String fgColor(Color c, String str) {
121: if ((str == null) || (str.length() == 0))
122: return str;
123:
124: StringBuffer sb = new StringBuffer(str.length() + 7);
125:
126: sb.append(ESCAPE_CHAR);
127: sb.append(FGCOLOR_ATTR_OPEN);
128: sb.append((char) (c.getRed()));
129: sb.append((char) (c.getGreen()));
130: sb.append((char) (c.getBlue()));
131: sb.append(str);
132: sb.append(ESCAPE_CHAR);
133: sb.append(FGCOLOR_ATTR_CLOSE);
134:
135: return sb.toString();
136: }
137:
138: /**
139: * Create a string with a background color attribute applied.
140: *
141: * @param c the color to apply
142: * @param str the string to apply it to
143: * @return a string with embedded control characters
144: */
145: public static String bgColor(Color c, String str) {
146: if ((str == null) || (str.length() == 0))
147: return str;
148:
149: StringBuffer sb = new StringBuffer(str.length() + 7);
150:
151: sb.append(ESCAPE_CHAR);
152: sb.append(BGCOLOR_ATTR_OPEN);
153: sb.append((char) (c.getRed()));
154: sb.append((char) (c.getGreen()));
155: sb.append((char) (c.getBlue()));
156: sb.append(str);
157: sb.append(ESCAPE_CHAR);
158: sb.append(BGCOLOR_ATTR_CLOSE);
159:
160: return sb.toString();
161: }
162:
163: /**
164: * Create a string with a hyperlink. Each time the user clicks the
165: * hyperlink, the runnable is invoked.
166: *
167: * @param r the runnable to invoke when user clicks link
168: * @param str the string to apply it to
169: * @return a string with embedded control characters
170: */
171: public static String hyperlink(Runnable r, String str) {
172: return hyperlink(Color.cyan, r, str);
173: }
174:
175: /**
176: * Create a string with a hyperlink. Each time the user clicks the
177: * hyperlink, the runnable is invoked.
178: *
179: * @param c the color to apply
180: * @param r the runnable to invoke when user clicks link
181: * @param str the string to apply it to
182: * @return a string with embedded control characters
183: */
184: public static String hyperlink(Color c, Runnable r, String str) {
185: if ((str == null) || (str.length() == 0))
186: return str;
187:
188: synchronized (runnables) {
189: int idx = 0;
190:
191: while ((idx < runnables.length) && (runnables[idx] != null))
192: idx++;
193:
194: if (idx < runnables.length)
195: runnables[idx] = r;
196:
197: StringBuffer sb = new StringBuffer(str.length() + 12);
198:
199: sb.append(ESCAPE_CHAR);
200: sb.append(HYPERLINK_ATTR_OPEN);
201: sb.append((char) (idx));
202:
203: // inline this instead, to avoid an extra copy:
204: // sb.append( fgColor( c, str ) );
205: sb.append(ESCAPE_CHAR);
206: sb.append(FGCOLOR_ATTR_OPEN);
207: sb.append((char) (c.getRed()));
208: sb.append((char) (c.getGreen()));
209: sb.append((char) (c.getBlue()));
210: sb.append(str);
211: sb.append(ESCAPE_CHAR);
212: sb.append(FGCOLOR_ATTR_CLOSE);
213:
214: sb.append(ESCAPE_CHAR);
215: sb.append(HYPERLINK_ATTR_CLOSE);
216:
217: return sb.toString();
218: }
219: }
220:
221: /*=======================================================================*/
222:
223: /**
224: * Append characters to the end of the character stream. Note that this
225: * method is the entry point to a bunch of internal processing that is
226: * not thread safe, so it is synchronized on the buffer lock.
227: *
228: * @param cbuf the character buffer
229: * @param off the offset into cbuf to first character to append
230: * @param len the number of characters to append
231: */
232: public void append(char[] cbuf, int off, int len) {
233: synchronized (getBufferLock()) {
234: lock();
235: for (int i = 0; i < len; i++)
236: handle(cbuf[off + i]);
237: unlock();
238: }
239: }
240:
241: private final void handle(char c) {
242: state = state.handle(c);
243: }
244:
245: /*=======================================================================*/
246:
247: /* The stacks for each of the attribute types. These are stacks of attr
248: * regions that have opened, but not yet closed, ie. we are still scanning
249: * for the close tag.
250: */
251: private Vector fgAttrStack = new Vector();
252: private Vector bgAttrStack = new Vector();
253: private Vector hyperlinkStack = new Vector();
254:
255: private void handleFgColorOpen(int r, int g, int b) {
256: fgAttrStack.add(new ColorStartMarker(getFgColorAttr(new Color(
257: r, g, b)), getOffset()));
258: }
259:
260: private void handleFgColorClose() {
261: int idx = fgAttrStack.size() - 1;
262: ColorStartMarker m = (ColorStartMarker) (fgAttrStack.get(idx));
263: fgAttrStack.remove(idx);
264: int startOffset = m.getOffset();
265: addRegion(m.getAttribute().getRegion(startOffset,
266: getOffset() - startOffset));
267: }
268:
269: private void handleBgColorOpen(int r, int g, int b) {
270: bgAttrStack.add(new ColorStartMarker(getBgColorAttr(new Color(
271: r, g, b)), getOffset()));
272: }
273:
274: private void handleBgColorClose() {
275: int idx = bgAttrStack.size() - 1;
276: ColorStartMarker m = (ColorStartMarker) (bgAttrStack.get(idx));
277: bgAttrStack.remove(idx);
278: int startOffset = m.getOffset();
279: addRegion(m.getAttribute().getRegion(startOffset,
280: getOffset() - startOffset));
281: }
282:
283: private void handleHyperlinkOpen(int idx) {
284: Runnable r = null;
285:
286: synchronized (runnables) {
287: if ((0 <= idx) && (idx < runnables.length)) {
288: r = runnables[idx];
289: runnables[idx] = null;
290: }
291: }
292:
293: hyperlinkStack.add(new HyperlinkStartMarker(r, getOffset()));
294: }
295:
296: private void handleHyperlinkClose() {
297: int idx = hyperlinkStack.size() - 1;
298: HyperlinkStartMarker m = (HyperlinkStartMarker) (hyperlinkStack
299: .get(idx));
300: hyperlinkStack.remove(idx);
301: int startOffset = m.getOffset();
302: addRegion(new HyperlinkRegion(startOffset, getOffset()
303: - startOffset, m.getRunnable()));
304: }
305:
306: /*=======================================================================*/
307:
308: private static class ColorStartMarker {
309: private Attribute attr;
310: private int offset;
311:
312: ColorStartMarker(Attribute attr, int offset) {
313: this .attr = attr;
314: this .offset = offset;
315: }
316:
317: Attribute getAttribute() {
318: return attr;
319: }
320:
321: int getOffset() {
322: return offset;
323: }
324: }
325:
326: private static class HyperlinkStartMarker {
327: private Runnable r;
328: private int offset;
329:
330: HyperlinkStartMarker(Runnable r, int offset) {
331: this .r = r;
332: this .offset = offset;
333: }
334:
335: Runnable getRunnable() {
336: return r;
337: }
338:
339: int getOffset() {
340: return offset;
341: }
342: }
343:
344: /*=======================================================================*/
345:
346: /**
347: * Region that is created to track a hyperlink in the console. The
348: * {@link HyperlinkMouseHandler} detects mouse clicks that intersect
349: * regions of this type, in order to handle mouse clicks on hyperlink.
350: */
351: private static class HyperlinkRegion extends Region {
352: private Runnable r;
353:
354: HyperlinkRegion(int offset, int length, Runnable r) {
355: super (offset, length);
356: this .r = r;
357: }
358:
359: void mouseClicked(MouseEvent evt) {
360: if (r != null)
361: r.run();
362: }
363: }
364:
365: /**
366: * Mouse listener that looks for mouse clicks intersecting a
367: * {@link HyperlinkRegion}
368: */
369: private class HyperlinkMouseHandler implements MouseMotionListener,
370: MouseListener {
371: private boolean inLink = false; // track current cursor state
372:
373: public void mouseDragged(MouseEvent evt) {
374: } // no-op
375:
376: public void mousePressed(MouseEvent evt) {
377: } // no-op
378:
379: public void mouseReleased(MouseEvent evt) {
380: } // no-op
381:
382: public void mouseClicked(final MouseEvent evt) {
383: final LinkedList intersectingRegionList = new LinkedList();
384:
385: // since we don't want to process the mouse click while holding the
386: // region-iterator-lock, so copy them into a list so they can be
387: // handled after lock is released
388: synchronized (getBufferLock()) {
389: for (Iterator itr = getRegions(console.toOffset(evt
390: .getPoint()), 0); itr.hasNext();) {
391: Region r = (Region) (itr.next());
392:
393: if (r instanceof HyperlinkRegion)
394: intersectingRegionList.add(r);
395: }
396: }
397:
398: for (Iterator itr = intersectingRegionList.iterator(); itr
399: .hasNext();)
400: ((HyperlinkRegion) (itr.next())).mouseClicked(evt);
401: }
402:
403: public void mouseMoved(MouseEvent evt) {
404: boolean currentlyInLink = false;
405:
406: synchronized (getBufferLock()) {
407: for (Iterator itr = getRegions(console.toOffset(evt
408: .getPoint()), 0); itr.hasNext();) {
409: Region r = (Region) (itr.next());
410:
411: if (r instanceof HyperlinkRegion) {
412: currentlyInLink = true;
413: break;
414: }
415: }
416: }
417:
418: if (currentlyInLink != inLink) {
419: inLink = currentlyInLink;
420:
421: if (inLink)
422: console.setCursor(Cursor
423: .getPredefinedCursor(Cursor.HAND_CURSOR));
424: else
425: console
426: .setCursor(Cursor
427: .getPredefinedCursor(Cursor.DEFAULT_CURSOR));
428: }
429: }
430:
431: public void mouseEntered(MouseEvent evt) {
432: } // no-op
433:
434: public void mouseExited(MouseEvent evt) {
435: } // no-op
436: }
437:
438: /*=======================================================================*/
439:
440: // Color attributes can be cached re-used:
441: private static Hashtable fgColorAttrTable = new Hashtable();
442:
443: private static Attribute getFgColorAttr(Color c) {
444: Attribute attr = (Attribute) (fgColorAttrTable.get(c));
445: if (attr == null) {
446: attr = new FgColorAttribute(c);
447: fgColorAttrTable.put(c, attr);
448: }
449: return attr;
450: }
451:
452: private static Hashtable bgColorAttrTable = new Hashtable();
453:
454: private static Attribute getBgColorAttr(Color c) {
455: Attribute attr = (Attribute) (bgColorAttrTable.get(c));
456: if (attr == null) {
457: attr = new BgColorAttribute(c);
458: bgColorAttrTable.put(c, attr);
459: }
460: return attr;
461: }
462:
463: /*=======================================================================*/
464:
465: /**
466: * The state interface. The state machine is used to implement scanning
467: * the multiple character sequences used to control the attributes. It
468: * isn't use to handle matching up open/close control sequences, that is
469: * done with the attribute stacks.
470: */
471: private abstract class State {
472: private char[] cbuf = new char[1];
473:
474: protected void write(char c) {
475: // note: access to this in synchronized in ColorInputHandler#append()
476: cbuf[0] = c;
477: ColorInputHandler.super .append(cbuf, 0, 1);
478: }
479:
480: abstract State handle(char c);
481:
482: // if internal state needs to be reset, overload this:
483: State reset() {
484: return this ;
485: }
486: }
487:
488: private final State NULL_STATE = new State() {
489:
490: State handle(char c) {
491: if (c == ESCAPE_CHAR)
492: return ESCAPE_STATE.reset();
493:
494: write(c);
495: return this ;
496: }
497:
498: };
499:
500: private final State ESCAPE_STATE = new State() {
501:
502: State handle(char c) {
503: switch (c) {
504: case FGCOLOR_ATTR_OPEN:
505: return FG_COLOR_OPEN_STATE.reset();
506:
507: case FGCOLOR_ATTR_CLOSE:
508: handleFgColorClose();
509: return NULL_STATE.reset();
510:
511: case BGCOLOR_ATTR_OPEN:
512: return BG_COLOR_OPEN_STATE.reset();
513:
514: case BGCOLOR_ATTR_CLOSE:
515: handleBgColorClose();
516: return NULL_STATE.reset();
517:
518: case HYPERLINK_ATTR_OPEN:
519: return HYPERLINK_OPEN_STATE.reset();
520:
521: case HYPERLINK_ATTR_CLOSE:
522: handleHyperlinkClose();
523: return NULL_STATE.reset();
524:
525: default:
526: // go back to null state, pass thru ESCAPE_CHAR in case it
527: // is used by a different InputHandler:
528: write(ESCAPE_CHAR);
529: write(c);
530: return NULL_STATE.reset();
531: }
532: }
533:
534: };
535:
536: // to avoid a proliferation of state classes, a single fg/bg open
537: // state class is used, but maintains some state internally...
538: private abstract class ColorOpenState extends State {
539: private int r;
540: private int g;
541: private int b;
542:
543: State handle(char c) {
544: if (r == -1) {
545: r = (int) c;
546: return this ;
547: } else if (g == -1) {
548: g = (int) c;
549: return this ;
550: } else {
551: b = (int) c;
552: handle(r, g, b);
553: return NULL_STATE.reset();
554: }
555: }
556:
557: State reset() {
558: r = -1;
559: g = -1;
560: b = -1;
561:
562: return super .reset();
563: }
564:
565: protected abstract void handle(int r, int g, int b);
566: }
567:
568: private final State FG_COLOR_OPEN_STATE = new ColorOpenState() {
569:
570: protected void handle(int r, int g, int b) {
571: handleFgColorOpen(r, g, b);
572: }
573:
574: };
575:
576: private final State BG_COLOR_OPEN_STATE = new ColorOpenState() {
577:
578: protected void handle(int r, int g, int b) {
579: handleBgColorOpen(r, g, b);
580: }
581:
582: };
583:
584: private final State HYPERLINK_OPEN_STATE = new State() {
585:
586: State handle(char c) {
587: int idx = (int) c;
588: handleHyperlinkOpen(idx);
589: return NULL_STATE.reset();
590: }
591:
592: };
593:
594: public void close() {
595: super .close();
596: }
597: }
598:
599: /*
600: * Local Variables:
601: * tab-width: 2
602: * indent-tabs-mode: nil
603: * mode: java
604: * c-indentation-style: java
605: * c-basic-offset: 2
606: * eval: (c-set-offset 'substatement-open '0)
607: * eval: (c-set-offset 'case-label '+)
608: * eval: (c-set-offset 'inclass '+)
609: * eval: (c-set-offset 'inline-open '0)
610: * End:
611: */
|