001: /*****************************************************************************
002: * *
003: * This file is part of the BeanShell Java Scripting distribution. *
004: * Documentation and updates may be found at http://www.beanshell.org/ *
005: * *
006: * Sun Public License Notice: *
007: * *
008: * The contents of this file are subject to the Sun Public License Version *
009: * 1.0 (the "License"); you may not use this file except in compliance with *
010: * the License. A copy of the License is available at http://www.sun.com *
011: * *
012: * The Original Code is BeanShell. The Initial Developer of the Original *
013: * Code is Pat Niemeyer. Portions created by Pat Niemeyer are Copyright *
014: * (C) 2000. All Rights Reserved. *
015: * *
016: * GNU Public License Notice: *
017: * *
018: * Alternatively, the contents of this file may be used under the terms of *
019: * the GNU Lesser General Public License (the "LGPL"), in which case the *
020: * provisions of LGPL are applicable instead of those above. If you wish to *
021: * allow use of your version of this file only under the terms of the LGPL *
022: * and not to allow others to use your version of this file under the SPL, *
023: * indicate your decision by deleting the provisions above and replace *
024: * them with the notice and other provisions required by the LGPL. If you *
025: * do not delete the provisions above, a recipient may use your version of *
026: * this file under either the SPL or the LGPL. *
027: * *
028: * Patrick Niemeyer (pat@pat.net) *
029: * Author of Learning Java, O'Reilly & Associates *
030: * http://www.pat.net/~pat/ *
031: * *
032: *****************************************************************************/package bsh.util;
033:
034: import java.awt.Component;
035: import java.awt.Font;
036: import java.awt.Color;
037: import java.awt.Insets;
038: import java.awt.event.*;
039: import java.beans.PropertyChangeEvent;
040: import java.beans.PropertyChangeListener;
041: import java.io.*;
042: import java.util.Vector;
043: import java.awt.Cursor;
044: import javax.swing.text.*;
045: import javax.swing.*;
046:
047: // Things that are not in the core packages
048:
049: import bsh.util.NameCompletion;
050:
051: /**
052: A JFC/Swing based console for the BeanShell desktop.
053: This is a descendant of the old AWTConsole.
054:
055: Improvements by: Mark Donszelmann <Mark.Donszelmann@cern.ch>
056: including Cut & Paste
057:
058: Improvements by: Daniel Leuck
059: including Color and Image support, key press bug workaround
060: */
061: public class JConsole extends JScrollPane implements
062: GUIConsoleInterface, Runnable, KeyListener, MouseListener,
063: ActionListener, PropertyChangeListener {
064: private final static String CUT = "Cut";
065: private final static String COPY = "Copy";
066: private final static String PASTE = "Paste";
067:
068: private OutputStream outPipe;
069: private InputStream inPipe;
070: private InputStream in;
071: private PrintStream out;
072:
073: public InputStream getInputStream() {
074: return in;
075: }
076:
077: public Reader getIn() {
078: return new InputStreamReader(in);
079: }
080:
081: public PrintStream getOut() {
082: return out;
083: }
084:
085: public PrintStream getErr() {
086: return out;
087: }
088:
089: private int cmdStart = 0;
090: private Vector history = new Vector();
091: private String startedLine;
092: private int histLine = 0;
093:
094: private JPopupMenu menu;
095: private JTextPane text;
096: private DefaultStyledDocument doc;
097:
098: NameCompletion nameCompletion;
099: final int SHOW_AMBIG_MAX = 10;
100:
101: // hack to prevent key repeat for some reason?
102: private boolean gotUp = true;
103:
104: public JConsole() {
105: this (null, null);
106: }
107:
108: public JConsole(InputStream cin, OutputStream cout) {
109: super ();
110:
111: // Special TextPane which catches for cut and paste, both L&F keys and
112: // programmatic behaviour
113: text = new JTextPane(doc = new DefaultStyledDocument()) {
114: public void cut() {
115: if (text.getCaretPosition() < cmdStart) {
116: super .copy();
117: } else {
118: super .cut();
119: }
120: }
121:
122: public void paste() {
123: forceCaretMoveToEnd();
124: super .paste();
125: }
126: };
127:
128: Font font = new Font("Monospaced", Font.PLAIN, 14);
129: text.setText("");
130: text.setFont(font);
131: text.setMargin(new Insets(7, 5, 7, 5));
132: text.addKeyListener(this );
133: setViewportView(text);
134:
135: // create popup menu
136: menu = new JPopupMenu("JConsole Menu");
137: menu.add(new JMenuItem(CUT)).addActionListener(this );
138: menu.add(new JMenuItem(COPY)).addActionListener(this );
139: menu.add(new JMenuItem(PASTE)).addActionListener(this );
140:
141: text.addMouseListener(this );
142:
143: // make sure popup menu follows Look & Feel
144: UIManager.addPropertyChangeListener(this );
145:
146: outPipe = cout;
147: if (outPipe == null) {
148: outPipe = new PipedOutputStream();
149: try {
150: in = new PipedInputStream((PipedOutputStream) outPipe);
151: } catch (IOException e) {
152: print("Console internal error (1)...", Color.red);
153: }
154: }
155:
156: inPipe = cin;
157: if (inPipe == null) {
158: PipedOutputStream pout = new PipedOutputStream();
159: out = new PrintStream(pout);
160: try {
161: inPipe = new BlockingPipedInputStream(pout);
162: } catch (IOException e) {
163: print("Console internal error: " + e);
164: }
165: }
166: // Start the inpipe watcher
167: new Thread(this ).start();
168:
169: requestFocus();
170: }
171:
172: public void requestFocus() {
173: super .requestFocus();
174: text.requestFocus();
175: }
176:
177: public void keyPressed(KeyEvent e) {
178: type(e);
179: gotUp = false;
180: }
181:
182: public void keyTyped(KeyEvent e) {
183: type(e);
184: }
185:
186: public void keyReleased(KeyEvent e) {
187: gotUp = true;
188: type(e);
189: }
190:
191: private synchronized void type(KeyEvent e) {
192: switch (e.getKeyCode()) {
193: case (KeyEvent.VK_ENTER ):
194: if (e.getID() == KeyEvent.KEY_PRESSED) {
195: if (gotUp) {
196: enter();
197: resetCommandStart();
198: text.setCaretPosition(cmdStart);
199: }
200: }
201: e.consume();
202: text.repaint();
203: break;
204:
205: case (KeyEvent.VK_UP ):
206: if (e.getID() == KeyEvent.KEY_PRESSED) {
207: historyUp();
208: }
209: e.consume();
210: break;
211:
212: case (KeyEvent.VK_DOWN ):
213: if (e.getID() == KeyEvent.KEY_PRESSED) {
214: historyDown();
215: }
216: e.consume();
217: break;
218:
219: case (KeyEvent.VK_LEFT ):
220: case (KeyEvent.VK_BACK_SPACE ):
221: case (KeyEvent.VK_DELETE ):
222: if (text.getCaretPosition() <= cmdStart) {
223: // This doesn't work for backspace.
224: // See default case for workaround
225: e.consume();
226: }
227: break;
228:
229: case (KeyEvent.VK_RIGHT ):
230: forceCaretMoveToStart();
231: break;
232:
233: case (KeyEvent.VK_HOME ):
234: text.setCaretPosition(cmdStart);
235: e.consume();
236: break;
237:
238: case (KeyEvent.VK_U ): // clear line
239: if ((e.getModifiers() & InputEvent.CTRL_MASK) > 0) {
240: replaceRange("", cmdStart, textLength());
241: histLine = 0;
242: e.consume();
243: }
244: break;
245:
246: case (KeyEvent.VK_ALT ):
247: case (KeyEvent.VK_CAPS_LOCK ):
248: case (KeyEvent.VK_CONTROL ):
249: case (KeyEvent.VK_META ):
250: case (KeyEvent.VK_SHIFT ):
251: case (KeyEvent.VK_PRINTSCREEN ):
252: case (KeyEvent.VK_SCROLL_LOCK ):
253: case (KeyEvent.VK_PAUSE ):
254: case (KeyEvent.VK_INSERT ):
255: case (KeyEvent.VK_F1):
256: case (KeyEvent.VK_F2):
257: case (KeyEvent.VK_F3):
258: case (KeyEvent.VK_F4):
259: case (KeyEvent.VK_F5):
260: case (KeyEvent.VK_F6):
261: case (KeyEvent.VK_F7):
262: case (KeyEvent.VK_F8):
263: case (KeyEvent.VK_F9):
264: case (KeyEvent.VK_F10):
265: case (KeyEvent.VK_F11):
266: case (KeyEvent.VK_F12):
267: case (KeyEvent.VK_ESCAPE ):
268:
269: // only modifier pressed
270: break;
271:
272: // Control-C
273: case (KeyEvent.VK_C ):
274: if (text.getSelectedText() == null) {
275: if (((e.getModifiers() & InputEvent.CTRL_MASK) > 0)
276: && (e.getID() == KeyEvent.KEY_PRESSED)) {
277: append("^C");
278: }
279: e.consume();
280: }
281: break;
282:
283: case (KeyEvent.VK_TAB ):
284: if (e.getID() == KeyEvent.KEY_RELEASED) {
285: String part = text.getText().substring(cmdStart);
286: doCommandCompletion(part);
287: }
288: e.consume();
289: break;
290:
291: default:
292: if ((e.getModifiers() & (InputEvent.CTRL_MASK
293: | InputEvent.ALT_MASK | InputEvent.META_MASK)) == 0) {
294: // plain character
295: forceCaretMoveToEnd();
296: }
297:
298: /*
299: The getKeyCode function always returns VK_UNDEFINED for
300: keyTyped events, so backspace is not fully consumed.
301: */
302: if (e.paramString().indexOf("Backspace") != -1) {
303: if (text.getCaretPosition() <= cmdStart) {
304: e.consume();
305: break;
306: }
307: }
308:
309: break;
310: }
311: }
312:
313: private void doCommandCompletion(String part) {
314: if (nameCompletion == null)
315: return;
316:
317: int i = part.length() - 1;
318:
319: // Character.isJavaIdentifierPart() How convenient for us!!
320: while (i >= 0
321: && (Character.isJavaIdentifierPart(part.charAt(i)) || part
322: .charAt(i) == '.'))
323: i--;
324:
325: part = part.substring(i + 1);
326:
327: if (part.length() < 2) // reasonable completion length
328: return;
329:
330: //System.out.println("completing part: "+part);
331:
332: // no completion
333: String[] complete = nameCompletion.completeName(part);
334: if (complete.length == 0) {
335: java.awt.Toolkit.getDefaultToolkit().beep();
336: return;
337: }
338:
339: // Found one completion (possibly what we already have)
340: if (complete.length == 1 && !complete.equals(part)) {
341: String append = complete[0].substring(part.length());
342: append(append);
343: return;
344: }
345:
346: // Found ambiguous, show (some of) them
347:
348: String line = text.getText();
349: String command = line.substring(cmdStart);
350: // Find prompt
351: for (i = cmdStart; line.charAt(i) != '\n' && i > 0; i--)
352: ;
353: String prompt = line.substring(i + 1, cmdStart);
354:
355: // Show ambiguous
356: StringBuffer sb = new StringBuffer("\n");
357: for (i = 0; i < complete.length && i < SHOW_AMBIG_MAX; i++)
358: sb.append(complete[i] + "\n");
359: if (i == SHOW_AMBIG_MAX)
360: sb.append("...\n");
361:
362: print(sb, Color.gray);
363: print(prompt); // print resets command start
364: append(command); // append does not reset command start
365: }
366:
367: private void resetCommandStart() {
368: cmdStart = textLength();
369: }
370:
371: private void append(String string) {
372: int slen = textLength();
373: text.select(slen, slen);
374: text.replaceSelection(string);
375: }
376:
377: private String replaceRange(Object s, int start, int end) {
378: String st = s.toString();
379: text.select(start, end);
380: text.replaceSelection(st);
381: //text.repaint();
382: return st;
383: }
384:
385: private void forceCaretMoveToEnd() {
386: if (text.getCaretPosition() < cmdStart) {
387: // move caret first!
388: text.setCaretPosition(textLength());
389: }
390: text.repaint();
391: }
392:
393: private void forceCaretMoveToStart() {
394: if (text.getCaretPosition() < cmdStart) {
395: // move caret first!
396: }
397: text.repaint();
398: }
399:
400: private void enter() {
401: String s = getCmd();
402:
403: if (s.length() == 0) // special hack for empty return!
404: s = ";\n";
405: else {
406: history.addElement(s);
407: s = s + "\n";
408: }
409:
410: append("\n");
411: histLine = 0;
412: acceptLine(s);
413: text.repaint();
414: }
415:
416: private String getCmd() {
417: String s = "";
418: try {
419: s = text.getText(cmdStart, textLength() - cmdStart);
420: } catch (BadLocationException e) {
421: // should not happen
422: System.out.println("Internal JConsole Error: " + e);
423: }
424: return s;
425: }
426:
427: private void historyUp() {
428: if (history.size() == 0)
429: return;
430: if (histLine == 0) // save current line
431: startedLine = getCmd();
432: if (histLine < history.size()) {
433: histLine++;
434: showHistoryLine();
435: }
436: }
437:
438: private void historyDown() {
439: if (histLine == 0)
440: return;
441:
442: histLine--;
443: showHistoryLine();
444: }
445:
446: private void showHistoryLine() {
447: String showline;
448: if (histLine == 0)
449: showline = startedLine;
450: else
451: showline = (String) history.elementAt(history.size()
452: - histLine);
453:
454: replaceRange(showline, cmdStart, textLength());
455: text.setCaretPosition(textLength());
456: text.repaint();
457: }
458:
459: String ZEROS = "000";
460:
461: private void acceptLine(String line) {
462: // Patch to handle Unicode characters
463: // Submitted by Daniel Leuck
464: StringBuffer buf = new StringBuffer();
465: int lineLength = line.length();
466: for (int i = 0; i < lineLength; i++) {
467: char c = line.charAt(i);
468: if (c > 127) {
469: String val = Integer.toString(c, 16);
470: val = ZEROS.substring(0, 4 - val.length()) + val;
471: buf.append("\\u" + val);
472: } else {
473: buf.append(c);
474: }
475: }
476: line = buf.toString();
477: // End unicode patch
478:
479: if (outPipe == null)
480: print("Console internal error: cannot output ...",
481: Color.red);
482: else
483: try {
484: outPipe.write(line.getBytes());
485: outPipe.flush();
486: } catch (IOException e) {
487: outPipe = null;
488: throw new RuntimeException("Console pipe broken...");
489: }
490: //text.repaint();
491: }
492:
493: public void println(Object o) {
494: print(String.valueOf(o) + "\n");
495: text.repaint();
496: }
497:
498: public void print(final Object o) {
499: invokeAndWait(new Runnable() {
500: public void run() {
501: append(String.valueOf(o));
502: resetCommandStart();
503: text.setCaretPosition(cmdStart);
504: }
505: });
506: }
507:
508: /**
509: * Prints "\\n" (i.e. newline)
510: */
511: public void println() {
512: print("\n");
513: text.repaint();
514: }
515:
516: public void error(Object o) {
517: print(o, Color.red);
518: }
519:
520: public void println(Icon icon) {
521: print(icon);
522: println();
523: text.repaint();
524: }
525:
526: public void print(final Icon icon) {
527: if (icon == null)
528: return;
529:
530: invokeAndWait(new Runnable() {
531: public void run() {
532: text.insertIcon(icon);
533: resetCommandStart();
534: text.setCaretPosition(cmdStart);
535: }
536: });
537: }
538:
539: public void print(Object s, Font font) {
540: print(s, font, null);
541: }
542:
543: public void print(Object s, Color color) {
544: print(s, null, color);
545: }
546:
547: public void print(final Object o, final Font font, final Color color) {
548: invokeAndWait(new Runnable() {
549: public void run() {
550: AttributeSet old = getStyle();
551: setStyle(font, color);
552: append(String.valueOf(o));
553: resetCommandStart();
554: text.setCaretPosition(cmdStart);
555: setStyle(old, true);
556: }
557: });
558: }
559:
560: public void print(Object s, String fontFamilyName, int size,
561: Color color) {
562:
563: print(s, fontFamilyName, size, color, false, false, false);
564: }
565:
566: public void print(final Object o, final String fontFamilyName,
567: final int size, final Color color, final boolean bold,
568: final boolean italic, final boolean underline) {
569: invokeAndWait(new Runnable() {
570: public void run() {
571: AttributeSet old = getStyle();
572: setStyle(fontFamilyName, size, color, bold, italic,
573: underline);
574: append(String.valueOf(o));
575: resetCommandStart();
576: text.setCaretPosition(cmdStart);
577: setStyle(old, true);
578: }
579: });
580: }
581:
582: private AttributeSet setStyle(Font font) {
583: return setStyle(font, null);
584: }
585:
586: private AttributeSet setStyle(Color color) {
587: return setStyle(null, color);
588: }
589:
590: private AttributeSet setStyle(Font font, Color color) {
591: if (font != null)
592: return setStyle(font.getFamily(), font.getSize(), color,
593: font.isBold(), font.isItalic(), StyleConstants
594: .isUnderline(getStyle()));
595: else
596: return setStyle(null, -1, color);
597: }
598:
599: private AttributeSet setStyle(String fontFamilyName, int size,
600: Color color) {
601: MutableAttributeSet attr = new SimpleAttributeSet();
602: if (color != null)
603: StyleConstants.setForeground(attr, color);
604: if (fontFamilyName != null)
605: StyleConstants.setFontFamily(attr, fontFamilyName);
606: if (size != -1)
607: StyleConstants.setFontSize(attr, size);
608:
609: setStyle(attr);
610:
611: return getStyle();
612: }
613:
614: private AttributeSet setStyle(String fontFamilyName, int size,
615: Color color, boolean bold, boolean italic, boolean underline) {
616: MutableAttributeSet attr = new SimpleAttributeSet();
617: if (color != null)
618: StyleConstants.setForeground(attr, color);
619: if (fontFamilyName != null)
620: StyleConstants.setFontFamily(attr, fontFamilyName);
621: if (size != -1)
622: StyleConstants.setFontSize(attr, size);
623: StyleConstants.setBold(attr, bold);
624: StyleConstants.setItalic(attr, italic);
625: StyleConstants.setUnderline(attr, underline);
626:
627: setStyle(attr);
628:
629: return getStyle();
630: }
631:
632: private void setStyle(AttributeSet attributes) {
633: setStyle(attributes, false);
634: }
635:
636: private void setStyle(AttributeSet attributes, boolean overWrite) {
637: text.setCharacterAttributes(attributes, overWrite);
638: }
639:
640: private AttributeSet getStyle() {
641: return text.getCharacterAttributes();
642: }
643:
644: public void setFont(Font font) {
645: super .setFont(font);
646:
647: if (text != null)
648: text.setFont(font);
649: }
650:
651: private void inPipeWatcher() throws IOException {
652: byte[] ba = new byte[256]; // arbitrary blocking factor
653: int read;
654: while ((read = inPipe.read(ba)) != -1) {
655: print(new String(ba, 0, read));
656: //text.repaint();
657: }
658:
659: println("Console: Input closed...");
660: }
661:
662: public void run() {
663: try {
664: inPipeWatcher();
665: } catch (IOException e) {
666: print("Console: I/O Error: " + e + "\n", Color.red);
667: }
668: }
669:
670: public String toString() {
671: return "BeanShell console";
672: }
673:
674: // MouseListener Interface
675: public void mouseClicked(MouseEvent event) {
676: }
677:
678: public void mousePressed(MouseEvent event) {
679: if (event.isPopupTrigger()) {
680: menu.show((Component) event.getSource(), event.getX(),
681: event.getY());
682: }
683: }
684:
685: public void mouseReleased(MouseEvent event) {
686: if (event.isPopupTrigger()) {
687: menu.show((Component) event.getSource(), event.getX(),
688: event.getY());
689: }
690: text.repaint();
691: }
692:
693: public void mouseEntered(MouseEvent event) {
694: }
695:
696: public void mouseExited(MouseEvent event) {
697: }
698:
699: // property change
700: public void propertyChange(PropertyChangeEvent event) {
701: if (event.getPropertyName().equals("lookAndFeel")) {
702: SwingUtilities.updateComponentTreeUI(menu);
703: }
704: }
705:
706: // handle cut, copy and paste
707: public void actionPerformed(ActionEvent event) {
708: String cmd = event.getActionCommand();
709: if (cmd.equals(CUT)) {
710: text.cut();
711: } else if (cmd.equals(COPY)) {
712: text.copy();
713: } else if (cmd.equals(PASTE)) {
714: text.paste();
715: }
716: }
717:
718: /**
719: * If not in the event thread run via SwingUtilities.invokeAndWait()
720: */
721: private void invokeAndWait(Runnable run) {
722: if (!SwingUtilities.isEventDispatchThread()) {
723: try {
724: SwingUtilities.invokeAndWait(run);
725: } catch (Exception e) {
726: // shouldn't happen
727: e.printStackTrace();
728: }
729: } else {
730: run.run();
731: }
732: }
733:
734: /**
735: The overridden read method in this class will not throw "Broken pipe"
736: IOExceptions; It will simply wait for new writers and data.
737: This is used by the JConsole internal read thread to allow writers
738: in different (and in particular ephemeral) threads to write to the pipe.
739:
740: It also checks a little more frequently than the original read().
741:
742: Warning: read() will not even error on a read to an explicitly closed
743: pipe (override closed to for that).
744: */
745: public static class BlockingPipedInputStream extends
746: PipedInputStream {
747: boolean closed;
748:
749: public BlockingPipedInputStream(PipedOutputStream pout)
750: throws IOException {
751: super (pout);
752: }
753:
754: public synchronized int read() throws IOException {
755: if (closed)
756: throw new IOException("stream closed");
757:
758: while (super .in < 0) { // While no data */
759: notifyAll(); // Notify any writers to wake up
760: try {
761: wait(750);
762: } catch (InterruptedException e) {
763: throw new InterruptedIOException();
764: }
765: }
766: // This is what the superclass does.
767: int ret = buffer[super .out++] & 0xFF;
768: if (super .out >= buffer.length)
769: super .out = 0;
770: if (super .in == super .out)
771: super .in = -1; /* now empty */
772: return ret;
773: }
774:
775: public void close() throws IOException {
776: closed = true;
777: super .close();
778: }
779: }
780:
781: public void setNameCompletion(NameCompletion nc) {
782: this .nameCompletion = nc;
783: }
784:
785: public void setWaitFeedback(boolean on) {
786: if (on)
787: setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
788: else
789: setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
790: }
791:
792: private int textLength() {
793: return text.getDocument().getLength();
794: }
795:
796: }
|