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