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.FontDescriptor; import org.eclipse.jface.resource.FontRegistry; import org.eclipse.jface.resource.JFaceResources; import org.eclipse.jface.resource.LocalResourceManager; import org.eclipse.jface.util.IPropertyChangeListener; import org.eclipse.jface.util.PropertyChangeEvent; 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.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.FontData; 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.Control; 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.PlatformUI; import org.eclipse.ui.preferences.ScopedPreferenceStore; import org.simantics.scl.runtime.tuple.Tuple2; import org.slf4j.Logger; /** * 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 { /** * Use this option mask to hide and disable the console input field. */ public static final int HIDE_INPUT = 1 << 0; 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; protected final int options; StyledText output; Sash sash; StyledText deco; protected StyledText input; int userInputHeight=0; int minInputHeight=0; protected Color greenColor; protected Color redColor; FontRegistry fontRegistry; FontDescriptor textFontDescriptor; Font textFont; ArrayList commandHistory = new ArrayList(); int commandHistoryPos = 0; boolean outputModiLock = false; /* Shell tip = null; Label label = null; */ public AbstractCommandConsole(Composite parent, int style, int options) { super(parent, style); this.options = options; createControl(); } @Override public boolean setFocus() { return input != null ? input.setFocus() : output.setFocus(); } protected boolean canExecuteCommand() { return true; } protected boolean hasOption(int mask) { return (options & mask) != 0; } 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)); // Initialize current text font fontRegistry = PlatformUI.getWorkbench().getThemeManager().getCurrentTheme().getFontRegistry(); fontRegistry.addListener(fontRegistryListener); FontDescriptor font = FontDescriptor.createFrom( fontRegistry.getFontData("org.simantics.scl.consolefont") ); setTextFont(font); setLayout(new FormLayout()); // 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); output.setLayoutData( formData(0, sash, 0, 100) ); output.addVerifyListener(new VerifyListener() { @Override public void verifyText(VerifyEvent e) { if(outputModiLock) return; if (input != null) { input.append(e.text); input.setFocus(); input.setCaretOffset(input.getText().length()); } e.doit = false; } }); if (hasOption(HIDE_INPUT)) { sash.setLayoutData( formData(new Tuple2(100, 0), null, 0, 100, 0) ); layout(true); } else { createInputArea(); } readPreferences(); addListener(SWT.Dispose, event -> { if (fontRegistry != null) fontRegistry.removeListener(fontRegistryListener); try { writePreferences(); } catch (IOException e) { e.printStackTrace(); } }); } protected void createInputArea() { 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; gc.dispose(); deco.setText(">"); deco.setLayoutData( formData(sash, 100, 0, new Tuple2(0, inputLeftPos)) ); // Input area input = new StyledText(this, SWT.MULTI); input.setFont(textFont); input.setLayoutData( formData(sash, 100, new Tuple2(0, inputLeftPos), 100) ); adjustInputSize(""); input.addVerifyKeyListener(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(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 { 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); } private FormData formData(Object top, Object bottom, Object left, Object right) { return formData(top, bottom, left, right, null); } private FormData formData(Object top, Object bottom, Object left, Object right, Integer height) { FormData d = new FormData(); d.top = formAttachment(top); d.bottom = formAttachment(bottom); d.left = formAttachment(left); d.right = formAttachment(right); d.height = height != null ? (Integer) height : SWT.DEFAULT; return d; } private FormAttachment formAttachment(Object o) { if (o == null) return null; if (o instanceof Control) return new FormAttachment((Control) o); if (o instanceof Integer) return new FormAttachment((Integer) o); if (o instanceof Tuple2) { Tuple2 t = (Tuple2) o; return new FormAttachment((Integer) t.c0, (Integer) t.c1); } throw new IllegalArgumentException("argument not supported: " + o); } 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) { if (input == null) return; 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(() -> { 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 styleRanges = new ArrayList(); private volatile boolean outputScheduled = false; public void appendOutput(final String text, final Color foreground, final Color background) { synchronized (outputBuffer) { styleRanges.add(new StyleRange(outputBuffer.length(), text.length(), foreground, background)); outputBuffer.append(text); } if(!outputScheduled) { outputScheduled = true; final Display display = Display.getDefault(); if(display.isDisposed()) return; display.asyncExec(() -> { if(output.isDisposed()) return; String outputText; StyleRange[] styleRangeArray; synchronized(outputBuffer) { outputScheduled = false; outputText = outputBuffer.toString(); outputBuffer = new StringBuilder(); styleRangeArray = styleRanges.toArray(new StyleRange[styleRanges.size()]); styleRanges.clear(); } int pos = output.getCharCount(); outputModiLock = true; output.replaceTextRange(pos, 0, outputText); outputModiLock = false; for(StyleRange styleRange : styleRangeArray) { styleRange.start += pos; output.setStyleRange(styleRange); } output.setCaretOffset(output.getCharCount()); output.showSelection(); }); } } private void execute() { String command = input.getText().trim(); if(command.isEmpty()) return; // Add command to command history if(commandHistory.isEmpty() || !commandHistory.get(commandHistory.size()-1).equals(command)) { commandHistory.add(command); if(commandHistory.size() > COMMAND_HISTORY_SIZE*2) commandHistory = new ArrayList( 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 { 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 recentImportPaths = Preferences.decodePaths(commandHistoryPref); commandHistory = new ArrayList(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; } IPropertyChangeListener fontRegistryListener = new IPropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent event) { setTextFont( FontDescriptor.createFrom((FontData[]) event.getNewValue()) ); } }; private void setTextFont(FontDescriptor font) { FontDescriptor oldFontDesc = textFontDescriptor; textFont = resourceManager.createFont(font); textFontDescriptor = font; applyTextFont(textFont); // Only destroy old font after the new font has been set! if (oldFontDesc != null) resourceManager.destroyFont(oldFontDesc); } private void applyTextFont(Font font) { if (output != null) output.setFont(font); if (deco != null) deco.setFont(font); if (input != null) { input.setFont(font); adjustInputSize(input.getText()); } } public abstract Logger getLogger(); }