1 package org.simantics.scl.ui.console;
3 import java.io.IOException;
4 import java.util.ArrayList;
5 import java.util.Deque;
7 import org.eclipse.core.runtime.IProgressMonitor;
8 import org.eclipse.core.runtime.IStatus;
9 import org.eclipse.core.runtime.Status;
10 import org.eclipse.core.runtime.jobs.Job;
11 import org.eclipse.core.runtime.preferences.InstanceScope;
12 import org.eclipse.jface.preference.IPersistentPreferenceStore;
13 import org.eclipse.jface.preference.IPreferenceStore;
14 import org.eclipse.jface.resource.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;
43 * A console with input and output area that can be embedded
44 * into any editor or view.
45 * @author Hannu Niemistö
47 public abstract class AbstractCommandConsole extends Composite {
49 public static final String PLUGIN_ID = "org.simantics.scl.ui";
51 public static final int COMMAND_HISTORY_SIZE = 50;
53 public static final int SASH_HEIGHT = 3;
55 LocalResourceManager resourceManager;
59 protected StyledText input;
61 int userInputHeight=0;
64 protected Color greenColor;
65 protected Color redColor;
67 ArrayList<String> commandHistory = new ArrayList<String>();
68 int commandHistoryPos = 0;
70 boolean outputModiLock = false;
77 public AbstractCommandConsole(Composite parent, int style) {
83 public boolean setFocus() {
84 return input.setFocus();
87 protected boolean canExecuteCommand() {
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));
96 setLayout(new FormLayout());
98 Font textFont = new Font(getDisplay(),"Courier New",12,SWT.NONE);
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;
107 userInputHeight = max-e.y;
109 int actualInputHeight = Math.max(userInputHeight, minInputHeight);
110 sash.setBounds(e.x, max-actualInputHeight, e.width, e.height);
111 setInputHeight(actualInputHeight);
116 output = new StyledText(this, SWT.MULTI /*| SWT.READ_ONLY*/ | SWT.V_SCROLL | SWT.H_SCROLL);
117 output.setFont(textFont);
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);
126 output.addVerifyListener(new VerifyListener() {
128 public void verifyText(VerifyEvent e) {
131 input.append(e.text);
133 input.setCaretOffset(input.getText().length());
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;
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);
155 input = new StyledText(this, SWT.MULTI);
156 input.setFont(textFont);
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);
166 input.addVerifyKeyListener(new VerifyKeyListener() {
169 public void verifyKey(VerifyEvent event) {
170 switch(event.keyCode) {
173 if((event.stateMask & SWT.CTRL) == 0) {
174 if(canExecuteCommand())
181 if((event.stateMask & SWT.CTRL) != 0) {
182 int targetHistoryPos = commandHistoryPos;
183 if(event.keyCode == SWT.ARROW_UP) {
184 if(commandHistoryPos <= 0)
189 if(commandHistoryPos >= commandHistory.size()-1)
193 setInputText(commandHistory.get(targetHistoryPos));
194 commandHistoryPos = targetHistoryPos;
200 // commandHistoryPos = commandHistory.size();
206 input.addVerifyListener(new VerifyListener() {
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);
214 lineOffset+indentAmount < input.getCharCount() &&
215 input.getTextRange(lineOffset+indentAmount, 1).equals(" ");
217 StringBuilder indent = new StringBuilder();
219 for(int i=0;i<indentAmount;++i)
221 e.text = e.text.replace("\n", indent);
225 input.addModifyListener(new ModifyListener() {
227 public void modifyText(ModifyEvent e) {
228 adjustInputSize(input.getText());
229 commandHistoryPos = commandHistory.size();
233 Listener hoverListener = new Listener() {
235 DefaultToolTip toolTip = new DefaultToolTip(input, ToolTip.RECREATE, true);
238 boolean toolTipVisible = false;
241 public void handleEvent(Event e) {
243 case SWT.MouseHover: {
244 int offset = getOffsetInInput(e.x, e.y);
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);
259 description.append('\n');
260 description.append(annotation.description);
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;
274 int offset = getOffsetInInput(e.x, e.y);
275 if(offset < min || offset >= max) {
277 toolTipVisible = false;
285 toolTipVisible = false;
291 input.addListener(SWT.MouseHover, hoverListener);
292 input.addListener(SWT.MouseMove, hoverListener);
293 input.addListener(SWT.MouseExit, hoverListener);
297 addListener(SWT.Dispose, new Listener() {
300 public void handleEvent(Event event) {
303 } catch (IOException e) {
312 private int getOffsetInInput(int x, int y) {
315 offset = input.getOffsetAtLocation(new Point(x, y));
316 } catch(IllegalArgumentException e) {
319 if(offset == input.getText().length())
321 else if(offset > 0) {
322 Rectangle rect = input.getTextBounds(offset, offset);
323 if(!rect.contains(x, y))
329 public void setInputText(String text) {
331 input.setCaretOffset(text.length());
332 adjustInputSize(text);
335 String validatedText;
337 Job validationJob = new Job("SCL input validation") {
340 protected IStatus run(IProgressMonitor monitor) {
341 String text = validatedText;
342 asyncSetErrorAnnotations(text, validate(text));
343 return Status.OK_STATUS;
348 Job preValidationJob = new Job("SCL input validation") {
350 protected IStatus run(IProgressMonitor monitor) {
351 if(!input.isDisposed()) {
352 input.getDisplay().asyncExec(new Runnable() {
355 if(!input.isDisposed()) {
356 validatedText = input.getText();
357 validationJob.setPriority(Job.BUILD);
358 validationJob.schedule();
364 return Status.OK_STATUS;
368 private void asyncValidate() {
369 if(!input.getText().equals(errorAnnotationsForCommand)) {
370 preValidationJob.cancel();
371 preValidationJob.setPriority(Job.BUILD);
372 preValidationJob.schedule(500);
376 private static int rowCount(String text) {
378 for(int i=0;i<text.length();++i)
379 if(text.charAt(i)=='\n')
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));
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);
403 public void appendOutput(final String text, final Color foreground, final Color background) {
404 final Display display = Display.getDefault();
405 if(display.isDisposed()) return;
406 display.asyncExec(new Runnable() {
409 if(output.isDisposed()) return;
410 int pos = output.getCharCount();
411 outputModiLock = true;
412 output.replaceTextRange(pos, 0, text);
413 outputModiLock = false;
414 output.setStyleRange(new StyleRange(pos, text.length(),
415 foreground, background));
416 output.setCaretOffset(output.getCharCount());
417 output.showSelection();
423 private void execute() {
424 String command = input.getText().trim();
425 if(command.isEmpty())
428 // Add command to command history
429 if(commandHistory.isEmpty() || !commandHistory.get(commandHistory.size()-1).equals(command)) {
430 commandHistory.add(command);
431 if(commandHistory.size() > COMMAND_HISTORY_SIZE*2)
432 commandHistory = new ArrayList<String>(
433 commandHistory.subList(COMMAND_HISTORY_SIZE, COMMAND_HISTORY_SIZE*2));
435 commandHistoryPos = commandHistory.size();
437 // Print it into output area
438 //appendOutput("> " + command.replace("\n", "\n ") + "\n", greenColor, null);
445 public static final ErrorAnnotation[] EMPTY_ANNOTATION_ARRAY = new ErrorAnnotation[0];
447 String errorAnnotationsForCommand;
448 ErrorAnnotation[] errorAnnotations = EMPTY_ANNOTATION_ARRAY;
450 private void syncSetErrorAnnotations(String forCommand, ErrorAnnotation[] annotations) {
451 errorAnnotationsForCommand = forCommand;
452 errorAnnotations = annotations;
455 StyleRange clearRange = new StyleRange(0, forCommand.length(), null, null);
456 input.setStyleRange(clearRange);
459 for(int i=0;i<annotations.length;++i) {
460 ErrorAnnotation annotation = annotations[i];
461 StyleRange range = new StyleRange(
463 annotation.end-annotation.start,
467 range.underline = true;
468 range.underlineColor = redColor;
469 range.underlineStyle = SWT.UNDERLINE_SQUIGGLE;
471 input.setStyleRange(range);
472 } catch(IllegalArgumentException e) {
475 input.setStyleRange(range);
476 System.err.println("The following error message didn't have a proper location:");
477 System.err.println(annotation.description);
482 private void asyncSetErrorAnnotations(final String forCommand, final ErrorAnnotation[] annotations) {
483 if(input.isDisposed())
485 input.getDisplay().asyncExec(new Runnable() {
488 if(input.isDisposed())
490 if(!input.getText().equals(forCommand))
492 syncSetErrorAnnotations(forCommand, annotations);
497 private boolean readPreferences() {
499 IPreferenceStore store = new ScopedPreferenceStore(InstanceScope.INSTANCE, PLUGIN_ID);
501 String commandHistoryPref = store.getString(Preferences.COMMAND_HISTORY);
502 Deque<String> recentImportPaths = Preferences.decodePaths(commandHistoryPref);
504 commandHistory = new ArrayList<String>(recentImportPaths);
505 commandHistoryPos = commandHistory.size();
510 private void writePreferences() throws IOException {
512 IPersistentPreferenceStore store = new ScopedPreferenceStore(InstanceScope.INSTANCE, PLUGIN_ID);
514 store.putValue(Preferences.COMMAND_HISTORY, Preferences.encodePaths(commandHistory));
516 if (store.needsSaving())
521 public abstract void execute(String command);
522 public abstract ErrorAnnotation[] validate(String command);
524 public void clear() {
525 outputModiLock = true;
527 outputModiLock = false;
530 public StyledText getOutputWidget() {