// GraphTea Project: http://github.com/graphtheorysoftware/GraphTea
// Copyright (C) 2012 Graph Theory Software Foundation: http://GraphTheorySoftware.com
// Copyright (C) 2008 Mathematical Science Department of Sharif University of Technology
// Distributed under the terms of the GNU General Public License (GPL): http://www.gnu.org/licenses/
package graphtea.plugins.commandline;
import bsh.ConsoleInterface;
import bsh.util.GUIConsoleInterface;
import bsh.util.NameCompletion;
import graphtea.platform.core.exception.ExceptionHandler;
import graphtea.plugins.commandline.parsers.DefaultParser;
import graphtea.plugins.commandline.util.Utils;
import javax.swing.*;
import javax.swing.text.*;
import java.awt.*;
import java.awt.event.*;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.*;
import java.util.Vector;
public class ShellConsole extends JScrollPane
implements GUIConsoleInterface, Runnable, KeyListener,
MouseListener, ActionListener, PropertyChangeListener {
private final static String CUT = "Cut";
private final static String COPY = "Copy";
private final static String PASTE = "Paste";
private OutputStream outPipe;
private InputStream inPipe;
private InputStream in;
private PrintStream out;
public Shell shell;
public InputStream getInputStream() {
return in;
}
public Reader getIn() {
return new InputStreamReader(in);
}
public PrintStream getOut() {
return out;
}
public PrintStream getErr() {
return out;
}
private int cmdStart = 0;
private Vector history = new Vector();
private String startedLine;
private int histLine = 0;
private JPopupMenu menu;
private JTextPane text;
private DefaultStyledDocument doc;
NameCompletion nameCompletion;
final int SHOW_AMBIG_MAX = 15;
// hack to prevent key repeat for some reason?
private boolean gotUp = true;
public ShellConsole() {
this(null, null);
}
public ConsoleInterface console_interface;
public boolean is_interface = false;
public void set(ConsoleInterface ci) {
console_interface = ci;
is_interface = true;
}
public ShellConsole(InputStream cin, OutputStream cout) {
super();
// parser.put(dp.getName(), dp);
is_interface = false;
// Special TextPane which catches for cut and paste, both L&F keys and
// programmatic behaviour
text = new JTextPane(doc = new DefaultStyledDocument()) {
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("MyConsole 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);
// make sure popup menu follows Look & Feel
UIManager.addPropertyChangeListener(this);
outPipe = cout;
if (outPipe == null) {
outPipe = new PipedOutputStream();
try {
in = new PipedInputStream((PipedOutputStream) outPipe);
} catch (IOException e) {
print("Console internal error (1)...", Color.red);
}
}
inPipe = cin;
if (inPipe == null) {
PipedOutputStream pout = new PipedOutputStream();
out = new PrintStream(pout);
try {
inPipe = new BlockingPipedInputStream(pout);
} catch (IOException e) {
print("Console internal error: " + e);
}
}
// Start the inpipe watcher
new Thread(this).start();
requestFocus();
}
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 synchronized void type(KeyEvent e) {
switch (e.getKeyCode()) {
case(KeyEvent.VK_ENTER):
if (e.getID() == KeyEvent.KEY_PRESSED) {
if (gotUp) {
enter();
resetCommandStart();
text.setCaretPosition(cmdStart);
}
}
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):
case(KeyEvent.VK_BACK_SPACE):
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().contains("Backspace")) {
if (text.getCaretPosition() <= cmdStart) {
e.consume();
break;
}
}
break;
}
}
public void clear() {
text.setText("");
text.repaint();
}
private void doCommandCompletion(String part) {
String bk = part;
if (nameCompletion == null)
return;
int i = part.length() - 1;
// Character.isJavaIdentifierPart() How convenient for us!!
while (
i >= 0 &&
(Character.isJavaIdentifierPart(part.charAt(i))
|| part.charAt(i) == '.' || part.charAt(i) == '('
|| part.charAt(i) == ')' || part.charAt(i) == '[' || part.charAt(i) == ']')
)
i--;
part = part.substring(i + 1);
if (part.length() < 1) // reasonable completion length
return;
int index = Math.max(bk.lastIndexOf("bsh % "), bk.lastIndexOf(">> "));
if (index <= 0) index = 0;
else index = Math.max(bk.lastIndexOf("bsh % ") + 6, bk.lastIndexOf(">> ") + 4);
String ret = bk.substring(index, i + 1);
ret.trim();
// no completion
String[] complete = nameCompletion.completeName(part);
if (complete.length == 0) {
java.awt.Toolkit.getDefaultToolkit().beep();
return;
}
// Found one completion (possibly what we already have)
if (complete.length == 1 && !complete.equals(part)) {
if (part.endsWith("(")) {
if (complete[0].equals(part + ");")) {
String append = complete[0].substring(part.length());
append(append);
return;
}
} else if (!part.startsWith("_")) {
String append = complete[0].substring(part.length()) + "(";
append(append);
return;
} else {
if (!complete[0].startsWith("_")) {
text.select(textLength() - part.length(), textLength());
text.replaceSelection(complete[0]);
return;
}
}
}
// Found ambiguous, show (some of) them
String line = text.getText();
// String command = line.substring(cmdStart);
// Find prompt
for (i = cmdStart; line.charAt(i) != '\n' && i > 0; i--) ;
String prompt = ">> ";//line.substring( i+1, cmdStart );
// Show ambiguous
StringBuffer sb = new StringBuffer("\n");
for (i = 0; i < complete.length && i < SHOW_AMBIG_MAX; i++)
sb.append(complete[i] + "\n");
if (i == SHOW_AMBIG_MAX)
sb.append("...\n");
print(sb, Color.blue);
print(prompt); // print resets command start
if (complete.length != 1)
append(ret + Utils.getMaximumSimilarities(complete));
else append(ret + part);
}
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 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();
}
private String getCmd() {
String s = "";
try {
s = text.getText(cmdStart, textLength() - cmdStart);
} catch (BadLocationException e) {
// should not happen
System.out.println("Internal MyConsole 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";
//boolean is_equal = ;
//HashMap<String, ExtParser> parser = new HashMap<String, ExtParser>();
//ExtParser defaultExtParser;
boolean is_accepted = true;
String me_buffered = "";
private void acceptLine(String line) {
if (line.contains(";"))
if (!is_accepted) {
line = me_buffered + line;
me_buffered = "";
is_accepted = true;
} else me_buffered = "";
else {
me_buffered += line;
is_accepted = false;
return;
}
line = new DefaultParser(shell).parse(line);
// 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
if (outPipe == null)
print("Console internal error: cannot output ...", Color.red);
else
try {
outPipe.write(line.getBytes());
outPipe.flush();
} catch (IOException e) {
outPipe = null;
throw new RuntimeException("Console pipe broken...");
}
text.repaint();
}
public void println(Object o) {
if (is_interface) {
console_interface.println(o);
return;
}
print(String.valueOf(o) + "\n");
text.repaint();
}
public void print(final Object o) {
if (is_interface) {
console_interface.print(o);
return;
}
invokeAndWait(new Runnable() {
public void run() {
append(String.valueOf(o));
resetCommandStart();
text.setCaretPosition(cmdStart);
}
});
}
public Color getResultColor() {
return Color.blue;
}
public void printResult(Object s) {
print(s, getResultColor());
}
public void printlnResult(Object s) {
println(s, getResultColor());
}
/**
* Prints "\\n" (i.e. newline)
*/
public void println() {
print("\n");
text.repaint();
}
public void error(Object o) {
if (is_interface) {
console_interface.error(o);
return;
}
print("err: " + o, Color.red);
println();
}
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) {
if (is_interface) {
print(s);
return;
}
print(s, font, null);
}
public void println(Object s, Color color) {
if (is_interface) {
println(s);
return;
}
print(s, null, color);
println();
}
public void print(Object s, Color color) {
if (is_interface) {
print(s);
return;
}
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(Font font) {
return setStyle(font, null);
}
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);
}
private void inPipeWatcher() throws IOException {
byte[] ba = new byte[256]; // arbitrary blocking factor
int read;
while ((read = inPipe.read(ba)) != -1) {
print(new String(ba, 0, read));
//text.repaint();
}
println("Console: Input closed...");
}
public void run() {
try {
inPipeWatcher();
} catch (IOException e) {
print("Console: I/O Error: " + e + "\n", Color.red);
}
}
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) {
}
public void mouseExited(MouseEvent event) {
}
// property change
public void propertyChange(PropertyChangeEvent event) {
if (event.getPropertyName().equals("lookAndFeel")) {
SwingUtilities.updateComponentTreeUI(menu);
}
}
// handle cut, copy and paste
public void actionPerformed(ActionEvent event) {
String cmd = event.getActionCommand();
switch (cmd) {
case CUT:
text.cut();
break;
case COPY:
text.copy();
break;
case PASTE:
text.paste();
break;
}
}
/**
* 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
ExceptionHandler.catchException(e);
}
} 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 MyConsole internal read thread to allow writers
* in different (and in particular ephemeral) threads to write to the pipe.
* <p/>
* It also checks a little more frequently than the original read().
* <p/>
* 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();
}
}