]> gerrit.simantics Code Review - simantics/platform.git/blob - bundles/org.simantics.scl.ui/src/org/simantics/scl/ui/console/AbstractCommandConsole.java
Added SCL Script Output console view.
[simantics/platform.git] / bundles / org.simantics.scl.ui / src / org / simantics / scl / ui / console / AbstractCommandConsole.java
1 package org.simantics.scl.ui.console;
2
3 import java.io.IOException;
4 import java.util.ArrayList;
5 import java.util.Deque;
6
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.JFaceResources;
16 import org.eclipse.jface.resource.LocalResourceManager;
17 import org.eclipse.jface.window.DefaultToolTip;
18 import org.eclipse.jface.window.ToolTip;
19 import org.eclipse.swt.SWT;
20 import org.eclipse.swt.custom.StyleRange;
21 import org.eclipse.swt.custom.StyledText;
22 import org.eclipse.swt.events.VerifyEvent;
23 import org.eclipse.swt.events.VerifyListener;
24 import org.eclipse.swt.graphics.Color;
25 import org.eclipse.swt.graphics.Font;
26 import org.eclipse.swt.graphics.GC;
27 import org.eclipse.swt.graphics.Point;
28 import org.eclipse.swt.graphics.RGB;
29 import org.eclipse.swt.graphics.Rectangle;
30 import org.eclipse.swt.layout.FormAttachment;
31 import org.eclipse.swt.layout.FormData;
32 import org.eclipse.swt.layout.FormLayout;
33 import org.eclipse.swt.widgets.Composite;
34 import org.eclipse.swt.widgets.Control;
35 import org.eclipse.swt.widgets.Display;
36 import org.eclipse.swt.widgets.Event;
37 import org.eclipse.swt.widgets.Listener;
38 import org.eclipse.swt.widgets.Sash;
39 import org.eclipse.ui.preferences.ScopedPreferenceStore;
40 import org.simantics.scl.runtime.tuple.Tuple2;
41
42 /**
43  * A console with input and output area that can be embedded
44  * into any editor or view.
45  * @author Hannu Niemistö
46  */
47 public abstract class AbstractCommandConsole extends Composite {
48
49     /**
50      * Use this option mask to hide and disable the console input field.
51      */
52     public static final int HIDE_INPUT = 1 << 0;
53
54     public static final String PLUGIN_ID = "org.simantics.scl.ui";
55
56     public static final int COMMAND_HISTORY_SIZE = 50;
57     
58     public static final int SASH_HEIGHT = 3;
59     
60     LocalResourceManager resourceManager;
61
62     protected final int options;
63
64     StyledText output;
65     Sash sash;
66     protected StyledText input;
67     
68     int userInputHeight=0;
69     int minInputHeight=0;
70     
71     protected Color greenColor;
72     protected Color redColor;
73     protected Font textFont;
74
75     ArrayList<String> commandHistory = new ArrayList<String>();
76     int commandHistoryPos = 0;
77     
78     boolean outputModiLock = false;
79     
80     /*
81     Shell tip = null;
82     Label label = null;
83     */
84
85     public AbstractCommandConsole(Composite parent, int style, int options) {
86         super(parent, style);
87         this.options = options;
88         createControl();
89     }
90
91     @Override
92     public boolean setFocus() {
93         return input != null ? input.setFocus() : output.setFocus();
94     }
95
96     protected boolean canExecuteCommand() {
97         return true;
98     }
99
100     private boolean hasOption(int mask) {
101         return (options & mask) != 0;
102     }
103
104     private void createControl() {
105         resourceManager = new LocalResourceManager(JFaceResources.getResources(), this);
106         greenColor = resourceManager.createColor(new RGB(0, 128, 0));
107         redColor = resourceManager.createColor(new RGB(172, 0, 0));
108         textFont = resourceManager.createFont( FontDescriptor.createFrom("Courier New", 12, SWT.NONE) );
109
110         setLayout(new FormLayout());
111
112         // Sash
113         sash = new Sash(this, /*SWT.BORDER |*/ SWT.HORIZONTAL);
114         sash.addListener(SWT.Selection, new Listener () {
115             public void handleEvent(Event e) {
116                 Rectangle bounds = AbstractCommandConsole.this.getBounds();
117                 int max = bounds.y + bounds.height;
118
119                 userInputHeight = max-e.y;
120                 
121                 int actualInputHeight = Math.max(userInputHeight, minInputHeight);
122                 sash.setBounds(e.x, max-actualInputHeight, e.width, e.height);
123                 setInputHeight(actualInputHeight);
124             }
125         });
126
127         // Upper
128         output = new StyledText(this, SWT.MULTI /*| SWT.READ_ONLY*/ | SWT.V_SCROLL | SWT.H_SCROLL);
129         output.setFont(textFont);
130         output.setLayoutData( formData(0, sash, 0, 100) );
131         output.addVerifyListener(new VerifyListener() {
132             @Override
133             public void verifyText(VerifyEvent e) {
134                 if(outputModiLock)
135                     return;
136                 if (input != null) {
137                     input.append(e.text);
138                     input.setFocus();
139                     input.setCaretOffset(input.getText().length());
140                 }
141                 e.doit = false;
142             }
143         });
144
145         if (hasOption(HIDE_INPUT)) {
146             sash.setLayoutData( formData(new Tuple2(100, 0), null, 0, 100, 0) );
147             layout(true);
148         } else {
149             createInputArea();
150         }
151
152         readPreferences();
153
154         addListener(SWT.Dispose, event -> {
155             try {
156                 writePreferences();
157             } catch (IOException e) {
158                 e.printStackTrace();
159             }
160         });
161     }
162
163     protected void createInputArea() {
164         // "> " Decoration
165         StyledText deco = new StyledText(this, SWT.MULTI | SWT.READ_ONLY);
166         deco.setFont(textFont);
167         deco.setEnabled(false);
168         GC gc = new GC(deco);
169         int inputLeftPos = gc.getFontMetrics().getAverageCharWidth()*2;
170         gc.dispose();
171         deco.setText(">");
172         deco.setLayoutData( formData(sash, 100, 0, new Tuple2(0, inputLeftPos)) );
173
174         // Input area
175         input = new StyledText(this, SWT.MULTI);
176         input.setFont(textFont);
177         input.setLayoutData( formData(sash, 100, new Tuple2(0, inputLeftPos), 100) );
178         adjustInputSize("");
179         input.addVerifyKeyListener(event -> {
180             switch(event.keyCode) {
181             case SWT.KEYPAD_CR:
182             case SWT.CR:
183                 if((event.stateMask & SWT.CTRL) == 0) {
184                     if(canExecuteCommand())
185                         execute();
186                     event.doit = false;
187                 }
188                 break;
189             case SWT.ARROW_UP:
190             case SWT.ARROW_DOWN: 
191                 if((event.stateMask & SWT.CTRL) != 0) {
192                     int targetHistoryPos = commandHistoryPos;
193                     if(event.keyCode == SWT.ARROW_UP) {
194                         if(commandHistoryPos <= 0)
195                             return;
196                         --targetHistoryPos;
197                     }
198                     else {
199                         if(commandHistoryPos >= commandHistory.size()-1)
200                             return;
201                         ++targetHistoryPos;
202                     }
203                     setInputText(commandHistory.get(targetHistoryPos));
204                     commandHistoryPos = targetHistoryPos;
205                     event.doit = false;
206                 }
207                 break;
208 //            case SWT.ESC:
209 //                setInputText("");
210 //                commandHistoryPos = commandHistory.size();
211 //                break;
212             }
213         });
214         input.addVerifyListener(e -> {
215             if(e.text.contains("\n")) {
216                 int lineId = input.getLineAtOffset(e.start);
217                 int lineOffset = input.getOffsetAtLine(lineId);
218                 int indentAmount;
219                 for(indentAmount=0;
220                         lineOffset+indentAmount < input.getCharCount() && 
221                         input.getTextRange(lineOffset+indentAmount, 1).equals(" ");
222                         ++indentAmount);
223                 StringBuilder indent = new StringBuilder();
224                 indent.append('\n');
225                 for(int i=0;i<indentAmount;++i)
226                     indent.append(' ');
227                 e.text = e.text.replace("\n", indent);
228             }
229         });
230         input.addModifyListener(e -> {
231             adjustInputSize(input.getText());
232             commandHistoryPos = commandHistory.size();
233             //asyncValidate();
234         });
235         Listener hoverListener = new Listener() {
236             
237             DefaultToolTip toolTip = new DefaultToolTip(input, ToolTip.RECREATE, true);
238             
239             int min, max;
240             boolean toolTipVisible = false;
241             
242             @Override
243             public void handleEvent(Event e) {
244                 switch(e.type) {
245                 case SWT.MouseHover: {
246                     int offset = getOffsetInInput(e.x, e.y);
247                     if(offset == -1)
248                         return;
249                     
250                     min = Integer.MIN_VALUE;
251                     max = Integer.MAX_VALUE;
252                     StringBuilder description = new StringBuilder();
253                     boolean first = true;
254                     for(ErrorAnnotation annotation : errorAnnotations) {
255                         if(annotation.start <= offset && annotation.end > offset) {
256                             min = Math.max(min, annotation.start);
257                             max = Math.max(min, annotation.end);
258                             if(first)
259                                 first = false;
260                             else
261                                 description.append('\n');
262                             description.append(annotation.description);
263                         }
264                     }
265                     
266                     if(min != Integer.MIN_VALUE) {
267                         Rectangle bounds = input.getTextBounds(min, max-1);
268                         toolTip.setText(description.toString());
269                         toolTip.show(new Point(bounds.x, bounds.y+bounds.height));
270                         toolTipVisible = true;
271                     }
272                     return;
273                 }
274                 case SWT.MouseMove:
275                     if(toolTipVisible) {
276                         int offset = getOffsetInInput(e.x, e.y);
277                         if(offset < min || offset >= max) {
278                             toolTip.hide();
279                             toolTipVisible = false;
280                             return;
281                         }
282                     }
283                     return;
284                 case SWT.MouseExit:
285                     if(toolTipVisible) {
286                         toolTip.hide();
287                         toolTipVisible = false;
288                     }
289                     return;
290                 }
291             }
292         };
293         input.addListener(SWT.MouseHover, hoverListener);
294         input.addListener(SWT.MouseMove, hoverListener);
295         input.addListener(SWT.MouseExit, hoverListener);
296     }
297
298     private FormData formData(Object top, Object bottom, Object left, Object right) {
299         return formData(top, bottom, left, right, null);
300     }
301
302     private FormData formData(Object top, Object bottom, Object left, Object right, Integer height) {
303         FormData d = new FormData();
304         d.top = formAttachment(top);
305         d.bottom = formAttachment(bottom);
306         d.left = formAttachment(left);
307         d.right = formAttachment(right);
308         d.height = height != null ? (Integer) height : SWT.DEFAULT;
309         return d;
310     }
311
312     private FormAttachment formAttachment(Object o) {
313         if (o == null)
314             return null;
315         if (o instanceof Control)
316             return new FormAttachment((Control) o);
317         if (o instanceof Integer)
318             return new FormAttachment((Integer) o);
319         if (o instanceof Tuple2) {
320             Tuple2 t = (Tuple2) o;
321             return new FormAttachment((Integer) t.c0, (Integer) t.c1);
322         }
323         throw new IllegalArgumentException("argument not supported: " + o);
324     }
325
326     private int getOffsetInInput(int x, int y) {
327         int offset;
328         try {
329             offset = input.getOffsetAtLocation(new Point(x, y));
330         } catch(IllegalArgumentException e) {
331             return -1;
332         }
333         if(offset == input.getText().length())
334             --offset;
335         else if(offset > 0) {
336             Rectangle rect = input.getTextBounds(offset, offset);
337             if(!rect.contains(x, y))
338                 --offset;
339         }
340         return offset;
341     }
342     
343     public void setInputText(String text) {
344         if (input == null)
345             return;
346         input.setText(text);
347         input.setCaretOffset(text.length());
348         adjustInputSize(text);
349     }
350     
351     String validatedText;
352     
353     Job validationJob = new Job("SCL input validation") {
354
355         @Override
356         protected IStatus run(IProgressMonitor monitor) {
357             String text = validatedText;
358             asyncSetErrorAnnotations(text, validate(text));
359             return Status.OK_STATUS;
360         }
361         
362     };
363     
364     Job preValidationJob = new Job("SCL input validation") {
365         @Override
366         protected IStatus run(IProgressMonitor monitor) {
367             if(!input.isDisposed()) {
368                 input.getDisplay().asyncExec(() -> {
369                     if(!input.isDisposed()) {
370                         validatedText = input.getText();
371                         validationJob.setPriority(Job.BUILD);
372                         validationJob.schedule();
373                     }
374                 });
375             }
376             
377             return Status.OK_STATUS;
378         }
379     };
380     
381     private void asyncValidate() {
382         if(!input.getText().equals(errorAnnotationsForCommand)) {
383             preValidationJob.cancel();
384             preValidationJob.setPriority(Job.BUILD);
385             preValidationJob.schedule(500); 
386         }
387     }
388     
389     private static int rowCount(String text) {
390         int rowCount = 1;
391         for(int i=0;i<text.length();++i)
392             if(text.charAt(i)=='\n')
393                 ++rowCount;
394         return rowCount;
395     }
396     
397     private void adjustInputSize(String text) {
398         int lineHeight = input.getLineHeight();
399         int height = rowCount(text)*lineHeight+SASH_HEIGHT;
400         if(height != minInputHeight) {
401             minInputHeight = height;
402             setInputHeight(Math.max(minInputHeight, userInputHeight));
403         }
404     }
405     
406     private void setInputHeight(int inputHeight) {
407         sash.setLayoutData( formData(new Tuple2(100, -inputHeight), null, 0, 100, SASH_HEIGHT) );
408         AbstractCommandConsole.this.layout(true);
409     }
410
411     private StringBuilder outputBuffer = new StringBuilder();
412     private ArrayList<StyleRange> styleRanges = new ArrayList<StyleRange>();
413     private volatile boolean outputScheduled = false;
414
415     public void appendOutput(final String text, final Color foreground, final Color background) {
416         synchronized (outputBuffer) {
417             styleRanges.add(new StyleRange(outputBuffer.length(), text.length(), foreground, background));
418             outputBuffer.append(text);
419         }
420         if(!outputScheduled) {
421             outputScheduled = true;
422             final Display display = Display.getDefault();
423             if(display.isDisposed()) return;
424             display.asyncExec(() -> {
425                 if(output.isDisposed()) return;
426                 String outputText;
427                 StyleRange[] styleRangeArray;
428                 synchronized(outputBuffer) {
429                     outputScheduled = false;
430
431                     outputText = outputBuffer.toString();
432                     outputBuffer = new StringBuilder();
433
434                     styleRangeArray = styleRanges.toArray(new StyleRange[styleRanges.size()]);
435                     styleRanges.clear();
436                 }
437                 int pos = output.getCharCount();
438
439                 outputModiLock = true;
440                 output.replaceTextRange(pos, 0, outputText);
441                 outputModiLock = false;
442
443                 for(StyleRange styleRange : styleRangeArray) {
444                     styleRange.start += pos;
445                     output.setStyleRange(styleRange);
446                 }
447
448                 output.setCaretOffset(output.getCharCount());
449                 output.showSelection();
450             });
451         }
452     }
453
454     private void execute() {
455         String command = input.getText().trim();
456         if(command.isEmpty())
457             return;
458         
459         // Add command to command history
460         if(commandHistory.isEmpty() || !commandHistory.get(commandHistory.size()-1).equals(command)) {
461             commandHistory.add(command);
462             if(commandHistory.size() > COMMAND_HISTORY_SIZE*2)
463                 commandHistory = new ArrayList<String>(
464                         commandHistory.subList(COMMAND_HISTORY_SIZE, COMMAND_HISTORY_SIZE*2));
465         }
466         commandHistoryPos = commandHistory.size();
467         
468         // Print it into output area
469         //appendOutput("> " + command.replace("\n", "\n  ") + "\n", greenColor, null);
470         input.setText("");
471         
472         // Execute
473         execute(command);
474     }
475     
476     public static final  ErrorAnnotation[] EMPTY_ANNOTATION_ARRAY = new ErrorAnnotation[0]; 
477     
478     String errorAnnotationsForCommand;
479     ErrorAnnotation[] errorAnnotations = EMPTY_ANNOTATION_ARRAY;
480     
481     private void syncSetErrorAnnotations(String forCommand, ErrorAnnotation[] annotations) {
482         errorAnnotationsForCommand = forCommand;
483         errorAnnotations = annotations;
484
485         {
486             StyleRange clearRange = new StyleRange(0, forCommand.length(), null, null);
487             input.setStyleRange(clearRange);
488         }
489         
490         for(int i=0;i<annotations.length;++i) {
491             ErrorAnnotation annotation = annotations[i];
492             StyleRange range = new StyleRange(
493                     annotation.start,
494                     annotation.end-annotation.start,
495                     null,
496                     null
497                     );
498             range.underline = true;
499             range.underlineColor = redColor;
500             range.underlineStyle = SWT.UNDERLINE_SQUIGGLE;
501             try {
502                 input.setStyleRange(range);
503             } catch(IllegalArgumentException e) {
504                 range.start = 0;
505                 range.length = 1;
506                 input.setStyleRange(range);
507                 System.err.println("The following error message didn't have a proper location:");
508                 System.err.println(annotation.description);
509             }
510         }
511     }
512     
513     private void asyncSetErrorAnnotations(final String forCommand, final ErrorAnnotation[] annotations) {
514         if(input.isDisposed())
515             return;
516         input.getDisplay().asyncExec(new Runnable() {
517             @Override
518             public void run() {
519                 if(input.isDisposed())
520                     return;
521                 if(!input.getText().equals(forCommand))
522                     return;
523                 syncSetErrorAnnotations(forCommand, annotations);
524             }
525         });
526     }
527     
528     private boolean readPreferences() {
529         
530         IPreferenceStore store = new ScopedPreferenceStore(InstanceScope.INSTANCE, PLUGIN_ID);
531
532         String commandHistoryPref = store.getString(Preferences.COMMAND_HISTORY);
533         Deque<String> recentImportPaths = Preferences.decodePaths(commandHistoryPref);
534         
535         commandHistory = new ArrayList<String>(recentImportPaths);
536         commandHistoryPos = commandHistory.size();
537
538         return true;
539     }
540
541     private void writePreferences() throws IOException {
542         
543         IPersistentPreferenceStore store = new ScopedPreferenceStore(InstanceScope.INSTANCE, PLUGIN_ID);
544
545         store.putValue(Preferences.COMMAND_HISTORY, Preferences.encodePaths(commandHistory));
546
547         if (store.needsSaving())
548             store.save();
549         
550     }
551     
552     public abstract void execute(String command);
553     public abstract ErrorAnnotation[] validate(String command);
554     
555     public void clear() {
556         outputModiLock = true;
557         output.setText("");
558         outputModiLock = false;
559     }
560
561     public StyledText getOutputWidget() {
562         return output;
563     }
564
565 }