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 ti.exceptions.ProgrammingErrorException;
022:
023: import java.awt.*;
024: import java.awt.event.*;
025: import java.io.*;
026: import java.util.Vector;
027:
028: import javax.swing.Timer;
029:
030: /**
031: * Handle keyboard input, and provide a reader interface.... this really
032: * should be thought of as an inner-class of ConsoleTextArea, but was moved
033: * here to preserve sane file sizes...
034: *
035: * @author Rob Clark
036: * @version 0.1
037: */
038: public class ConsoleKeyListener extends KeyAdapter implements
039: Serializable {
040: private final static int RUNNING = 0;
041: private final static int KILLED = 1;
042: private int state = RUNNING;
043:
044: private static final int VK_CONTROL = (System
045: .getProperty("mrj.version") == null) ? KeyEvent.VK_CONTROL
046: : KeyEvent.VK_META;
047:
048: /**
049: * Modifiers....
050: */
051: private boolean overwrite = false;
052: private boolean control = false;
053:
054: private static final int BUFSIZE = 40;
055:
056: /**
057: * The console we are a member of... we really should be an inner class
058: * of ConsoleTextArea, but that would be too much for one src file. Instead
059: * we manually implement inner-classes by keeping a reference back to our
060: * creator.
061: */
062: private Console console;
063:
064: private static final char[] EMPTY_BUF = new char[0];
065:
066: /**
067: * The buffer of characters typed since last LF
068: */
069: private char buf[] = EMPTY_BUF;
070: private int bufLength = 0;
071:
072: /**
073: * The cursor into the buf.
074: */
075: private int cursor = 0;
076:
077: /**
078: * The "queue" of characters that have been committed (ie. user pressed
079: * <ENTER>), but not yet consumed by reader.
080: */
081: private char cqueue[] = EMPTY_BUF;
082:
083: /**
084: * Since cqueue is overwritten, but we still need to synchronize access
085: * to it, this object is used:
086: */
087: private final Object cqueueLock = new String("cqueueLock");
088:
089: /**
090: * The history buffer.
091: */
092: private History history = new History();
093:
094: /**
095: * The tab completion engine, if one is configured. Tab completion is
096: * disabled if this is <code>null</code>.
097: */
098: private ConsoleTabCompleter tabCompleter;
099:
100: /**
101: * Class Constructor.
102: *
103: * @param console the console we belong to
104: */
105: public ConsoleKeyListener(Console console) {
106: this .console = console;
107: }
108:
109: /**
110: * Set the history. The history object must be an object that was previously
111: * returned by {@link #getHistory}, but may be serialized/deserialized.
112: *
113: * @param history a history object, as returned by {@link #getHistory}
114: * @see #getHistory
115: */
116: public void setHistory(Object history) {
117: if (history instanceof History)
118: this .history = (History) history;
119: }
120:
121: /**
122: * Get the history. The history is externally treated as opaque, but the
123: * get/set methods allow the creator of the {@link Console} to make history
124: * persistant. The history object is {@link java.io.Serializable}
125: *
126: * @return a opaque history object, which is serializable
127: * @see #setHistory
128: */
129: public Object getHistory() {
130: return history;
131: }
132:
133: /**
134: * Set the {@link ConsoleTabCompleter}
135: */
136: public void setTabCompleter(ConsoleTabCompleter tabCompleter) {
137: this .tabCompleter = tabCompleter;
138: }
139:
140: /**
141: * Get the Reader for the console. By using this reader, an application
142: * can use this console for input.
143: *
144: * @return a reader that can be used to read from the console
145: */
146: public Reader getReader() {
147: return new Reader() {
148:
149: /**
150: * Tell whether this stream is ready to be read.
151: *
152: * @return True if the next read() is guaranteed not to block for input,
153: * false otherwise. Note that returning false does not guarantee that the
154: * next read will block.
155: *
156: * @exception IOException If an I/O error occurs
157: */
158: public boolean ready() throws IOException {
159: return (state != KILLED) && (cqueue.length > 0);
160: }
161:
162: /**
163: * Read into a portion of an array of characters. This method will block
164: * until some input is available, an I/O error occurs, or the end of the
165: * stream is reached, e.g. the outerclass is finalized.
166: *
167: * @param cbuf Array of characters to read into
168: * @param off Offset from which to start writing characters
169: * @param len Number of characters to write
170: * @return the number of characters read, or -1 if the end of the stream
171: * has been reached
172: * @exception IOException If an I/O error occurs
173: */
174: public int read(char[] cbuf, int off, int len)
175: throws IOException {
176: while (true) {
177: // wait for data to be available:
178: synchronized (cqueueLock) {
179: while ((state != KILLED) && !ready()) {
180: try {
181: cqueueLock.wait();
182: } catch (InterruptedException e) {
183: }
184: }
185: }
186:
187: if (state == KILLED)
188: return -1;
189:
190: synchronized (cqueueLock) {
191: if (cqueue.length != 0) {
192: int i = off; // i < off + len
193: int j = 0; // j < cqueue.length
194:
195: while ((i < off + len)
196: && (j < cqueue.length))
197: cbuf[i++] = cqueue[j++];
198:
199: // if there are remaining chars, copy them to the new cqueue:
200: if (j < cqueue.length) {
201: char tmp[] = new char[cqueue.length - j];
202: for (i = 0; i < (cqueue.length - j); i++)
203: tmp[i] = cqueue[j + i];
204: cqueue = tmp;
205: } else {
206: cqueue = new char[0];
207: }
208:
209: // return the number of chars read:
210: return j;
211: }
212: }
213: }
214: }
215:
216: /**
217: * Don't do anything, since you can't close this!
218: *
219: * @exception IOException - If an I/O error occurs
220: */
221: public void close() throws IOException {
222: }
223: };
224: }
225:
226: /**
227: * This should called when getting rid of this text-area... this will
228: * cause any blocking readers to return.
229: */
230: public void dispose() {
231: state = KILLED;
232:
233: // in case we have someone waiting on cqueue:
234: synchronized (cqueueLock) {
235: cqueueLock.notifyAll();
236: }
237: }
238:
239: /**
240: * Append char to current active buffer.
241: */
242: public void insertChar(char c) {
243: InputHandler ih = console.getInputHandler();
244:
245: if (c == Console.LF) {
246: synchronized (cqueueLock) {
247: // append chars in buf to cqueue:
248: char[] tmp = new char[cqueue.length + bufLength + 1];
249: System.arraycopy(cqueue, 0, tmp, 0, cqueue.length);
250: System.arraycopy(buf, 0, tmp, cqueue.length, bufLength);
251: tmp[tmp.length - 1] = Console.LF;
252:
253: // update history buffer:
254: history.append(buf, 0, bufLength);
255:
256: // reset buffer:
257: buf = EMPTY_BUF;
258: cursor = bufLength = 0;
259:
260: // append an EOL:
261: ih.append(tmp, tmp.length - 1, 1);
262:
263: cqueue = tmp;
264:
265: cqueueLock.notifyAll();
266: }
267: } else {
268: // check if we need to grow buf:
269: ensureCapacity(1);
270:
271: if (!overwrite) {
272: // delete everything back to cursor:
273: ih.zap(bufLength - cursor);
274:
275: // copy buf array elements, shifting up by one:
276: System.arraycopy(buf, cursor, buf, cursor + 1,
277: bufLength - cursor);
278:
279: // update buf, length and cursor:
280: buf[cursor] = c;
281: bufLength++;
282: cursor++;
283:
284: // write remainging chars back out:
285: ih.append(buf, cursor - 1, bufLength - cursor + 1);
286: } else {
287: // this is basically the same, leaving out the shifting...
288: throw new ProgrammingErrorException("unimplemented");
289: }
290: }
291: }
292:
293: /**
294: * Invoked when a key is typed.
295: */
296: public synchronized void keyTyped(KeyEvent evt) {
297: if (!console.isEnabled())
298: return;
299:
300: if (console.getInputMap().get(
301: javax.swing.KeyStroke.getKeyStroke(evt.getKeyCode(),
302: evt.getModifiers())) != null)
303: return;
304:
305: InputHandler ih = console.getInputHandler();
306:
307: ih.lock();
308:
309: char c = evt.getKeyChar();
310: int keyCode = (int) c;
311:
312: if (c == Console.CR)
313: c = Console.LF;
314:
315: switch (keyCode) {
316: case KeyEvent.VK_BACK_SPACE:
317: if (cursor > 0) {
318: // delete everything back to before cursor:
319: ih.zap(bufLength - cursor + 1);
320:
321: // copy buf array elements, shifting down one:
322: System.arraycopy(buf, cursor, buf, cursor - 1,
323: bufLength - cursor);
324:
325: // update length and cursor:
326: bufLength--;
327: cursor--;
328:
329: // write remaining chars back out:
330: ih.append(buf, cursor, bufLength - cursor);
331: }
332: break;
333:
334: case KeyEvent.VK_DELETE:
335: if (cursor < bufLength) {
336: // delete everything back to cursor:
337: ih.zap(bufLength - cursor);
338:
339: // copy buf array elements, shifting down one:
340: System.arraycopy(buf, cursor + 1, buf, cursor,
341: bufLength - cursor - 1);
342:
343: // update length and cursor:
344: bufLength--;
345:
346: // write remaining chars back out:
347: ih.append(buf, cursor, bufLength - cursor);
348: }
349: break;
350:
351: case KeyEvent.VK_TAB:
352: if (tabCompleter != null) {
353: String pre = new String(buf, 0, cursor);
354: String post = new String(buf, cursor, bufLength
355: - cursor);
356: replaceLine(pre + tabCompleter.complete(pre) + post);
357: break;
358: }
359: // fall thru
360: default:
361: if (!control)
362: insertChar(c);
363:
364: }
365:
366: ih.unlock();
367: }
368:
369: /**
370: * Keep track of modifiers.
371: */
372: public void keyPressed(KeyEvent evt) {
373: if (!console.isEnabled())
374: return;
375:
376: int keyCode = evt.getKeyCode();
377:
378: /* on some platforms we treat other keys as KeyEvent.VK_CONTROL:
379: */
380: if (keyCode == VK_CONTROL)
381: keyCode = KeyEvent.VK_CONTROL;
382:
383: /* handle LEFT/RIGHT here so they repeat if the key is held down!
384: */
385: switch (keyCode) {
386: case KeyEvent.VK_CONTROL:
387: control = true;
388: break;
389:
390: case KeyEvent.VK_LEFT:
391: while (cursor > 0) {
392: cursor--;
393:
394: if (!control || isSeparator(buf[cursor]))
395: break;
396: }
397: console.repaint();
398: break;
399:
400: case KeyEvent.VK_RIGHT:
401: while (++cursor < bufLength)
402: if (!control || isSeparator(buf[cursor]))
403: break;
404: if (cursor > bufLength)
405: cursor = bufLength;
406: console.repaint();
407: break;
408: }
409: }
410:
411: /**
412: * Certain keys don't generate keyTyped events, so we have to do this.
413: */
414: public void keyReleased(KeyEvent evt) {
415: if (!console.isEnabled())
416: return;
417:
418: int keyCode = evt.getKeyCode();
419:
420: /* on some platforms we treat other keys as KeyEvent.VK_CONTROL:
421: */
422: if (keyCode == VK_CONTROL)
423: keyCode = KeyEvent.VK_CONTROL;
424:
425: int oldCursor = cursor;
426: switch (keyCode) {
427: case KeyEvent.VK_CONTROL:
428: control = false;
429: break;
430:
431: case KeyEvent.VK_UP:
432: replaceLine(history.getPrev(new String(buf, 0, cursor)));
433: cursor = Math.min(oldCursor, cursor);
434: break;
435:
436: case KeyEvent.VK_DOWN:
437: replaceLine(history.getNext(new String(buf, 0, cursor)));
438: cursor = Math.min(oldCursor, cursor);
439: break;
440:
441: case KeyEvent.VK_HOME:
442: cursor = 0;
443: console.repaint();
444: break;
445:
446: case KeyEvent.VK_END:
447: cursor = bufLength;
448: console.repaint();
449: break;
450:
451: case KeyEvent.VK_ESCAPE:
452: if (control)
453: replaceLine("");
454: break;
455: }
456: }
457:
458: private static final String SEPARATOR_CHARS = " .()[]{}-+*/_";
459:
460: private final boolean isSeparator(char c) {
461: for (int i = 0; i < SEPARATOR_CHARS.length(); i++)
462: if (c == SEPARATOR_CHARS.charAt(i))
463: return true;
464: return false;
465: }
466:
467: private final void replaceLine(String str) {
468: if (str != null) {
469: char[] buf = str.toCharArray();
470:
471: InputHandler ih = console.getInputHandler();
472: ih.lock();
473:
474: // remove old line:
475: ih.zap(bufLength);
476:
477: // replace line:
478: this .buf = buf;
479: bufLength = cursor = buf.length;
480:
481: // append new line to console:
482: ih.append(buf, 0, bufLength);
483:
484: ih.unlock();
485: }
486: }
487:
488: private final void ensureCapacity(int cnt) {
489: if ((bufLength + cnt) >= buf.length) {
490: char[] newBuf = new char[buf.length + BUFSIZE + cnt];
491: System.arraycopy(buf, 0, newBuf, 0, buf.length);
492: buf = newBuf;
493: }
494: }
495:
496: /**
497: * Draw the cursor over the graphics.
498: */
499: public void paintCursor(Graphics g) {
500: InputHandler ih = console.getInputHandler();
501:
502: if (!console.locked()) {
503: int off = ih.getOffset() - (bufLength - cursor);
504: Point p = console.toPoint(off);
505:
506: // we don't want to disturb the state of the current graphics:
507: g = g.create();
508: g.setColor(Color.white);
509: g.setXORMode(Color.blue);
510:
511: g.fillRect(p.x, p.y, console.getColumnWidth(), console
512: .getRowHeight());
513: }
514: }
515:
516: /**
517: * Class to implement history buffer.
518: */
519: private static class History implements Serializable {
520: private Vector history = new Vector();
521: private int idx = 0;
522:
523: /**
524: * Get the prev entry in the history that begins with <code>current</code>
525: * (which is the characters in the current buffer up to the cursor)
526: */
527: String getPrev(String current) {
528: int idx = this .idx;
529: char[] tmp;
530: while ((idx > 0)
531: && ((tmp = (char[]) (history.get(--idx))).length >= current
532: .length()))
533: if (current
534: .equals(new String(tmp, 0, current.length())))
535: return new String((char[]) (history
536: .get(this .idx = idx)));
537: return null;
538: }
539:
540: /**
541: * Get the next entry in the history that begins with <code>current</code>
542: * (which is the characters in the current buffer up to the cursor)
543: */
544: String getNext(String current) {
545: int idx = this .idx;
546: while (idx < (history.size() - 1)) {
547: char[] tmp = (char[]) (history.get(++idx));
548: if (current.equals(new String(tmp, 0, Math.min(
549: tmp.length, current.length())))) {
550: this .idx = idx;
551: return new String(tmp);
552: }
553: }
554: if (idx < history.size())
555: return "";
556: return null;
557: }
558:
559: void append(char[] buf, int off, int len) {
560: // always reset idx, even if we don't append:
561: idx = history.size();
562:
563: // ignore zero length lines:
564: if (len <= 0)
565: return;
566:
567: char[] line = new char[len];
568: System.arraycopy(buf, off, line, 0, len);
569:
570: // ignore lines same as prev line:
571: if ((history.size() > 0)
572: && equals(line, (char[]) (history.get(history
573: .size() - 1))))
574: return;
575:
576: history.add(line);
577: idx = history.size();
578: }
579:
580: private static boolean equals(char[] a1, char[] a2) {
581: if (a1.length != a2.length)
582: return false;
583:
584: for (int i = 0; i < a1.length; i++)
585: if (a1[i] != a2[i])
586: return false;
587:
588: return true;
589: }
590:
591: public String toString() {
592: StringBuffer sb = new StringBuffer();
593: sb.append('{'); // }
594:
595: int i = 0;
596: for (; i < history.size(); i++)
597: sb.append("\"" + new String((char[]) (history.get(i)))
598: + "\",");
599:
600: if (i > 0)
601: sb.setCharAt(sb.length() - 1, '}');
602: else
603: sb.append('}');
604:
605: return sb.toString();
606: }
607: }
608: /*
609: private final static char[] duplicate( char[] buf )
610: {
611: char[] newBuf = null;
612:
613: if( buf != null )
614: {
615: newBuf = new char[buf.length];
616: System.arraycopy( buf, 0, newBuf, 0, buf.length );
617: }
618:
619: return newBuf;
620: }
621: */
622: }
623:
624: /*
625: * Local Variables:
626: * tab-width: 2
627: * indent-tabs-mode: nil
628: * mode: java
629: * c-indentation-style: java
630: * c-basic-offset: 2
631: * eval: (c-set-offset 'substatement-open '0)
632: * eval: (c-set-offset 'case-label '+)
633: * eval: (c-set-offset 'inclass '+)
634: * eval: (c-set-offset 'inline-open '0)
635: * End:
636: */
|