]> gerrit.simantics Code Review - simantics/platform.git/blobdiff - bundles/org.simantics.scl.ui/src/org/simantics/scl/ui/console/AbstractCommandConsole.java
Migrated source code from Simantics SVN
[simantics/platform.git] / bundles / org.simantics.scl.ui / src / org / simantics / scl / ui / console / AbstractCommandConsole.java
diff --git a/bundles/org.simantics.scl.ui/src/org/simantics/scl/ui/console/AbstractCommandConsole.java b/bundles/org.simantics.scl.ui/src/org/simantics/scl/ui/console/AbstractCommandConsole.java
new file mode 100755 (executable)
index 0000000..b4260b4
--- /dev/null
@@ -0,0 +1,532 @@
+package org.simantics.scl.ui.console;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Deque;
+
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.core.runtime.jobs.Job;
+import org.eclipse.core.runtime.preferences.InstanceScope;
+import org.eclipse.jface.preference.IPersistentPreferenceStore;
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.jface.resource.JFaceResources;
+import org.eclipse.jface.resource.LocalResourceManager;
+import org.eclipse.jface.window.DefaultToolTip;
+import org.eclipse.jface.window.ToolTip;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.StyleRange;
+import org.eclipse.swt.custom.StyledText;
+import org.eclipse.swt.custom.VerifyKeyListener;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.VerifyEvent;
+import org.eclipse.swt.events.VerifyListener;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.RGB;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.FormAttachment;
+import org.eclipse.swt.layout.FormData;
+import org.eclipse.swt.layout.FormLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.Sash;
+import org.eclipse.ui.preferences.ScopedPreferenceStore;
+
+/**
+ * A console with input and output area that can be embedded
+ * into any editor or view.
+ * @author Hannu Niemistö
+ */
+public abstract class AbstractCommandConsole extends Composite {
+
+    public static final String PLUGIN_ID = "org.simantics.scl.ui";
+
+    public static final int COMMAND_HISTORY_SIZE = 50;
+    
+    public static final int SASH_HEIGHT = 3;
+    
+    LocalResourceManager resourceManager;
+    
+    StyledText output;
+    Sash sash;
+    protected StyledText input;
+    
+    int userInputHeight=0;
+    int minInputHeight=0;
+    
+    protected Color greenColor;
+    protected Color redColor;
+    
+    ArrayList<String> commandHistory = new ArrayList<String>();
+    int commandHistoryPos = 0;
+    
+    boolean outputModiLock = false;
+    
+    /*
+    Shell tip = null;
+    Label label = null;
+    */
+    
+    public AbstractCommandConsole(Composite parent, int style) {
+        super(parent, style);      
+        createControl();
+    }
+    
+    @Override
+    public boolean setFocus() {
+        return input.setFocus();
+    }
+    
+    protected boolean canExecuteCommand() {
+        return true;
+    }
+
+    private void createControl() {
+        resourceManager = new LocalResourceManager(JFaceResources.getResources(), this);
+        greenColor = resourceManager.createColor(new RGB(0, 128, 0));
+        redColor = resourceManager.createColor(new RGB(172, 0, 0));
+        
+        setLayout(new FormLayout());
+        
+        Font textFont = new Font(getDisplay(),"Courier New",12,SWT.NONE);
+
+        // Sash
+        sash = new Sash(this, /*SWT.BORDER |*/ SWT.HORIZONTAL);
+        sash.addListener(SWT.Selection, new Listener () {
+            public void handleEvent(Event e) {
+                Rectangle bounds = AbstractCommandConsole.this.getBounds();
+                int max = bounds.y + bounds.height;
+
+                userInputHeight = max-e.y;
+                
+                int actualInputHeight = Math.max(userInputHeight, minInputHeight);
+                sash.setBounds(e.x, max-actualInputHeight, e.width, e.height);
+                setInputHeight(actualInputHeight);
+            }
+        });
+
+        // Upper
+        output = new StyledText(this, SWT.MULTI /*| SWT.READ_ONLY*/ | SWT.V_SCROLL | SWT.H_SCROLL);
+        output.setFont(textFont);
+        {
+            FormData formData = new FormData();
+            formData.top = new FormAttachment(0);      
+            formData.bottom = new FormAttachment(sash);
+            formData.left = new FormAttachment(0);
+            formData.right = new FormAttachment(100);
+            output.setLayoutData(formData);
+        }
+        output.addVerifyListener(new VerifyListener() {
+            @Override
+            public void verifyText(VerifyEvent e) {
+                if(outputModiLock)
+                    return;
+                input.append(e.text);
+                input.setFocus();
+                input.setCaretOffset(input.getText().length());
+                e.doit = false;
+            }
+        });
+
+        // Deco
+        StyledText deco = new StyledText(this, SWT.MULTI | SWT.READ_ONLY);   
+        deco.setFont(textFont);
+        deco.setEnabled(false);
+        GC gc = new GC(deco);
+        int inputLeftPos = gc.getFontMetrics().getAverageCharWidth()*2;
+        deco.setText(">");
+        {
+            FormData formData = new FormData();
+            formData.top = new FormAttachment(sash);
+            formData.bottom = new FormAttachment(100);
+            formData.left = new FormAttachment(0);
+            formData.right = new FormAttachment(0, inputLeftPos);
+            deco.setLayoutData(formData);
+        }
+        
+        // Input area        
+        input = new StyledText(this, SWT.MULTI);        
+        input.setFont(textFont);
+        {
+            FormData formData = new FormData();
+            formData.top = new FormAttachment(sash);
+            formData.bottom = new FormAttachment(100);
+            formData.left = new FormAttachment(0, inputLeftPos);
+            formData.right = new FormAttachment(100);
+            input.setLayoutData(formData);
+        }
+        adjustInputSize("");
+        input.addVerifyKeyListener(new VerifyKeyListener() {
+            
+            @Override
+            public void verifyKey(VerifyEvent event) {
+                switch(event.keyCode) {
+                case SWT.KEYPAD_CR:
+                case SWT.CR:
+                    if((event.stateMask & SWT.CTRL) == 0) {
+                        if(canExecuteCommand())
+                            execute();
+                        event.doit = false;
+                    }
+                    break;
+                case SWT.ARROW_UP:
+                case SWT.ARROW_DOWN: 
+                    if((event.stateMask & SWT.CTRL) != 0) {
+                        int targetHistoryPos = commandHistoryPos;
+                        if(event.keyCode == SWT.ARROW_UP) {
+                            if(commandHistoryPos <= 0)
+                                return;
+                            --targetHistoryPos;
+                        }
+                        else {
+                            if(commandHistoryPos >= commandHistory.size()-1)
+                                return;
+                            ++targetHistoryPos;
+                        }
+                        setInputText(commandHistory.get(targetHistoryPos));
+                        commandHistoryPos = targetHistoryPos;
+                        event.doit = false;
+                    }
+                    break;                
+//                case SWT.ESC:
+//                    setInputText("");
+//                    commandHistoryPos = commandHistory.size();
+//                    break;
+                }
+            }
+            
+        });        
+        input.addVerifyListener(new VerifyListener() {
+            @Override
+            public void verifyText(VerifyEvent e) {
+                if(e.text.contains("\n")) {
+                    int lineId = input.getLineAtOffset(e.start);
+                    int lineOffset = input.getOffsetAtLine(lineId);
+                    int indentAmount;
+                    for(indentAmount=0;
+                            lineOffset+indentAmount < input.getCharCount() && 
+                            input.getTextRange(lineOffset+indentAmount, 1).equals(" ");
+                            ++indentAmount);
+                    StringBuilder indent = new StringBuilder();
+                    indent.append('\n');
+                    for(int i=0;i<indentAmount;++i)
+                        indent.append(' ');
+                    e.text = e.text.replace("\n", indent);
+                }
+            }
+        });
+        input.addModifyListener(new ModifyListener() {
+            @Override
+            public void modifyText(ModifyEvent e) {
+                adjustInputSize(input.getText());
+                commandHistoryPos = commandHistory.size();
+                asyncValidate();
+            }
+        });
+        Listener hoverListener = new Listener() {
+            
+            DefaultToolTip toolTip = new DefaultToolTip(input, ToolTip.RECREATE, true);
+            
+            int min, max;
+            boolean toolTipVisible = false;
+            
+            @Override
+            public void handleEvent(Event e) {
+                switch(e.type) {
+                case SWT.MouseHover: {
+                    int offset = getOffsetInInput(e.x, e.y);
+                    if(offset == -1)
+                        return;
+                    
+                    min = Integer.MIN_VALUE;
+                    max = Integer.MAX_VALUE;
+                    StringBuilder description = new StringBuilder();
+                    boolean first = true;
+                    for(ErrorAnnotation annotation : errorAnnotations) {
+                        if(annotation.start <= offset && annotation.end > offset) {
+                            min = Math.max(min, annotation.start);
+                            max = Math.max(min, annotation.end);
+                            if(first)
+                                first = false;
+                            else
+                                description.append('\n');
+                            description.append(annotation.description);
+                        }
+                    }
+                    
+                    if(min != Integer.MIN_VALUE) {
+                        Rectangle bounds = input.getTextBounds(min, max-1);
+                        toolTip.setText(description.toString());
+                        toolTip.show(new Point(bounds.x, bounds.y+bounds.height));
+                        toolTipVisible = true;
+                    }
+                    return;
+                }
+                case SWT.MouseMove:
+                    if(toolTipVisible) {
+                        int offset = getOffsetInInput(e.x, e.y);
+                        if(offset < min || offset >= max) {
+                            toolTip.hide();
+                            toolTipVisible = false;
+                            return;
+                        }
+                    }
+                    return;
+                case SWT.MouseExit:
+                    if(toolTipVisible) {
+                        toolTip.hide();
+                        toolTipVisible = false;
+                    }
+                    return;
+                }                
+            }            
+        };
+        input.addListener(SWT.MouseHover, hoverListener);
+        input.addListener(SWT.MouseMove, hoverListener);
+        input.addListener(SWT.MouseExit, hoverListener);
+        
+        readPreferences();
+        
+        addListener(SWT.Dispose, new Listener() {
+            
+            @Override
+            public void handleEvent(Event event) {
+                try {
+                    writePreferences();
+                } catch (IOException e) {
+                    e.printStackTrace();
+               }
+            }
+            
+        });
+        
+    }
+    
+    private int getOffsetInInput(int x, int y) {
+        int offset;
+        try {
+            offset = input.getOffsetAtLocation(new Point(x, y));
+        } catch(IllegalArgumentException e) {
+            return -1;
+        }
+        if(offset == input.getText().length())
+            --offset;
+        else if(offset > 0) {
+            Rectangle rect = input.getTextBounds(offset, offset);
+            if(!rect.contains(x, y))
+                --offset;
+        }
+        return offset;
+    }
+    
+    public void setInputText(String text) {
+        input.setText(text);
+        input.setCaretOffset(text.length());
+        adjustInputSize(text);
+    }
+    
+    String validatedText;
+    
+    Job validationJob = new Job("SCL input validation") {
+
+        @Override
+        protected IStatus run(IProgressMonitor monitor) {
+            String text = validatedText;
+            asyncSetErrorAnnotations(text, validate(text));
+            return Status.OK_STATUS;
+        }
+        
+    };
+    
+    Job preValidationJob = new Job("SCL input validation") {        
+        @Override
+        protected IStatus run(IProgressMonitor monitor) {
+            if(!input.isDisposed()) {
+                input.getDisplay().asyncExec(new Runnable() {
+                    @Override
+                    public void run() {
+                        if(!input.isDisposed()) {
+                            validatedText = input.getText();
+                            validationJob.setPriority(Job.BUILD);
+                            validationJob.schedule();
+                        }                     
+                    }                    
+                });                
+            }
+            
+            return Status.OK_STATUS;
+        }
+    };
+    
+    private void asyncValidate() {
+        if(!input.getText().equals(errorAnnotationsForCommand)) {
+            preValidationJob.cancel();
+            preValidationJob.setPriority(Job.BUILD);
+            preValidationJob.schedule(500); 
+        }
+    }
+    
+    private static int rowCount(String text) {
+        int rowCount = 1;
+        for(int i=0;i<text.length();++i)
+            if(text.charAt(i)=='\n')
+                ++rowCount;
+        return rowCount;
+    }
+    
+    private void adjustInputSize(String text) {
+        int lineHeight = input.getLineHeight();
+        int height = rowCount(text)*lineHeight+SASH_HEIGHT;
+        if(height != minInputHeight) {
+            minInputHeight = height;
+            setInputHeight(Math.max(minInputHeight, userInputHeight));
+        }
+    }
+    
+    private void setInputHeight(int inputHeight) {
+        FormData formData = new FormData();
+        formData.top = new FormAttachment(100, -inputHeight);
+        formData.left = new FormAttachment(0);
+        formData.right = new FormAttachment(100);
+        formData.height = SASH_HEIGHT;
+        sash.setLayoutData(formData);
+        AbstractCommandConsole.this.layout(true);
+    }
+
+    public void appendOutput(final String text, final Color foreground, final Color background) {
+        final Display display = Display.getDefault();
+        if(display.isDisposed()) return;
+        display.asyncExec(new Runnable() {
+            @Override
+            public void run() {
+                if(output.isDisposed()) return;
+                int pos = output.getCharCount();
+                outputModiLock = true;
+                output.replaceTextRange(pos, 0, text);
+                outputModiLock = false;
+                output.setStyleRange(new StyleRange(pos, text.length(), 
+                        foreground, background));
+                output.setCaretOffset(output.getCharCount());
+                output.showSelection();
+            }
+
+        });
+    }
+    
+    private void execute() {        
+        String command = input.getText().trim();
+        if(command.isEmpty())
+            return;
+        
+        // Add command to command history
+        commandHistory.add(command);
+        if(commandHistory.size() > COMMAND_HISTORY_SIZE*2)
+            commandHistory = new ArrayList<String>(
+                    commandHistory.subList(COMMAND_HISTORY_SIZE, COMMAND_HISTORY_SIZE*2));
+        commandHistoryPos = commandHistory.size();
+        
+        // Print it into output area
+        //appendOutput("> " + command.replace("\n", "\n  ") + "\n", greenColor, null);
+        input.setText("");
+        
+        // Execute
+        execute(command);
+    }
+    
+    public static final  ErrorAnnotation[] EMPTY_ANNOTATION_ARRAY = new ErrorAnnotation[0]; 
+    
+    String errorAnnotationsForCommand;
+    ErrorAnnotation[] errorAnnotations = EMPTY_ANNOTATION_ARRAY;
+    
+    private void syncSetErrorAnnotations(String forCommand, ErrorAnnotation[] annotations) {
+        errorAnnotationsForCommand = forCommand;
+        errorAnnotations = annotations;
+
+        {
+            StyleRange clearRange = new StyleRange(0, forCommand.length(), null, null);
+            input.setStyleRange(clearRange);
+        }
+        
+        for(int i=0;i<annotations.length;++i) {
+            ErrorAnnotation annotation = annotations[i];
+            StyleRange range = new StyleRange(
+                    annotation.start,
+                    annotation.end-annotation.start,
+                    null,
+                    null
+                    );
+            range.underline = true;
+            range.underlineColor = redColor;
+            range.underlineStyle = SWT.UNDERLINE_SQUIGGLE;
+            try {
+                input.setStyleRange(range);
+            } catch(IllegalArgumentException e) {
+                range.start = 0;
+                range.length = 1;
+                input.setStyleRange(range);
+                System.err.println("The following error message didn't have a proper location:");
+                System.err.println(annotation.description);
+            }
+        }
+    }
+    
+    private void asyncSetErrorAnnotations(final String forCommand, final ErrorAnnotation[] annotations) {
+        if(input.isDisposed())
+            return;
+        input.getDisplay().asyncExec(new Runnable() {
+            @Override
+            public void run() {
+                if(input.isDisposed())
+                    return;
+                if(!input.getText().equals(forCommand))
+                    return;                
+                syncSetErrorAnnotations(forCommand, annotations);
+            }
+        });
+    }
+    
+    private boolean readPreferences() {
+        
+        IPreferenceStore store = new ScopedPreferenceStore(InstanceScope.INSTANCE, PLUGIN_ID);
+
+        String commandHistoryPref = store.getString(Preferences.COMMAND_HISTORY);
+        Deque<String> recentImportPaths = Preferences.decodePaths(commandHistoryPref);
+        
+        commandHistory = new ArrayList<String>(recentImportPaths);
+        commandHistoryPos = commandHistory.size();
+
+        return true;
+    }
+
+    private void writePreferences() throws IOException {
+        
+        IPersistentPreferenceStore store = new ScopedPreferenceStore(InstanceScope.INSTANCE, PLUGIN_ID);
+
+        store.putValue(Preferences.COMMAND_HISTORY, Preferences.encodePaths(commandHistory));
+
+        if (store.needsSaving())
+            store.save();
+        
+    }
+    
+    public abstract void execute(String command);
+    public abstract ErrorAnnotation[] validate(String command);
+    
+    public void clear() {
+        outputModiLock = true;
+        output.setText("");
+        outputModiLock = false;
+    }
+
+    public StyledText getOutputWidget() {
+        return output;
+    }
+
+}