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;
46 import org.slf4j.Logger;
49 * A console with input and output area that can be embedded
50 * into any editor or view.
51 * @author Hannu Niemistö
53 public abstract class AbstractCommandConsole extends Composite {
56 * Use this option mask to hide and disable the console input field.
58 public static final int HIDE_INPUT = 1 << 0;
60 public static final String PLUGIN_ID = "org.simantics.scl.ui";
62 public static final int COMMAND_HISTORY_SIZE = 50;
64 public static final int SASH_HEIGHT = 3;
66 LocalResourceManager resourceManager;
68 protected final int options;
73 protected StyledText input;
75 int userInputHeight=0;
78 protected Color greenColor;
79 protected Color redColor;
81 FontRegistry fontRegistry;
82 FontDescriptor textFontDescriptor;
85 ArrayList<String> commandHistory = new ArrayList<String>();
86 int commandHistoryPos = 0;
88 boolean outputModiLock = false;
95 public AbstractCommandConsole(Composite parent, int style, int options) {
97 this.options = options;
102 public boolean setFocus() {
103 return input != null ? input.setFocus() : output.setFocus();
106 protected boolean canExecuteCommand() {
110 protected boolean hasOption(int mask) {
111 return (options & mask) != 0;
114 private void createControl() {
115 resourceManager = new LocalResourceManager(JFaceResources.getResources(), this);
116 greenColor = resourceManager.createColor(new RGB(0, 128, 0));
117 redColor = resourceManager.createColor(new RGB(172, 0, 0));
119 // Initialize current text font
120 fontRegistry = PlatformUI.getWorkbench().getThemeManager().getCurrentTheme().getFontRegistry();
121 fontRegistry.addListener(fontRegistryListener);
122 FontDescriptor font = FontDescriptor.createFrom( fontRegistry.getFontData("org.simantics.scl.consolefont") );
125 setLayout(new FormLayout());
128 sash = new Sash(this, /*SWT.BORDER |*/ SWT.HORIZONTAL);
129 sash.addListener(SWT.Selection, new Listener () {
130 public void handleEvent(Event e) {
131 Rectangle bounds = AbstractCommandConsole.this.getBounds();
132 int max = bounds.y + bounds.height;
134 userInputHeight = max-e.y;
136 int actualInputHeight = Math.max(userInputHeight, minInputHeight);
137 sash.setBounds(e.x, max-actualInputHeight, e.width, e.height);
138 setInputHeight(actualInputHeight);
143 output = new StyledText(this, SWT.MULTI /*| SWT.READ_ONLY*/ | SWT.V_SCROLL | SWT.H_SCROLL);
144 output.setFont(textFont);
145 output.setLayoutData( formData(0, sash, 0, 100) );
146 output.addVerifyListener(new VerifyListener() {
148 public void verifyText(VerifyEvent e) {
152 input.append(e.text);
154 input.setCaretOffset(input.getText().length());
160 if (hasOption(HIDE_INPUT)) {
161 sash.setLayoutData( formData(new Tuple2(100, 0), null, 0, 100, 0) );
169 addListener(SWT.Dispose, event -> {
170 if (fontRegistry != null)
171 fontRegistry.removeListener(fontRegistryListener);
174 } catch (IOException e) {
180 protected void createInputArea() {
181 deco = new StyledText(this, SWT.MULTI | SWT.READ_ONLY);
182 deco.setFont(textFont);
183 deco.setEnabled(false);
184 GC gc = new GC(deco);
185 int inputLeftPos = gc.getFontMetrics().getAverageCharWidth()*2;
188 deco.setLayoutData( formData(sash, 100, 0, new Tuple2(0, inputLeftPos)) );
191 input = new StyledText(this, SWT.MULTI);
192 input.setFont(textFont);
193 input.setLayoutData( formData(sash, 100, new Tuple2(0, inputLeftPos), 100) );
195 input.addVerifyKeyListener(event -> {
196 switch(event.keyCode) {
199 if((event.stateMask & SWT.CTRL) == 0) {
200 if(canExecuteCommand())
207 if((event.stateMask & SWT.CTRL) != 0) {
208 int targetHistoryPos = commandHistoryPos;
209 if(event.keyCode == SWT.ARROW_UP) {
210 if(commandHistoryPos <= 0)
215 if(commandHistoryPos >= commandHistory.size()-1)
219 setInputText(commandHistory.get(targetHistoryPos));
220 commandHistoryPos = targetHistoryPos;
226 // commandHistoryPos = commandHistory.size();
230 input.addVerifyListener(e -> {
231 if(e.text.contains("\n")) {
232 int lineId = input.getLineAtOffset(e.start);
233 int lineOffset = input.getOffsetAtLine(lineId);
236 lineOffset+indentAmount < input.getCharCount() &&
237 input.getTextRange(lineOffset+indentAmount, 1).equals(" ");
239 StringBuilder indent = new StringBuilder();
241 for(int i=0;i<indentAmount;++i)
243 e.text = e.text.replace("\n", indent);
246 input.addModifyListener(e -> {
247 adjustInputSize(input.getText());
248 commandHistoryPos = commandHistory.size();
251 Listener hoverListener = new Listener() {
253 DefaultToolTip toolTip = new DefaultToolTip(input, ToolTip.RECREATE, true);
256 boolean toolTipVisible = false;
259 public void handleEvent(Event e) {
261 case SWT.MouseHover: {
262 int offset = getOffsetInInput(e.x, e.y);
266 min = Integer.MIN_VALUE;
267 max = Integer.MAX_VALUE;
268 StringBuilder description = new StringBuilder();
269 boolean first = true;
270 for(ErrorAnnotation annotation : errorAnnotations) {
271 if(annotation.start <= offset && annotation.end > offset) {
272 min = Math.max(min, annotation.start);
273 max = Math.max(min, annotation.end);
277 description.append('\n');
278 description.append(annotation.description);
282 if(min != Integer.MIN_VALUE) {
283 Rectangle bounds = input.getTextBounds(min, max-1);
284 toolTip.setText(description.toString());
285 toolTip.show(new Point(bounds.x, bounds.y+bounds.height));
286 toolTipVisible = true;
292 int offset = getOffsetInInput(e.x, e.y);
293 if(offset < min || offset >= max) {
295 toolTipVisible = false;
303 toolTipVisible = false;
309 input.addListener(SWT.MouseHover, hoverListener);
310 input.addListener(SWT.MouseMove, hoverListener);
311 input.addListener(SWT.MouseExit, hoverListener);
314 private FormData formData(Object top, Object bottom, Object left, Object right) {
315 return formData(top, bottom, left, right, null);
318 private FormData formData(Object top, Object bottom, Object left, Object right, Integer height) {
319 FormData d = new FormData();
320 d.top = formAttachment(top);
321 d.bottom = formAttachment(bottom);
322 d.left = formAttachment(left);
323 d.right = formAttachment(right);
324 d.height = height != null ? (Integer) height : SWT.DEFAULT;
328 private FormAttachment formAttachment(Object o) {
331 if (o instanceof Control)
332 return new FormAttachment((Control) o);
333 if (o instanceof Integer)
334 return new FormAttachment((Integer) o);
335 if (o instanceof Tuple2) {
336 Tuple2 t = (Tuple2) o;
337 return new FormAttachment((Integer) t.c0, (Integer) t.c1);
339 throw new IllegalArgumentException("argument not supported: " + o);
342 private int getOffsetInInput(int x, int y) {
345 offset = input.getOffsetAtLocation(new Point(x, y));
346 } catch(IllegalArgumentException e) {
349 if(offset == input.getText().length())
351 else if(offset > 0) {
352 Rectangle rect = input.getTextBounds(offset, offset);
353 if(!rect.contains(x, y))
359 public void setInputText(String text) {
363 input.setCaretOffset(text.length());
364 adjustInputSize(text);
367 String validatedText;
369 Job validationJob = new Job("SCL input validation") {
372 protected IStatus run(IProgressMonitor monitor) {
373 String text = validatedText;
374 asyncSetErrorAnnotations(text, validate(text));
375 return Status.OK_STATUS;
380 Job preValidationJob = new Job("SCL input validation") {
382 protected IStatus run(IProgressMonitor monitor) {
383 if(!input.isDisposed()) {
384 input.getDisplay().asyncExec(() -> {
385 if(!input.isDisposed()) {
386 validatedText = input.getText();
387 validationJob.setPriority(Job.BUILD);
388 validationJob.schedule();
393 return Status.OK_STATUS;
397 private void asyncValidate() {
398 if(!input.getText().equals(errorAnnotationsForCommand)) {
399 preValidationJob.cancel();
400 preValidationJob.setPriority(Job.BUILD);
401 preValidationJob.schedule(500);
405 private static int rowCount(String text) {
407 for(int i=0;i<text.length();++i)
408 if(text.charAt(i)=='\n')
413 private void adjustInputSize(String text) {
414 int lineHeight = input.getLineHeight();
415 int height = rowCount(text)*lineHeight+SASH_HEIGHT;
416 if(height != minInputHeight) {
417 minInputHeight = height;
418 setInputHeight(Math.max(minInputHeight, userInputHeight));
422 private void setInputHeight(int inputHeight) {
423 sash.setLayoutData( formData(new Tuple2(100, -inputHeight), null, 0, 100, SASH_HEIGHT) );
424 AbstractCommandConsole.this.layout(true);
427 private StringBuilder outputBuffer = new StringBuilder();
428 private ArrayList<StyleRange> styleRanges = new ArrayList<StyleRange>();
429 private volatile boolean outputScheduled = false;
431 public void appendOutput(final String text, final Color foreground, final Color background) {
432 synchronized (outputBuffer) {
433 styleRanges.add(new StyleRange(outputBuffer.length(), text.length(), foreground, background));
434 outputBuffer.append(text);
436 if(!outputScheduled) {
437 outputScheduled = true;
438 final Display display = Display.getDefault();
439 if(display.isDisposed()) return;
440 display.asyncExec(() -> {
441 if(output.isDisposed()) return;
443 StyleRange[] styleRangeArray;
444 synchronized(outputBuffer) {
445 outputScheduled = false;
447 outputText = outputBuffer.toString();
448 outputBuffer = new StringBuilder();
450 styleRangeArray = styleRanges.toArray(new StyleRange[styleRanges.size()]);
453 int pos = output.getCharCount();
455 outputModiLock = true;
456 output.replaceTextRange(pos, 0, outputText);
457 outputModiLock = false;
459 for(StyleRange styleRange : styleRangeArray) {
460 styleRange.start += pos;
461 output.setStyleRange(styleRange);
464 output.setCaretOffset(output.getCharCount());
465 output.showSelection();
470 private void execute() {
471 String command = input.getText().trim();
472 if(command.isEmpty())
475 // Add command to command history
476 if(commandHistory.isEmpty() || !commandHistory.get(commandHistory.size()-1).equals(command)) {
477 commandHistory.add(command);
478 if(commandHistory.size() > COMMAND_HISTORY_SIZE*2)
479 commandHistory = new ArrayList<String>(
480 commandHistory.subList(COMMAND_HISTORY_SIZE, COMMAND_HISTORY_SIZE*2));
482 commandHistoryPos = commandHistory.size();
484 // Print it into output area
485 //appendOutput("> " + command.replace("\n", "\n ") + "\n", greenColor, null);
492 public static final ErrorAnnotation[] EMPTY_ANNOTATION_ARRAY = new ErrorAnnotation[0];
494 String errorAnnotationsForCommand;
495 ErrorAnnotation[] errorAnnotations = EMPTY_ANNOTATION_ARRAY;
497 private void syncSetErrorAnnotations(String forCommand, ErrorAnnotation[] annotations) {
498 errorAnnotationsForCommand = forCommand;
499 errorAnnotations = annotations;
502 StyleRange clearRange = new StyleRange(0, forCommand.length(), null, null);
503 input.setStyleRange(clearRange);
506 for(int i=0;i<annotations.length;++i) {
507 ErrorAnnotation annotation = annotations[i];
508 StyleRange range = new StyleRange(
510 annotation.end-annotation.start,
514 range.underline = true;
515 range.underlineColor = redColor;
516 range.underlineStyle = SWT.UNDERLINE_SQUIGGLE;
518 input.setStyleRange(range);
519 } catch(IllegalArgumentException e) {
522 input.setStyleRange(range);
523 getLogger().error("The following error message didn't have a proper location: {}", annotation.description, e);
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());
606 public abstract Logger getLogger();