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