]> gerrit.simantics Code Review - simantics/platform.git/blob - bundles/org.simantics.scl.ui/src/org/simantics/scl/ui/console/AbstractCommandConsole.java
b0019688386fcbbf8f7d019dad052630c75410d0
[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.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;
47
48 /**
49  * A console with input and output area that can be embedded
50  * into any editor or view.
51  * @author Hannu Niemistö
52  */
53 public abstract class AbstractCommandConsole extends Composite {
54
55     /**
56      * Use this option mask to hide and disable the console input field.
57      */
58     public static final int HIDE_INPUT = 1 << 0;
59
60     public static final String PLUGIN_ID = "org.simantics.scl.ui"; //$NON-NLS-1$
61
62     public static final int COMMAND_HISTORY_SIZE = 50;
63     
64     public static final int SASH_HEIGHT = 3;
65     
66     LocalResourceManager resourceManager;
67
68     protected final int options;
69
70     StyledText output;
71     Sash sash;
72     StyledText deco;
73     protected StyledText input;
74     
75     int userInputHeight=0;
76     int minInputHeight=0;
77     
78     protected Color greenColor;
79     protected Color redColor;
80
81     FontRegistry fontRegistry;
82     FontDescriptor textFontDescriptor;
83     Font textFont;
84
85     ArrayList<String> commandHistory = new ArrayList<String>();
86     int commandHistoryPos = 0;
87     
88     boolean outputModiLock = false;
89     
90     /*
91     Shell tip = null;
92     Label label = null;
93     */
94
95     public AbstractCommandConsole(Composite parent, int style, int options) {
96         super(parent, style);
97         this.options = options;
98         createControl();
99     }
100
101     @Override
102     public boolean setFocus() {
103         return input != null ? input.setFocus() : output.setFocus();
104     }
105
106     protected boolean canExecuteCommand() {
107         return true;
108     }
109
110     protected boolean hasOption(int mask) {
111         return (options & mask) != 0;
112     }
113
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));
118
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") ); //$NON-NLS-1$
123         setTextFont(font);
124
125         setLayout(new FormLayout());
126
127         // Sash
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;
133
134                 userInputHeight = max-e.y;
135                 
136                 int actualInputHeight = Math.max(userInputHeight, minInputHeight);
137                 sash.setBounds(e.x, max-actualInputHeight, e.width, e.height);
138                 setInputHeight(actualInputHeight);
139             }
140         });
141
142         // Upper
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() {
147             @Override
148             public void verifyText(VerifyEvent e) {
149                 if(outputModiLock)
150                     return;
151                 if (input != null) {
152                     input.append(e.text);
153                     input.setFocus();
154                     input.setCaretOffset(input.getText().length());
155                 }
156                 e.doit = false;
157             }
158         });
159
160         if (hasOption(HIDE_INPUT)) {
161             sash.setLayoutData( formData(new Tuple2(100, 0), null, 0, 100, 0) );
162             layout(true);
163         } else {
164             createInputArea();
165         }
166
167         readPreferences();
168
169         addListener(SWT.Dispose, event -> {
170             if (fontRegistry != null)
171                 fontRegistry.removeListener(fontRegistryListener);
172             try {
173                 writePreferences();
174             } catch (IOException e) {
175                 e.printStackTrace();
176             }
177         });
178     }
179
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;
186         gc.dispose();
187         deco.setText(">"); //$NON-NLS-1$
188         deco.setLayoutData( formData(sash, 100, 0, new Tuple2(0, inputLeftPos)) );
189
190         // Input area
191         input = new StyledText(this, SWT.MULTI);
192         input.setFont(textFont);
193         input.setLayoutData( formData(sash, 100, new Tuple2(0, inputLeftPos), 100) );
194         adjustInputSize(""); //$NON-NLS-1$
195         input.addVerifyKeyListener(event -> {
196             switch(event.keyCode) {
197             case SWT.KEYPAD_CR:
198             case SWT.CR:
199                 if((event.stateMask & SWT.CTRL) == 0) {
200                     if(canExecuteCommand())
201                         execute();
202                     event.doit = false;
203                 }
204                 break;
205             case SWT.ARROW_UP:
206             case SWT.ARROW_DOWN: 
207                 if((event.stateMask & SWT.CTRL) != 0) {
208                     int targetHistoryPos = commandHistoryPos;
209                     if(event.keyCode == SWT.ARROW_UP) {
210                         if(commandHistoryPos <= 0)
211                             return;
212                         --targetHistoryPos;
213                     }
214                     else {
215                         if(commandHistoryPos >= commandHistory.size()-1)
216                             return;
217                         ++targetHistoryPos;
218                     }
219                     setInputText(commandHistory.get(targetHistoryPos));
220                     commandHistoryPos = targetHistoryPos;
221                     event.doit = false;
222                 }
223                 break;
224 //            case SWT.ESC:
225 //                setInputText("");
226 //                commandHistoryPos = commandHistory.size();
227 //                break;
228             }
229         });
230         input.addVerifyListener(e -> {
231             if(e.text.contains("\n")) { //$NON-NLS-1$
232                 int lineId = input.getLineAtOffset(e.start);
233                 int lineOffset = input.getOffsetAtLine(lineId);
234                 int indentAmount;
235                 for(indentAmount=0;
236                         lineOffset+indentAmount < input.getCharCount() && 
237                         input.getTextRange(lineOffset+indentAmount, 1).equals(" "); //$NON-NLS-1$
238                         ++indentAmount);
239                 StringBuilder indent = new StringBuilder();
240                 indent.append('\n');
241                 for(int i=0;i<indentAmount;++i)
242                     indent.append(' ');
243                 e.text = e.text.replace("\n", indent); //$NON-NLS-1$
244             }
245         });
246         input.addModifyListener(e -> {
247             adjustInputSize(input.getText());
248             commandHistoryPos = commandHistory.size();
249             //asyncValidate();
250         });
251         Listener hoverListener = new Listener() {
252             
253             DefaultToolTip toolTip = new DefaultToolTip(input, ToolTip.RECREATE, true);
254             
255             int min, max;
256             boolean toolTipVisible = false;
257             
258             @Override
259             public void handleEvent(Event e) {
260                 switch(e.type) {
261                 case SWT.MouseHover: {
262                     int offset = getOffsetInInput(e.x, e.y);
263                     if(offset == -1)
264                         return;
265                     
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);
274                             if(first)
275                                 first = false;
276                             else
277                                 description.append('\n');
278                             description.append(annotation.description);
279                         }
280                     }
281                     
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;
287                     }
288                     return;
289                 }
290                 case SWT.MouseMove:
291                     if(toolTipVisible) {
292                         int offset = getOffsetInInput(e.x, e.y);
293                         if(offset < min || offset >= max) {
294                             toolTip.hide();
295                             toolTipVisible = false;
296                             return;
297                         }
298                     }
299                     return;
300                 case SWT.MouseExit:
301                     if(toolTipVisible) {
302                         toolTip.hide();
303                         toolTipVisible = false;
304                     }
305                     return;
306                 }
307             }
308         };
309         input.addListener(SWT.MouseHover, hoverListener);
310         input.addListener(SWT.MouseMove, hoverListener);
311         input.addListener(SWT.MouseExit, hoverListener);
312     }
313
314     private FormData formData(Object top, Object bottom, Object left, Object right) {
315         return formData(top, bottom, left, right, null);
316     }
317
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;
325         return d;
326     }
327
328     private FormAttachment formAttachment(Object o) {
329         if (o == null)
330             return null;
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);
338         }
339         throw new IllegalArgumentException("argument not supported: " + o); //$NON-NLS-1$
340     }
341
342     private int getOffsetInInput(int x, int y) {
343         int offset;
344         try {
345             offset = input.getOffsetAtLocation(new Point(x, y));
346         } catch(IllegalArgumentException e) {
347             return -1;
348         }
349         if(offset == input.getText().length())
350             --offset;
351         else if(offset > 0) {
352             Rectangle rect = input.getTextBounds(offset, offset);
353             if(!rect.contains(x, y))
354                 --offset;
355         }
356         return offset;
357     }
358     
359     public void setInputText(String text) {
360         if (input == null)
361             return;
362         input.setText(text);
363         input.setCaretOffset(text.length());
364         adjustInputSize(text);
365     }
366     
367     String validatedText;
368     
369     Job validationJob = new Job("SCL input validation") { //$NON-NLS-1$
370
371         @Override
372         protected IStatus run(IProgressMonitor monitor) {
373             String text = validatedText;
374             asyncSetErrorAnnotations(text, validate(text));
375             return Status.OK_STATUS;
376         }
377         
378     };
379     
380     Job preValidationJob = new Job("SCL input validation") { //$NON-NLS-1$
381         @Override
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();
389                     }
390                 });
391             }
392             
393             return Status.OK_STATUS;
394         }
395     };
396     
397     private void asyncValidate() {
398         if(!input.getText().equals(errorAnnotationsForCommand)) {
399             preValidationJob.cancel();
400             preValidationJob.setPriority(Job.BUILD);
401             preValidationJob.schedule(500); 
402         }
403     }
404     
405     private static int rowCount(String text) {
406         int rowCount = 1;
407         for(int i=0;i<text.length();++i)
408             if(text.charAt(i)=='\n')
409                 ++rowCount;
410         return rowCount;
411     }
412     
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));
419         }
420     }
421     
422     private void setInputHeight(int inputHeight) {
423         sash.setLayoutData( formData(new Tuple2(100, -inputHeight), null, 0, 100, SASH_HEIGHT) );
424         AbstractCommandConsole.this.layout(true);
425     }
426
427     private StringBuilder outputBuffer = new StringBuilder();
428     private ArrayList<StyleRange> styleRanges = new ArrayList<StyleRange>();
429     private volatile boolean outputScheduled = false;
430
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);
435         }
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;
442                 String outputText;
443                 StyleRange[] styleRangeArray;
444                 synchronized(outputBuffer) {
445                     outputScheduled = false;
446
447                     outputText = outputBuffer.toString();
448                     outputBuffer = new StringBuilder();
449
450                     styleRangeArray = styleRanges.toArray(new StyleRange[styleRanges.size()]);
451                     styleRanges.clear();
452                 }
453                 int pos = output.getCharCount();
454
455                 outputModiLock = true;
456                 output.replaceTextRange(pos, 0, outputText);
457                 outputModiLock = false;
458
459                 for(StyleRange styleRange : styleRangeArray) {
460                     styleRange.start += pos;
461                     output.setStyleRange(styleRange);
462                 }
463
464                 output.setCaretOffset(output.getCharCount());
465                 output.showSelection();
466             });
467         }
468     }
469
470     private void execute() {
471         String command = input.getText().trim();
472         if(command.isEmpty())
473             return;
474         
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));
481         }
482         commandHistoryPos = commandHistory.size();
483         
484         // Print it into output area
485         //appendOutput("> " + command.replace("\n", "\n  ") + "\n", greenColor, null);
486         input.setText(""); //$NON-NLS-1$
487         
488         // Execute
489         execute(command);
490     }
491     
492     public static final  ErrorAnnotation[] EMPTY_ANNOTATION_ARRAY = new ErrorAnnotation[0]; 
493     
494     String errorAnnotationsForCommand;
495     ErrorAnnotation[] errorAnnotations = EMPTY_ANNOTATION_ARRAY;
496     
497     private void syncSetErrorAnnotations(String forCommand, ErrorAnnotation[] annotations) {
498         errorAnnotationsForCommand = forCommand;
499         errorAnnotations = annotations;
500
501         {
502             StyleRange clearRange = new StyleRange(0, forCommand.length(), null, null);
503             input.setStyleRange(clearRange);
504         }
505         
506         for(int i=0;i<annotations.length;++i) {
507             ErrorAnnotation annotation = annotations[i];
508             StyleRange range = new StyleRange(
509                     annotation.start,
510                     annotation.end-annotation.start,
511                     null,
512                     null
513                     );
514             range.underline = true;
515             range.underlineColor = redColor;
516             range.underlineStyle = SWT.UNDERLINE_SQUIGGLE;
517             try {
518                 input.setStyleRange(range);
519             } catch(IllegalArgumentException e) {
520                 range.start = 0;
521                 range.length = 1;
522                 input.setStyleRange(range);
523                 getLogger().error("The following error message didn't have a proper location: {}", annotation.description, e); //$NON-NLS-1$
524             }
525         }
526     }
527     
528     private void asyncSetErrorAnnotations(final String forCommand, final ErrorAnnotation[] annotations) {
529         if(input.isDisposed())
530             return;
531         input.getDisplay().asyncExec(() -> {
532             if(input.isDisposed())
533                 return;
534             if(!input.getText().equals(forCommand))
535                 return;
536             syncSetErrorAnnotations(forCommand, annotations);
537         });
538     }
539     
540     private boolean readPreferences() {
541         
542         IPreferenceStore store = new ScopedPreferenceStore(InstanceScope.INSTANCE, PLUGIN_ID);
543
544         String commandHistoryPref = store.getString(Preferences.COMMAND_HISTORY);
545         Deque<String> recentImportPaths = Preferences.decodePaths(commandHistoryPref);
546         
547         commandHistory = new ArrayList<String>(recentImportPaths);
548         commandHistoryPos = commandHistory.size();
549
550         return true;
551     }
552
553     private void writePreferences() throws IOException {
554         
555         IPersistentPreferenceStore store = new ScopedPreferenceStore(InstanceScope.INSTANCE, PLUGIN_ID);
556
557         store.putValue(Preferences.COMMAND_HISTORY, Preferences.encodePaths(commandHistory));
558
559         if (store.needsSaving())
560             store.save();
561         
562     }
563     
564     public abstract void execute(String command);
565     public abstract ErrorAnnotation[] validate(String command);
566     
567     public void clear() {
568         outputModiLock = true;
569         output.setText(""); //$NON-NLS-1$
570         outputModiLock = false;
571     }
572
573     public StyledText getOutputWidget() {
574         return output;
575     }
576
577     IPropertyChangeListener fontRegistryListener = new IPropertyChangeListener() {
578         @Override
579         public void propertyChange(PropertyChangeEvent event) {
580             setTextFont( FontDescriptor.createFrom((FontData[]) event.getNewValue()) );
581         }
582     };
583
584     private void setTextFont(FontDescriptor font) {
585         FontDescriptor oldFontDesc = textFontDescriptor;
586         textFont = resourceManager.createFont(font);
587         textFontDescriptor = font;
588         applyTextFont(textFont);
589
590         // Only destroy old font after the new font has been set!
591         if (oldFontDesc != null)
592             resourceManager.destroyFont(oldFontDesc);
593     }
594
595     private void applyTextFont(Font font) {
596         if (output != null)
597             output.setFont(font);
598         if (deco != null)
599             deco.setFont(font);
600         if (input != null) {
601             input.setFont(font);
602             adjustInputSize(input.getText());
603         }
604     }
605
606     public abstract Logger getLogger();
607 }