1 package org.simantics.scl.ui.console;
3 import java.io.IOException;
4 import java.util.ArrayList;
5 import java.util.Deque;
7 import org.eclipse.core.runtime.IProgressMonitor;
8 import org.eclipse.core.runtime.IStatus;
9 import org.eclipse.core.runtime.Status;
10 import org.eclipse.core.runtime.jobs.Job;
11 import org.eclipse.core.runtime.preferences.InstanceScope;
12 import org.eclipse.jface.preference.IPersistentPreferenceStore;
13 import org.eclipse.jface.preference.IPreferenceStore;
14 import org.eclipse.jface.resource.FontDescriptor;
15 import org.eclipse.jface.resource.FontRegistry;
16 import org.eclipse.jface.resource.JFaceResources;
17 import org.eclipse.jface.resource.LocalResourceManager;
18 import org.eclipse.jface.util.IPropertyChangeListener;
19 import org.eclipse.jface.util.PropertyChangeEvent;
20 import org.eclipse.jface.window.DefaultToolTip;
21 import org.eclipse.jface.window.ToolTip;
22 import org.eclipse.swt.SWT;
23 import org.eclipse.swt.custom.StyleRange;
24 import org.eclipse.swt.custom.StyledText;
25 import org.eclipse.swt.events.VerifyEvent;
26 import org.eclipse.swt.events.VerifyListener;
27 import org.eclipse.swt.graphics.Color;
28 import org.eclipse.swt.graphics.Font;
29 import org.eclipse.swt.graphics.FontData;
30 import org.eclipse.swt.graphics.GC;
31 import org.eclipse.swt.graphics.Point;
32 import org.eclipse.swt.graphics.RGB;
33 import org.eclipse.swt.graphics.Rectangle;
34 import org.eclipse.swt.layout.FormAttachment;
35 import org.eclipse.swt.layout.FormData;
36 import org.eclipse.swt.layout.FormLayout;
37 import org.eclipse.swt.widgets.Composite;
38 import org.eclipse.swt.widgets.Control;
39 import org.eclipse.swt.widgets.Display;
40 import org.eclipse.swt.widgets.Event;
41 import org.eclipse.swt.widgets.Listener;
42 import org.eclipse.swt.widgets.Sash;
43 import org.eclipse.ui.PlatformUI;
44 import org.eclipse.ui.preferences.ScopedPreferenceStore;
45 import org.simantics.scl.runtime.tuple.Tuple2;
48 * A console with input and output area that can be embedded
49 * into any editor or view.
50 * @author Hannu Niemistö
52 public abstract class AbstractCommandConsole extends Composite {
55 * Use this option mask to hide and disable the console input field.
57 public static final int HIDE_INPUT = 1 << 0;
59 public static final String PLUGIN_ID = "org.simantics.scl.ui";
61 public static final int COMMAND_HISTORY_SIZE = 50;
63 public static final int SASH_HEIGHT = 3;
65 LocalResourceManager resourceManager;
67 protected final int options;
72 protected StyledText input;
74 int userInputHeight=0;
77 protected Color greenColor;
78 protected Color redColor;
80 FontRegistry fontRegistry;
81 FontDescriptor textFontDescriptor;
84 ArrayList<String> commandHistory = new ArrayList<String>();
85 int commandHistoryPos = 0;
87 boolean outputModiLock = false;
94 public AbstractCommandConsole(Composite parent, int style, int options) {
96 this.options = options;
101 public boolean setFocus() {
102 return input != null ? input.setFocus() : output.setFocus();
105 protected boolean canExecuteCommand() {
109 protected boolean hasOption(int mask) {
110 return (options & mask) != 0;
113 private void createControl() {
114 resourceManager = new LocalResourceManager(JFaceResources.getResources(), this);
115 greenColor = resourceManager.createColor(new RGB(0, 128, 0));
116 redColor = resourceManager.createColor(new RGB(172, 0, 0));
118 // Initialize current text font
119 fontRegistry = PlatformUI.getWorkbench().getThemeManager().getCurrentTheme().getFontRegistry();
120 fontRegistry.addListener(fontRegistryListener);
121 FontDescriptor font = FontDescriptor.createFrom( fontRegistry.getFontData("org.simantics.scl.consolefont") );
124 setLayout(new FormLayout());
127 sash = new Sash(this, /*SWT.BORDER |*/ SWT.HORIZONTAL);
128 sash.addListener(SWT.Selection, new Listener () {
129 public void handleEvent(Event e) {
130 Rectangle bounds = AbstractCommandConsole.this.getBounds();
131 int max = bounds.y + bounds.height;
133 userInputHeight = max-e.y;
135 int actualInputHeight = Math.max(userInputHeight, minInputHeight);
136 sash.setBounds(e.x, max-actualInputHeight, e.width, e.height);
137 setInputHeight(actualInputHeight);
142 output = new StyledText(this, SWT.MULTI /*| SWT.READ_ONLY*/ | SWT.V_SCROLL | SWT.H_SCROLL);
143 output.setFont(textFont);
144 output.setLayoutData( formData(0, sash, 0, 100) );
145 output.addVerifyListener(new VerifyListener() {
147 public void verifyText(VerifyEvent e) {
151 input.append(e.text);
153 input.setCaretOffset(input.getText().length());
159 if (hasOption(HIDE_INPUT)) {
160 sash.setLayoutData( formData(new Tuple2(100, 0), null, 0, 100, 0) );
168 addListener(SWT.Dispose, event -> {
169 if (fontRegistry != null)
170 fontRegistry.removeListener(fontRegistryListener);
173 } catch (IOException e) {
179 protected void createInputArea() {
180 deco = new StyledText(this, SWT.MULTI | SWT.READ_ONLY);
181 deco.setFont(textFont);
182 deco.setEnabled(false);
183 GC gc = new GC(deco);
184 int inputLeftPos = gc.getFontMetrics().getAverageCharWidth()*2;
187 deco.setLayoutData( formData(sash, 100, 0, new Tuple2(0, inputLeftPos)) );
190 input = new StyledText(this, SWT.MULTI);
191 input.setFont(textFont);
192 input.setLayoutData( formData(sash, 100, new Tuple2(0, inputLeftPos), 100) );
194 input.addVerifyKeyListener(event -> {
195 switch(event.keyCode) {
198 if((event.stateMask & SWT.CTRL) == 0) {
199 if(canExecuteCommand())
206 if((event.stateMask & SWT.CTRL) != 0) {
207 int targetHistoryPos = commandHistoryPos;
208 if(event.keyCode == SWT.ARROW_UP) {
209 if(commandHistoryPos <= 0)
214 if(commandHistoryPos >= commandHistory.size()-1)
218 setInputText(commandHistory.get(targetHistoryPos));
219 commandHistoryPos = targetHistoryPos;
225 // commandHistoryPos = commandHistory.size();
229 input.addVerifyListener(e -> {
230 if(e.text.contains("\n")) {
231 int lineId = input.getLineAtOffset(e.start);
232 int lineOffset = input.getOffsetAtLine(lineId);
235 lineOffset+indentAmount < input.getCharCount() &&
236 input.getTextRange(lineOffset+indentAmount, 1).equals(" ");
238 StringBuilder indent = new StringBuilder();
240 for(int i=0;i<indentAmount;++i)
242 e.text = e.text.replace("\n", indent);
245 input.addModifyListener(e -> {
246 adjustInputSize(input.getText());
247 commandHistoryPos = commandHistory.size();
250 Listener hoverListener = new Listener() {
252 DefaultToolTip toolTip = new DefaultToolTip(input, ToolTip.RECREATE, true);
255 boolean toolTipVisible = false;
258 public void handleEvent(Event e) {
260 case SWT.MouseHover: {
261 int offset = getOffsetInInput(e.x, e.y);
265 min = Integer.MIN_VALUE;
266 max = Integer.MAX_VALUE;
267 StringBuilder description = new StringBuilder();
268 boolean first = true;
269 for(ErrorAnnotation annotation : errorAnnotations) {
270 if(annotation.start <= offset && annotation.end > offset) {
271 min = Math.max(min, annotation.start);
272 max = Math.max(min, annotation.end);
276 description.append('\n');
277 description.append(annotation.description);
281 if(min != Integer.MIN_VALUE) {
282 Rectangle bounds = input.getTextBounds(min, max-1);
283 toolTip.setText(description.toString());
284 toolTip.show(new Point(bounds.x, bounds.y+bounds.height));
285 toolTipVisible = true;
291 int offset = getOffsetInInput(e.x, e.y);
292 if(offset < min || offset >= max) {
294 toolTipVisible = false;
302 toolTipVisible = false;
308 input.addListener(SWT.MouseHover, hoverListener);
309 input.addListener(SWT.MouseMove, hoverListener);
310 input.addListener(SWT.MouseExit, hoverListener);
313 private FormData formData(Object top, Object bottom, Object left, Object right) {
314 return formData(top, bottom, left, right, null);
317 private FormData formData(Object top, Object bottom, Object left, Object right, Integer height) {
318 FormData d = new FormData();
319 d.top = formAttachment(top);
320 d.bottom = formAttachment(bottom);
321 d.left = formAttachment(left);
322 d.right = formAttachment(right);
323 d.height = height != null ? (Integer) height : SWT.DEFAULT;
327 private FormAttachment formAttachment(Object o) {
330 if (o instanceof Control)
331 return new FormAttachment((Control) o);
332 if (o instanceof Integer)
333 return new FormAttachment((Integer) o);
334 if (o instanceof Tuple2) {
335 Tuple2 t = (Tuple2) o;
336 return new FormAttachment((Integer) t.c0, (Integer) t.c1);
338 throw new IllegalArgumentException("argument not supported: " + o);
341 private int getOffsetInInput(int x, int y) {
344 offset = input.getOffsetAtLocation(new Point(x, y));
345 } catch(IllegalArgumentException e) {
348 if(offset == input.getText().length())
350 else if(offset > 0) {
351 Rectangle rect = input.getTextBounds(offset, offset);
352 if(!rect.contains(x, y))
358 public void setInputText(String text) {
362 input.setCaretOffset(text.length());
363 adjustInputSize(text);
366 String validatedText;
368 Job validationJob = new Job("SCL input validation") {
371 protected IStatus run(IProgressMonitor monitor) {
372 String text = validatedText;
373 asyncSetErrorAnnotations(text, validate(text));
374 return Status.OK_STATUS;
379 Job preValidationJob = new Job("SCL input validation") {
381 protected IStatus run(IProgressMonitor monitor) {
382 if(!input.isDisposed()) {
383 input.getDisplay().asyncExec(() -> {
384 if(!input.isDisposed()) {
385 validatedText = input.getText();
386 validationJob.setPriority(Job.BUILD);
387 validationJob.schedule();
392 return Status.OK_STATUS;
396 private void asyncValidate() {
397 if(!input.getText().equals(errorAnnotationsForCommand)) {
398 preValidationJob.cancel();
399 preValidationJob.setPriority(Job.BUILD);
400 preValidationJob.schedule(500);
404 private static int rowCount(String text) {
406 for(int i=0;i<text.length();++i)
407 if(text.charAt(i)=='\n')
412 private void adjustInputSize(String text) {
413 int lineHeight = input.getLineHeight();
414 int height = rowCount(text)*lineHeight+SASH_HEIGHT;
415 if(height != minInputHeight) {
416 minInputHeight = height;
417 setInputHeight(Math.max(minInputHeight, userInputHeight));
421 private void setInputHeight(int inputHeight) {
422 sash.setLayoutData( formData(new Tuple2(100, -inputHeight), null, 0, 100, SASH_HEIGHT) );
423 AbstractCommandConsole.this.layout(true);
426 private StringBuilder outputBuffer = new StringBuilder();
427 private ArrayList<StyleRange> styleRanges = new ArrayList<StyleRange>();
428 private volatile boolean outputScheduled = false;
430 public void appendOutput(final String text, final Color foreground, final Color background) {
431 synchronized (outputBuffer) {
432 styleRanges.add(new StyleRange(outputBuffer.length(), text.length(), foreground, background));
433 outputBuffer.append(text);
435 if(!outputScheduled) {
436 outputScheduled = true;
437 final Display display = Display.getDefault();
438 if(display.isDisposed()) return;
439 display.asyncExec(() -> {
440 if(output.isDisposed()) return;
442 StyleRange[] styleRangeArray;
443 synchronized(outputBuffer) {
444 outputScheduled = false;
446 outputText = outputBuffer.toString();
447 outputBuffer = new StringBuilder();
449 styleRangeArray = styleRanges.toArray(new StyleRange[styleRanges.size()]);
452 int pos = output.getCharCount();
454 outputModiLock = true;
455 output.replaceTextRange(pos, 0, outputText);
456 outputModiLock = false;
458 for(StyleRange styleRange : styleRangeArray) {
459 styleRange.start += pos;
460 output.setStyleRange(styleRange);
463 output.setCaretOffset(output.getCharCount());
464 output.showSelection();
469 private void execute() {
470 String command = input.getText().trim();
471 if(command.isEmpty())
474 // Add command to command history
475 if(commandHistory.isEmpty() || !commandHistory.get(commandHistory.size()-1).equals(command)) {
476 commandHistory.add(command);
477 if(commandHistory.size() > COMMAND_HISTORY_SIZE*2)
478 commandHistory = new ArrayList<String>(
479 commandHistory.subList(COMMAND_HISTORY_SIZE, COMMAND_HISTORY_SIZE*2));
481 commandHistoryPos = commandHistory.size();
483 // Print it into output area
484 //appendOutput("> " + command.replace("\n", "\n ") + "\n", greenColor, null);
491 public static final ErrorAnnotation[] EMPTY_ANNOTATION_ARRAY = new ErrorAnnotation[0];
493 String errorAnnotationsForCommand;
494 ErrorAnnotation[] errorAnnotations = EMPTY_ANNOTATION_ARRAY;
496 private void syncSetErrorAnnotations(String forCommand, ErrorAnnotation[] annotations) {
497 errorAnnotationsForCommand = forCommand;
498 errorAnnotations = annotations;
501 StyleRange clearRange = new StyleRange(0, forCommand.length(), null, null);
502 input.setStyleRange(clearRange);
505 for(int i=0;i<annotations.length;++i) {
506 ErrorAnnotation annotation = annotations[i];
507 StyleRange range = new StyleRange(
509 annotation.end-annotation.start,
513 range.underline = true;
514 range.underlineColor = redColor;
515 range.underlineStyle = SWT.UNDERLINE_SQUIGGLE;
517 input.setStyleRange(range);
518 } catch(IllegalArgumentException e) {
521 input.setStyleRange(range);
522 System.err.println("The following error message didn't have a proper location:");
523 System.err.println(annotation.description);
528 private void asyncSetErrorAnnotations(final String forCommand, final ErrorAnnotation[] annotations) {
529 if(input.isDisposed())
531 input.getDisplay().asyncExec(() -> {
532 if(input.isDisposed())
534 if(!input.getText().equals(forCommand))
536 syncSetErrorAnnotations(forCommand, annotations);
540 private boolean readPreferences() {
542 IPreferenceStore store = new ScopedPreferenceStore(InstanceScope.INSTANCE, PLUGIN_ID);
544 String commandHistoryPref = store.getString(Preferences.COMMAND_HISTORY);
545 Deque<String> recentImportPaths = Preferences.decodePaths(commandHistoryPref);
547 commandHistory = new ArrayList<String>(recentImportPaths);
548 commandHistoryPos = commandHistory.size();
553 private void writePreferences() throws IOException {
555 IPersistentPreferenceStore store = new ScopedPreferenceStore(InstanceScope.INSTANCE, PLUGIN_ID);
557 store.putValue(Preferences.COMMAND_HISTORY, Preferences.encodePaths(commandHistory));
559 if (store.needsSaving())
564 public abstract void execute(String command);
565 public abstract ErrorAnnotation[] validate(String command);
567 public void clear() {
568 outputModiLock = true;
570 outputModiLock = false;
573 public StyledText getOutputWidget() {
577 IPropertyChangeListener fontRegistryListener = new IPropertyChangeListener() {
579 public void propertyChange(PropertyChangeEvent event) {
580 setTextFont( FontDescriptor.createFrom((FontData[]) event.getNewValue()) );
584 private void setTextFont(FontDescriptor font) {
585 FontDescriptor oldFontDesc = textFontDescriptor;
586 textFont = resourceManager.createFont(font);
587 textFontDescriptor = font;
588 applyTextFont(textFont);
590 // Only destroy old font after the new font has been set!
591 if (oldFontDesc != null)
592 resourceManager.destroyFont(oldFontDesc);
595 private void applyTextFont(Font font) {
597 output.setFont(font);
602 adjustInputSize(input.getText());