ScriptConsole.java
// @formatter:off
/*******************************************************************************
*
* This file is part of JMad.
*
* Copyright (c) 2008-2011, CERN. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
******************************************************************************/
// @formatter:on
package cern.accsoft.steering.util.gui.script;
import java.awt.Color;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.Font;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.io.PrintStream;
import java.io.Reader;
import java.util.LinkedList;
import java.util.Queue;
import java.util.Vector;
import javax.script.ScriptEngine;
import javax.script.ScriptException;
import javax.swing.Icon;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JTextPane;
import javax.swing.SwingUtilities;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.DefaultStyledDocument;
import javax.swing.text.MutableAttributeSet;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyledDocument;
/**
* Script console for BSF-compliant languages. The console is independent of the scripting language, which can be
* changed dynamically. This class uses a BSFManager, and not an Interpreter, as most other examples do. NB: This class
* is inspired from bsh/util/JConsole.java by Patrick Niemeyer (pat@pat.net). JConsole is subject to the Sun Public
* License version 1.0 and to the GNU Lesser General Public License (the "LGPL"). http://cvs.sourceforge.
* net/viewcvs.py/beanshell/BeanShell/src/bsh/util/JConsole.java This is a small refactoring, which does not use the
* apache-bsf library anymore, but the java-internal classes
*
* @author Olivier Dameron (dameron@smi.stanford.edu), Patrick Niemeyer (pat@pat.net)
*/
public class ScriptConsole extends JScrollPane implements KeyListener, MouseListener, ActionListener {
private static final long serialVersionUID = 1L;
private final static String CUT = "Cut";
private final static String COPY = "Copy";
private final static String PASTE = "Paste";
private InputStream in;
private ConsoleOutputStream out = new ConsoleOutputStream();
private ConsoleOutputStream err = new ConsoleOutputStream();
private PrintStream outPrintStream = new PrintStream(out, true);
private PrintStream outErrStream = new PrintStream(err, true);
private PrintStream defaultSystemOutput = System.out;
private PrintStream defaultSystemError = System.err;
private int cmdStart = 0;
private Vector<String> history = new Vector<String>();
private String startedLine;
private int histLine = 0;
private String executionBuffer = "";
private boolean multilineCommand = false;
private JPopupMenu menu;
private JTextPane text;
private StyledDocument doc = new DefaultStyledDocument();
private ScriptEngine scriptEngine;
private String indentOffset;
private boolean output_able = true;
private MutableAttributeSet out_att = new SimpleAttributeSet();
private MutableAttributeSet err_att = new SimpleAttributeSet();
{
StyleConstants.setForeground(out_att, Color.GREEN);
StyleConstants.setForeground(err_att, Color.RED);
}
public ScriptConsole() {
this(null, null);
}
public ScriptConsole(InputStream cin, OutputStream cout) {
super();
indentOffset = "";
/*
* Special TextPane which catches for cut and paste, both L&F keys and programmatic behaviour
*/
text = new JTextPane(doc) {
private static final long serialVersionUID = 1L;
public void cut() {
if (text.getCaretPosition() < cmdStart) {
super.copy();
} else {
super.cut();
}
}
public void paste() {
forceCaretMoveToEnd();
super.paste();
}
};
Font font = new Font("Monospaced", Font.PLAIN, 12);
text.setText("");
text.setFont(font);
text.setMargin(new Insets(7, 5, 7, 5));
text.addKeyListener(this);
setViewportView(text);
// create popup menu
menu = new JPopupMenu("JConsole Menu");
menu.add(new JMenuItem(CUT)).addActionListener(this);
menu.add(new JMenuItem(COPY)).addActionListener(this);
menu.add(new JMenuItem(PASTE)).addActionListener(this);
text.addMouseListener(this);
requestFocus();
setStyle(Color.GREEN.darker());
println("Welcome the scripting console!");
setStyle(Color.black);
addPrompt();
}
public void captureSystemOut(boolean mode) {
if (mode) {
System.setOut(outPrintStream);
System.setErr(outErrStream);
} else {
System.setOut(defaultSystemOutput);
System.setErr(defaultSystemError);
}
}
public InputStream getInputStream() {
return in;
}
public Reader getIn() {
return new InputStreamReader(in);
}
public void requestFocus() {
super.requestFocus();
text.requestFocus();
}
public void keyPressed(KeyEvent e) {
type(e);
// gotUp=false;
}
public void keyTyped(KeyEvent e) {
type(e);
}
public void keyReleased(KeyEvent e) {
// gotUp=true;
type(e);
}
private void addPrompt() {
setStyle(Color.magenta);
if (!multilineCommand) {
append(">>> ");
} else {
append("... ");
}
// append(indentOffset);
setStyle(Color.black);
resetCommandStart();
}
private synchronized void type(KeyEvent e) {
/* Necessary for overcoming color bug */
setStyle(Color.black);
switch (e.getKeyCode()) {
case (KeyEvent.VK_ENTER):
if (e.getID() == KeyEvent.KEY_PRESSED) {
enter();
if (indentOffset.length() == 0) {
executeBuffer();
}
blankLine();
addPrompt();
resetCommandStart();
append(indentOffset);
text.setCaretPosition(cmdStart + indentOffset.length());
}
e.consume();
text.repaint();
break;
case (KeyEvent.VK_UP):
if (e.getID() == KeyEvent.KEY_PRESSED) {
historyUp();
}
e.consume();
break;
case (KeyEvent.VK_DOWN):
if (e.getID() == KeyEvent.KEY_PRESSED) {
historyDown();
}
e.consume();
break;
case (KeyEvent.VK_LEFT):
break;
case (KeyEvent.VK_BACK_SPACE):
// if (e.getID() == KeyEvent.KEY_PRESSED) {
// backspace();
// }
// e.consume();
// break;
case (KeyEvent.VK_DELETE):
if (text.getCaretPosition() <= cmdStart) {
// This doesn't work for backspace.
// See default case for workaround
e.consume();
}
break;
case (KeyEvent.VK_RIGHT):
forceCaretMoveToStart();
break;
case (KeyEvent.VK_HOME):
text.setCaretPosition(cmdStart);
e.consume();
break;
case (KeyEvent.VK_U): // clear line
if ((e.getModifiers() & InputEvent.CTRL_MASK) > 0) {
replaceRange("", cmdStart, textLength());
histLine = 0;
e.consume();
}
break;
case (KeyEvent.VK_ALT):
case (KeyEvent.VK_CAPS_LOCK):
case (KeyEvent.VK_CONTROL):
case (KeyEvent.VK_META):
case (KeyEvent.VK_SHIFT):
case (KeyEvent.VK_PRINTSCREEN):
case (KeyEvent.VK_SCROLL_LOCK):
case (KeyEvent.VK_PAUSE):
case (KeyEvent.VK_INSERT):
case (KeyEvent.VK_F1):
case (KeyEvent.VK_F2):
case (KeyEvent.VK_F3):
case (KeyEvent.VK_F4):
case (KeyEvent.VK_F5):
case (KeyEvent.VK_F6):
case (KeyEvent.VK_F7):
case (KeyEvent.VK_F8):
case (KeyEvent.VK_F9):
case (KeyEvent.VK_F10):
case (KeyEvent.VK_F11):
case (KeyEvent.VK_F12):
case (KeyEvent.VK_ESCAPE):
// only modifier pressed
break;
// Control-C
case (KeyEvent.VK_C):
if (text.getSelectedText() == null) {
if (((e.getModifiers() & InputEvent.CTRL_MASK) > 0) && (e.getID() == KeyEvent.KEY_PRESSED)) {
append("^C");
}
e.consume();
}
break;
// case ( KeyEvent.VK_TAB ):
// if (e.getID() == KeyEvent.KEY_RELEASED) {
// String part = text.getText().substring( cmdStart );
// doCommandCompletion( part );
// }
// e.consume();
// break;
default:
if ((e.getModifiers() & (InputEvent.CTRL_MASK | InputEvent.ALT_MASK | InputEvent.META_MASK)) == 0) {
// plain character
forceCaretMoveToEnd();
}
/*
* The getKeyCode function always returns VK_UNDEFINED for keyTyped events, so backspace is not fully
* consumed.
*/
if (e.paramString().indexOf("Backspace") != -1) {
if (text.getCaretPosition() <= cmdStart) {
e.consume();
break;
}
}
break;
}
}
private void resetCommandStart() {
cmdStart = textLength();
}
private void append(String string) {
int slen = textLength();
text.select(slen, slen);
text.replaceSelection(string);
}
private String replaceRange(Object s, int start, int end) {
String st = s.toString();
text.select(start, end);
text.replaceSelection(st);
// text.repaint();
return st;
}
private void forceCaretMoveToEnd() {
if (text.getCaretPosition() < cmdStart) {
// move caret first!
text.setCaretPosition(textLength());
}
text.repaint();
}
private void forceCaretMoveToStart() {
if (text.getCaretPosition() < cmdStart) {
// move caret first!
}
text.repaint();
}
private String getIndentation(String target) {
if (target.length() == 0) {
return "";
}
if (target.startsWith(" ")) {
return " " + getIndentation(target.substring(1));
}
return "";
}
private void blankLine() {
String s = getCmd();
if (s.equals(indentOffset + "\n")) {
indentOffset = "";
}
}
private void enter() {
String s = getCmd();
if (s.length() == 0) {
// special hack for empty return!
// s = ";\n";
} else {
history.addElement(s);
s = s + "\n";
}
append("\n");
histLine = 0;
acceptLine(s);
text.repaint();
indentOffset = getIndentation(s);
if (isJython()) {
if (s.endsWith(":\n")) {
indentOffset += " ";
}
}
multilineCommand = true; // will be reset to false in executeBuffer()
}
private boolean isJython() {
if (this.scriptEngine == null) {
return false;
}
return scriptEngine.toString().equals("jython");
}
private String getCmd() {
String s = "";
try {
s = text.getText(cmdStart, textLength() - cmdStart);
} catch (BadLocationException e) {
// should not happen
System.out.println("Internal JConsole Error: " + e);
}
return s;
}
private void historyUp() {
if (history.size() == 0)
return;
if (histLine == 0) // save current line
startedLine = getCmd();
if (histLine < history.size()) {
histLine++;
showHistoryLine();
}
}
private void historyDown() {
if (histLine == 0)
return;
histLine--;
showHistoryLine();
}
private void showHistoryLine() {
String showline;
if (histLine == 0)
showline = startedLine;
else
showline = (String) history.elementAt(history.size() - histLine);
replaceRange(showline, cmdStart, textLength());
text.setCaretPosition(textLength());
text.repaint();
}
String ZEROS = "000";
private void acceptLine(String line) {
// backup original line
String lineOrig = line;
// Switch color to blue
setStyle(Color.blue);
// Patch to handle Unicode characters
// Submitted by Daniel Leuck
StringBuffer buf = new StringBuffer();
int lineLength = line.length();
for (int i = 0; i < lineLength; i++) {
String val = Integer.toString(line.charAt(i), 16);
val = ZEROS.substring(0, 4 - val.length()) + val;
buf.append("\\u" + val);
}
line = buf.toString();
// End unicode patch
try {
addLineToExecutionBuffer(lineOrig);
} catch (Exception e) {
e.printStackTrace();
}
/* Restoring default black color */
setStyle(Color.black);
}
private void addLineToExecutionBuffer(String s) {
// executionBuffer.concat(s);
executionBuffer = executionBuffer + s;
}
private void executeBuffer() {
executeCommand(executionBuffer);
executionBuffer = "";
multilineCommand = false;
indentOffset = "";
}
public void executeCommand(String command) {
if (this.scriptEngine == null) {
print("No script engine set. Cannot execute command.\n", Color.red);
return;
}
captureSystemOut(true);
setStyle(Color.blue);
try {
this.scriptEngine.eval(command);
} catch (ScriptException e) {
setStyle(Color.red);
e.printStackTrace();
}
setStyle(Color.black);
captureSystemOut(false);
}
public void println(Object o) {
print(String.valueOf(o) + "\n");
text.repaint();
}
public void setScriptEngine(ScriptEngine scriptEngine) {
this.scriptEngine = scriptEngine;
AttributeSet style = setStyle(Color.green);
println("New scriptEngine: '" + scriptEngine.getFactory().getEngineName());
setStyle(style);
}
public void print(final Object o) {
print(o, null, null);
}
/**
* Prints "\\n" (i.e. newline)
*/
public void println() {
print("\n");
text.repaint();
}
public void error(Object o) {
print(o, Color.red);
}
public void println(Icon icon) {
print(icon);
println();
text.repaint();
}
public void print(final Icon icon) {
if (icon == null)
return;
invokeAndWait(new Runnable() {
public void run() {
text.insertIcon(icon);
resetCommandStart();
text.setCaretPosition(cmdStart);
}
});
}
public void print(Object s, Font font) {
print(s, font, null);
}
public void print(Object s, Color color) {
print(s, null, color);
}
public void print(final Object o, final Font font, final Color color) {
invokeAndWait(new Runnable() {
public void run() {
AttributeSet old = getStyle();
setStyle(font, color);
append(String.valueOf(o));
resetCommandStart();
text.setCaretPosition(cmdStart);
setStyle(old, true);
}
});
}
public void print(Object s, String fontFamilyName, int size, Color color) {
print(s, fontFamilyName, size, color, false, false, false);
}
public void print(final Object o, final String fontFamilyName, final int size, final Color color,
final boolean bold, final boolean italic, final boolean underline) {
invokeAndWait(new Runnable() {
public void run() {
AttributeSet old = getStyle();
setStyle(fontFamilyName, size, color, bold, italic, underline);
append(String.valueOf(o));
resetCommandStart();
text.setCaretPosition(cmdStart);
setStyle(old, true);
}
});
}
private AttributeSet setStyle(Color color) {
return setStyle(null, color);
}
private AttributeSet setStyle(Font font, Color color) {
if (font != null)
return setStyle(font.getFamily(), font.getSize(), color, font.isBold(), font.isItalic(),
StyleConstants.isUnderline(getStyle()));
else
return setStyle(null, -1, color);
}
private AttributeSet setStyle(String fontFamilyName, int size, Color color) {
MutableAttributeSet attr = new SimpleAttributeSet();
if (color != null)
StyleConstants.setForeground(attr, color);
if (fontFamilyName != null)
StyleConstants.setFontFamily(attr, fontFamilyName);
if (size != -1)
StyleConstants.setFontSize(attr, size);
setStyle(attr);
return getStyle();
}
private AttributeSet setStyle(String fontFamilyName, int size, Color color, boolean bold, boolean italic,
boolean underline) {
MutableAttributeSet attr = new SimpleAttributeSet();
if (color != null)
StyleConstants.setForeground(attr, color);
if (fontFamilyName != null)
StyleConstants.setFontFamily(attr, fontFamilyName);
if (size != -1)
StyleConstants.setFontSize(attr, size);
StyleConstants.setBold(attr, bold);
StyleConstants.setItalic(attr, italic);
StyleConstants.setUnderline(attr, underline);
setStyle(attr);
return getStyle();
}
private void setStyle(AttributeSet attributes) {
setStyle(attributes, false);
}
private void setStyle(AttributeSet attributes, boolean overWrite) {
text.setCharacterAttributes(attributes, overWrite);
}
private AttributeSet getStyle() {
return text.getCharacterAttributes();
}
public void setFont(Font font) {
super.setFont(font);
if (text != null)
text.setFont(font);
}
public String toString() {
return "BeanShell console";
}
// MouseListener Interface
public void mouseClicked(MouseEvent event) {
}
public void mousePressed(MouseEvent event) {
if (event.isPopupTrigger()) {
menu.show((Component) event.getSource(), event.getX(), event.getY());
}
}
public void mouseReleased(MouseEvent event) {
if (event.isPopupTrigger()) {
menu.show((Component) event.getSource(), event.getX(), event.getY());
}
text.repaint();
}
public void mouseEntered(MouseEvent event) {
giveFocusToConsole();
}
public void giveFocusToConsole() {
requestFocus();
text.setCaretPosition(textLength());
}
// handle cut, copy and paste
public void actionPerformed(ActionEvent event) {
String cmd = event.getActionCommand();
if (cmd.equals(CUT)) {
text.cut();
} else if (cmd.equals(COPY)) {
text.copy();
} else if (cmd.equals(PASTE)) {
text.paste();
}
}
/**
* If not in the event thread run via SwingUtilities.invokeAndWait()
*/
private void invokeAndWait(Runnable run) {
if (!SwingUtilities.isEventDispatchThread()) {
try {
SwingUtilities.invokeAndWait(run);
} catch (Exception e) {
// shouldn't happen
e.printStackTrace();
}
} else {
run.run();
}
}
/**
* The overridden read method in this class will not throw "Broken pipe" IOExceptions; It will simply wait for new
* writers and data. This is used by the JConsole internal read thread to allow writers in different (and in
* particular ephemeral) threads to write to the pipe. It also checks a little more frequently than the original
* read(). Warning: read() will not even error on a read to an explicitly closed pipe (override closed to for that).
*/
public static class BlockingPipedInputStream extends PipedInputStream {
boolean closed;
public BlockingPipedInputStream(PipedOutputStream pout) throws IOException {
super(pout);
}
public synchronized int read() throws IOException {
if (closed)
throw new IOException("stream closed");
while (super.in < 0) { // While no data */
notifyAll(); // Notify any writers to wake up
try {
wait(750);
} catch (InterruptedException e) {
throw new InterruptedIOException();
}
}
// This is what the superclass does.
int ret = buffer[super.out++] & 0xFF;
if (super.out >= buffer.length)
super.out = 0;
if (super.in == super.out)
super.in = -1; /* now empty */
return ret;
}
public void close() throws IOException {
closed = true;
super.close();
}
}
// public void setNameCompletion( NameCompletion nc ) {
// this.nameCompletion = nc;
// }
public void setWaitFeedback(boolean on) {
if (on)
setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
else
setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
}
private int textLength() {
return text.getDocument().getLength();
}
@Override
public void mouseExited(MouseEvent e) {
/* nothing to do */
}
private synchronized void print(byte[] b, ConsoleOutputStream os) {
try {
doc.insertString(doc.getLength(), new String(b, 0, b.length), os == out ? out_att : err_att);
} catch (BadLocationException e) {
e.printStackTrace();
}
}
/**
* OutputStream der Text in diesem JTextPane ausgibt
*/
private class ConsoleOutputStream extends OutputStream {
private Queue<Byte> buffer = new LinkedList<Byte>();
private boolean closed;
public ConsoleOutputStream() {
closed = false;
}
@Override
public void write(int b) throws IOException {
if (closed)
throw new IOException("Stream closed");
buffer.offer((byte) b);
}
@Override
public void flush() {
if (output_able)
synchronized (buffer) {
byte[] b = new byte[buffer.size()];
int cnt = 0;
while (!buffer.isEmpty())
b[cnt++] = buffer.poll();
print(b, this);
}
}
@Override
public void close() {
closed = true;
}
}
}