--- /dev/null
+package org.simantics.scl.ui.console;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Deque;
+
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.core.runtime.jobs.Job;
+import org.eclipse.core.runtime.preferences.InstanceScope;
+import org.eclipse.jface.preference.IPersistentPreferenceStore;
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.jface.resource.JFaceResources;
+import org.eclipse.jface.resource.LocalResourceManager;
+import org.eclipse.jface.window.DefaultToolTip;
+import org.eclipse.jface.window.ToolTip;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.StyleRange;
+import org.eclipse.swt.custom.StyledText;
+import org.eclipse.swt.custom.VerifyKeyListener;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.VerifyEvent;
+import org.eclipse.swt.events.VerifyListener;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.RGB;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.FormAttachment;
+import org.eclipse.swt.layout.FormData;
+import org.eclipse.swt.layout.FormLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.Sash;
+import org.eclipse.ui.preferences.ScopedPreferenceStore;
+
+/**
+ * A console with input and output area that can be embedded
+ * into any editor or view.
+ * @author Hannu Niemistö
+ */
+public abstract class AbstractCommandConsole extends Composite {
+
+ public static final String PLUGIN_ID = "org.simantics.scl.ui";
+
+ public static final int COMMAND_HISTORY_SIZE = 50;
+
+ public static final int SASH_HEIGHT = 3;
+
+ LocalResourceManager resourceManager;
+
+ StyledText output;
+ Sash sash;
+ protected StyledText input;
+
+ int userInputHeight=0;
+ int minInputHeight=0;
+
+ protected Color greenColor;
+ protected Color redColor;
+
+ ArrayList<String> commandHistory = new ArrayList<String>();
+ int commandHistoryPos = 0;
+
+ boolean outputModiLock = false;
+
+ /*
+ Shell tip = null;
+ Label label = null;
+ */
+
+ public AbstractCommandConsole(Composite parent, int style) {
+ super(parent, style);
+ createControl();
+ }
+
+ @Override
+ public boolean setFocus() {
+ return input.setFocus();
+ }
+
+ protected boolean canExecuteCommand() {
+ return true;
+ }
+
+ private void createControl() {
+ resourceManager = new LocalResourceManager(JFaceResources.getResources(), this);
+ greenColor = resourceManager.createColor(new RGB(0, 128, 0));
+ redColor = resourceManager.createColor(new RGB(172, 0, 0));
+
+ setLayout(new FormLayout());
+
+ Font textFont = new Font(getDisplay(),"Courier New",12,SWT.NONE);
+
+ // Sash
+ sash = new Sash(this, /*SWT.BORDER |*/ SWT.HORIZONTAL);
+ sash.addListener(SWT.Selection, new Listener () {
+ public void handleEvent(Event e) {
+ Rectangle bounds = AbstractCommandConsole.this.getBounds();
+ int max = bounds.y + bounds.height;
+
+ userInputHeight = max-e.y;
+
+ int actualInputHeight = Math.max(userInputHeight, minInputHeight);
+ sash.setBounds(e.x, max-actualInputHeight, e.width, e.height);
+ setInputHeight(actualInputHeight);
+ }
+ });
+
+ // Upper
+ output = new StyledText(this, SWT.MULTI /*| SWT.READ_ONLY*/ | SWT.V_SCROLL | SWT.H_SCROLL);
+ output.setFont(textFont);
+ {
+ FormData formData = new FormData();
+ formData.top = new FormAttachment(0);
+ formData.bottom = new FormAttachment(sash);
+ formData.left = new FormAttachment(0);
+ formData.right = new FormAttachment(100);
+ output.setLayoutData(formData);
+ }
+ output.addVerifyListener(new VerifyListener() {
+ @Override
+ public void verifyText(VerifyEvent e) {
+ if(outputModiLock)
+ return;
+ input.append(e.text);
+ input.setFocus();
+ input.setCaretOffset(input.getText().length());
+ e.doit = false;
+ }
+ });
+
+ // Deco
+ StyledText deco = new StyledText(this, SWT.MULTI | SWT.READ_ONLY);
+ deco.setFont(textFont);
+ deco.setEnabled(false);
+ GC gc = new GC(deco);
+ int inputLeftPos = gc.getFontMetrics().getAverageCharWidth()*2;
+ deco.setText(">");
+ {
+ FormData formData = new FormData();
+ formData.top = new FormAttachment(sash);
+ formData.bottom = new FormAttachment(100);
+ formData.left = new FormAttachment(0);
+ formData.right = new FormAttachment(0, inputLeftPos);
+ deco.setLayoutData(formData);
+ }
+
+ // Input area
+ input = new StyledText(this, SWT.MULTI);
+ input.setFont(textFont);
+ {
+ FormData formData = new FormData();
+ formData.top = new FormAttachment(sash);
+ formData.bottom = new FormAttachment(100);
+ formData.left = new FormAttachment(0, inputLeftPos);
+ formData.right = new FormAttachment(100);
+ input.setLayoutData(formData);
+ }
+ adjustInputSize("");
+ input.addVerifyKeyListener(new VerifyKeyListener() {
+
+ @Override
+ public void verifyKey(VerifyEvent event) {
+ switch(event.keyCode) {
+ case SWT.KEYPAD_CR:
+ case SWT.CR:
+ if((event.stateMask & SWT.CTRL) == 0) {
+ if(canExecuteCommand())
+ execute();
+ event.doit = false;
+ }
+ break;
+ case SWT.ARROW_UP:
+ case SWT.ARROW_DOWN:
+ if((event.stateMask & SWT.CTRL) != 0) {
+ int targetHistoryPos = commandHistoryPos;
+ if(event.keyCode == SWT.ARROW_UP) {
+ if(commandHistoryPos <= 0)
+ return;
+ --targetHistoryPos;
+ }
+ else {
+ if(commandHistoryPos >= commandHistory.size()-1)
+ return;
+ ++targetHistoryPos;
+ }
+ setInputText(commandHistory.get(targetHistoryPos));
+ commandHistoryPos = targetHistoryPos;
+ event.doit = false;
+ }
+ break;
+// case SWT.ESC:
+// setInputText("");
+// commandHistoryPos = commandHistory.size();
+// break;
+ }
+ }
+
+ });
+ input.addVerifyListener(new VerifyListener() {
+ @Override
+ public void verifyText(VerifyEvent e) {
+ if(e.text.contains("\n")) {
+ int lineId = input.getLineAtOffset(e.start);
+ int lineOffset = input.getOffsetAtLine(lineId);
+ int indentAmount;
+ for(indentAmount=0;
+ lineOffset+indentAmount < input.getCharCount() &&
+ input.getTextRange(lineOffset+indentAmount, 1).equals(" ");
+ ++indentAmount);
+ StringBuilder indent = new StringBuilder();
+ indent.append('\n');
+ for(int i=0;i<indentAmount;++i)
+ indent.append(' ');
+ e.text = e.text.replace("\n", indent);
+ }
+ }
+ });
+ input.addModifyListener(new ModifyListener() {
+ @Override
+ public void modifyText(ModifyEvent e) {
+ adjustInputSize(input.getText());
+ commandHistoryPos = commandHistory.size();
+ asyncValidate();
+ }
+ });
+ Listener hoverListener = new Listener() {
+
+ DefaultToolTip toolTip = new DefaultToolTip(input, ToolTip.RECREATE, true);
+
+ int min, max;
+ boolean toolTipVisible = false;
+
+ @Override
+ public void handleEvent(Event e) {
+ switch(e.type) {
+ case SWT.MouseHover: {
+ int offset = getOffsetInInput(e.x, e.y);
+ if(offset == -1)
+ return;
+
+ min = Integer.MIN_VALUE;
+ max = Integer.MAX_VALUE;
+ StringBuilder description = new StringBuilder();
+ boolean first = true;
+ for(ErrorAnnotation annotation : errorAnnotations) {
+ if(annotation.start <= offset && annotation.end > offset) {
+ min = Math.max(min, annotation.start);
+ max = Math.max(min, annotation.end);
+ if(first)
+ first = false;
+ else
+ description.append('\n');
+ description.append(annotation.description);
+ }
+ }
+
+ if(min != Integer.MIN_VALUE) {
+ Rectangle bounds = input.getTextBounds(min, max-1);
+ toolTip.setText(description.toString());
+ toolTip.show(new Point(bounds.x, bounds.y+bounds.height));
+ toolTipVisible = true;
+ }
+ return;
+ }
+ case SWT.MouseMove:
+ if(toolTipVisible) {
+ int offset = getOffsetInInput(e.x, e.y);
+ if(offset < min || offset >= max) {
+ toolTip.hide();
+ toolTipVisible = false;
+ return;
+ }
+ }
+ return;
+ case SWT.MouseExit:
+ if(toolTipVisible) {
+ toolTip.hide();
+ toolTipVisible = false;
+ }
+ return;
+ }
+ }
+ };
+ input.addListener(SWT.MouseHover, hoverListener);
+ input.addListener(SWT.MouseMove, hoverListener);
+ input.addListener(SWT.MouseExit, hoverListener);
+
+ readPreferences();
+
+ addListener(SWT.Dispose, new Listener() {
+
+ @Override
+ public void handleEvent(Event event) {
+ try {
+ writePreferences();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ });
+
+ }
+
+ private int getOffsetInInput(int x, int y) {
+ int offset;
+ try {
+ offset = input.getOffsetAtLocation(new Point(x, y));
+ } catch(IllegalArgumentException e) {
+ return -1;
+ }
+ if(offset == input.getText().length())
+ --offset;
+ else if(offset > 0) {
+ Rectangle rect = input.getTextBounds(offset, offset);
+ if(!rect.contains(x, y))
+ --offset;
+ }
+ return offset;
+ }
+
+ public void setInputText(String text) {
+ input.setText(text);
+ input.setCaretOffset(text.length());
+ adjustInputSize(text);
+ }
+
+ String validatedText;
+
+ Job validationJob = new Job("SCL input validation") {
+
+ @Override
+ protected IStatus run(IProgressMonitor monitor) {
+ String text = validatedText;
+ asyncSetErrorAnnotations(text, validate(text));
+ return Status.OK_STATUS;
+ }
+
+ };
+
+ Job preValidationJob = new Job("SCL input validation") {
+ @Override
+ protected IStatus run(IProgressMonitor monitor) {
+ if(!input.isDisposed()) {
+ input.getDisplay().asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ if(!input.isDisposed()) {
+ validatedText = input.getText();
+ validationJob.setPriority(Job.BUILD);
+ validationJob.schedule();
+ }
+ }
+ });
+ }
+
+ return Status.OK_STATUS;
+ }
+ };
+
+ private void asyncValidate() {
+ if(!input.getText().equals(errorAnnotationsForCommand)) {
+ preValidationJob.cancel();
+ preValidationJob.setPriority(Job.BUILD);
+ preValidationJob.schedule(500);
+ }
+ }
+
+ private static int rowCount(String text) {
+ int rowCount = 1;
+ for(int i=0;i<text.length();++i)
+ if(text.charAt(i)=='\n')
+ ++rowCount;
+ return rowCount;
+ }
+
+ private void adjustInputSize(String text) {
+ int lineHeight = input.getLineHeight();
+ int height = rowCount(text)*lineHeight+SASH_HEIGHT;
+ if(height != minInputHeight) {
+ minInputHeight = height;
+ setInputHeight(Math.max(minInputHeight, userInputHeight));
+ }
+ }
+
+ private void setInputHeight(int inputHeight) {
+ FormData formData = new FormData();
+ formData.top = new FormAttachment(100, -inputHeight);
+ formData.left = new FormAttachment(0);
+ formData.right = new FormAttachment(100);
+ formData.height = SASH_HEIGHT;
+ sash.setLayoutData(formData);
+ AbstractCommandConsole.this.layout(true);
+ }
+
+ public void appendOutput(final String text, final Color foreground, final Color background) {
+ final Display display = Display.getDefault();
+ if(display.isDisposed()) return;
+ display.asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ if(output.isDisposed()) return;
+ int pos = output.getCharCount();
+ outputModiLock = true;
+ output.replaceTextRange(pos, 0, text);
+ outputModiLock = false;
+ output.setStyleRange(new StyleRange(pos, text.length(),
+ foreground, background));
+ output.setCaretOffset(output.getCharCount());
+ output.showSelection();
+ }
+
+ });
+ }
+
+ private void execute() {
+ String command = input.getText().trim();
+ if(command.isEmpty())
+ return;
+
+ // Add command to command history
+ commandHistory.add(command);
+ if(commandHistory.size() > COMMAND_HISTORY_SIZE*2)
+ commandHistory = new ArrayList<String>(
+ commandHistory.subList(COMMAND_HISTORY_SIZE, COMMAND_HISTORY_SIZE*2));
+ commandHistoryPos = commandHistory.size();
+
+ // Print it into output area
+ //appendOutput("> " + command.replace("\n", "\n ") + "\n", greenColor, null);
+ input.setText("");
+
+ // Execute
+ execute(command);
+ }
+
+ public static final ErrorAnnotation[] EMPTY_ANNOTATION_ARRAY = new ErrorAnnotation[0];
+
+ String errorAnnotationsForCommand;
+ ErrorAnnotation[] errorAnnotations = EMPTY_ANNOTATION_ARRAY;
+
+ private void syncSetErrorAnnotations(String forCommand, ErrorAnnotation[] annotations) {
+ errorAnnotationsForCommand = forCommand;
+ errorAnnotations = annotations;
+
+ {
+ StyleRange clearRange = new StyleRange(0, forCommand.length(), null, null);
+ input.setStyleRange(clearRange);
+ }
+
+ for(int i=0;i<annotations.length;++i) {
+ ErrorAnnotation annotation = annotations[i];
+ StyleRange range = new StyleRange(
+ annotation.start,
+ annotation.end-annotation.start,
+ null,
+ null
+ );
+ range.underline = true;
+ range.underlineColor = redColor;
+ range.underlineStyle = SWT.UNDERLINE_SQUIGGLE;
+ try {
+ input.setStyleRange(range);
+ } catch(IllegalArgumentException e) {
+ range.start = 0;
+ range.length = 1;
+ input.setStyleRange(range);
+ System.err.println("The following error message didn't have a proper location:");
+ System.err.println(annotation.description);
+ }
+ }
+ }
+
+ private void asyncSetErrorAnnotations(final String forCommand, final ErrorAnnotation[] annotations) {
+ if(input.isDisposed())
+ return;
+ input.getDisplay().asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ if(input.isDisposed())
+ return;
+ if(!input.getText().equals(forCommand))
+ return;
+ syncSetErrorAnnotations(forCommand, annotations);
+ }
+ });
+ }
+
+ private boolean readPreferences() {
+
+ IPreferenceStore store = new ScopedPreferenceStore(InstanceScope.INSTANCE, PLUGIN_ID);
+
+ String commandHistoryPref = store.getString(Preferences.COMMAND_HISTORY);
+ Deque<String> recentImportPaths = Preferences.decodePaths(commandHistoryPref);
+
+ commandHistory = new ArrayList<String>(recentImportPaths);
+ commandHistoryPos = commandHistory.size();
+
+ return true;
+ }
+
+ private void writePreferences() throws IOException {
+
+ IPersistentPreferenceStore store = new ScopedPreferenceStore(InstanceScope.INSTANCE, PLUGIN_ID);
+
+ store.putValue(Preferences.COMMAND_HISTORY, Preferences.encodePaths(commandHistory));
+
+ if (store.needsSaving())
+ store.save();
+
+ }
+
+ public abstract void execute(String command);
+ public abstract ErrorAnnotation[] validate(String command);
+
+ public void clear() {
+ outputModiLock = true;
+ output.setText("");
+ outputModiLock = false;
+ }
+
+ public StyledText getOutputWidget() {
+ return output;
+ }
+
+}