]> gerrit.simantics Code Review - simantics/platform.git/blobdiff - bundles/org.simantics.scl.ui/src/org/simantics/scl/ui/browser/SCLDocumentationBrowser.java
Migrated source code from Simantics SVN
[simantics/platform.git] / bundles / org.simantics.scl.ui / src / org / simantics / scl / ui / browser / SCLDocumentationBrowser.java
diff --git a/bundles/org.simantics.scl.ui/src/org/simantics/scl/ui/browser/SCLDocumentationBrowser.java b/bundles/org.simantics.scl.ui/src/org/simantics/scl/ui/browser/SCLDocumentationBrowser.java
new file mode 100644 (file)
index 0000000..fe871ab
--- /dev/null
@@ -0,0 +1,422 @@
+package org.simantics.scl.ui.browser;
+
+import gnu.trove.map.hash.THashMap;
+
+import java.io.IOException;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+
+import org.eclipse.core.runtime.Status;
+import org.eclipse.jface.dialogs.ErrorDialog;
+import org.eclipse.jface.layout.GridDataFactory;
+import org.eclipse.jface.layout.GridLayoutFactory;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.browser.Browser;
+import org.eclipse.swt.browser.LocationAdapter;
+import org.eclipse.swt.browser.LocationEvent;
+import org.eclipse.swt.browser.ProgressAdapter;
+import org.eclipse.swt.browser.ProgressEvent;
+import org.eclipse.swt.custom.SashForm;
+import org.eclipse.swt.events.KeyAdapter;
+import org.eclipse.swt.events.KeyEvent;
+import org.eclipse.swt.events.KeyListener;
+import org.eclipse.swt.events.MouseEvent;
+import org.eclipse.swt.events.MouseWheelListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.TraverseEvent;
+import org.eclipse.swt.events.TraverseListener;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.DirectoryDialog;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Listener;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.swt.widgets.Tree;
+import org.eclipse.swt.widgets.TreeItem;
+import org.simantics.scl.compiler.elaboration.modules.SCLValue;
+import org.simantics.scl.compiler.markdown.html.GenerateAllHtmlDocumentation;
+import org.simantics.scl.compiler.markdown.html.HierarchicalDocumentationRef;
+import org.simantics.scl.compiler.markdown.html.HtmlDocumentationGeneration;
+import org.simantics.scl.compiler.markdown.internal.HtmlEscape;
+import org.simantics.scl.osgi.SCLOsgi;
+import org.simantics.scl.ui.Activator;
+
+public class SCLDocumentationBrowser {
+    
+    public static final String STANDARD_LIBRARY = "StandardLibrary";
+
+    Browser browser;
+    Button backButton;
+    Text pageName;
+    Button refreshButton;
+    Button forwardButton;
+    String currentPageName = "";
+    Button saveButton;
+    Button findButton;
+    Tree navigationTree;
+    
+    ArrayList<String> locationHistory = new ArrayList<String>();
+    int locationHistoryPosition = -1;
+    
+    ArrayList<Runnable> runWhenCompleted = new ArrayList<Runnable>(2);
+    Object runWhenCompletedLock = new Object();
+    
+    private void executeWhenCompleted(final String script) {
+        synchronized (runWhenCompletedLock) {
+            runWhenCompleted.add(new Runnable() {
+                @Override
+                public void run() {
+                    browser.execute(script);
+                }
+            });
+        }
+    }
+    
+    private void newLocation(String location) {
+        if(locationHistoryPosition < 0 || 
+                !location.equals(locationHistory.get(locationHistoryPosition))) {
+            ++locationHistoryPosition;
+            while(locationHistory.size() > locationHistoryPosition)
+                locationHistory.remove(locationHistory.size()-1);
+            locationHistory.add(location);
+            updateButtons();
+        }
+    }
+    
+    private void back() {
+        if(locationHistoryPosition > 0) {
+            browser.setUrl(locationHistory.get(--locationHistoryPosition));
+            updateButtons();
+        }
+    }
+    
+    private void refresh() {
+        SCLOsgi.SOURCE_REPOSITORY.checkUpdates();
+        final Object yOffset = browser.evaluate("return window.pageYOffset !== undefined ? window.pageYOffset : ((document.compatMode || \"\") === \"CSS1Compat\") ? document.documentElement.scrollTop : document.body.scrollTop;");
+        if(yOffset != null)
+            executeWhenCompleted("window.scroll(0,"+yOffset+");");
+        browser.setUrl(locationHistory.get(locationHistoryPosition));
+        updateNavigationTree();
+    }
+    
+    private void forward() {
+        if(locationHistoryPosition < locationHistory.size()-1) {
+            browser.setUrl(locationHistory.get(++locationHistoryPosition));
+            updateButtons();
+        }
+    }
+    
+    private void updateButtons() {
+        backButton.setEnabled(locationHistoryPosition > 0);
+        forwardButton.setEnabled(locationHistoryPosition < locationHistory.size()-1);
+    }
+    
+    private void setCurrentLocation(String location) {
+        pageName.setText(location);
+        currentPageName = location;
+    }
+    
+    public SCLDocumentationBrowser(Composite parent) {
+        Color white = parent.getDisplay().getSystemColor(SWT.COLOR_WHITE);
+        
+        final Composite composite = new Composite(parent, SWT.NONE);
+        GridLayoutFactory.fillDefaults().spacing(0, 0).applyTo(composite);
+        composite.setBackground(white);
+        
+        Composite buttons = new Composite(composite, SWT.NONE);
+        buttons.setBackground(white);
+        GridDataFactory.fillDefaults().grab(true, false).align(SWT.FILL, SWT.CENTER).applyTo(buttons);
+        GridLayoutFactory.fillDefaults().numColumns(6).margins(3, 3).applyTo(buttons);
+        
+        backButton = new Button(buttons, SWT.PUSH);
+        buttons.setBackground(composite.getDisplay().getSystemColor(SWT.COLOR_GRAY));
+        backButton.setToolTipText("Back (Alt-Left)");
+        backButton.setEnabled(false);
+        backButton.setImage(Activator.getInstance().getImageRegistry().get("arrow_left"));
+        
+        forwardButton = new Button(buttons, SWT.PUSH);
+        forwardButton.setToolTipText("Forward (Alt-Right)");
+        forwardButton.setEnabled(false);
+        forwardButton.setImage(Activator.getInstance().getImageRegistry().get("arrow_right"));
+        
+        refreshButton = new Button(buttons, SWT.PUSH);
+        refreshButton.setToolTipText("Refresh page (Ctrl-R)");
+        refreshButton.setImage(Activator.getInstance().getImageRegistry().get("arrow_refresh"));
+        
+        pageName = new Text(buttons, SWT.BORDER | SWT.SINGLE);
+        GridDataFactory.fillDefaults().grab(true, false).align(SWT.FILL, SWT.CENTER).applyTo(pageName);
+
+        findButton = new Button(buttons, SWT.PUSH);
+        findButton.setToolTipText("Find SCL definitions (Ctrl-H)");
+        findButton.setImage(Activator.getInstance().getImageRegistry().get("find"));
+
+        saveButton = new Button(buttons, SWT.PUSH);
+        saveButton.setToolTipText("Save documentation to disk");
+        saveButton.setImage(Activator.getInstance().getImageRegistry().get("disk"));
+        
+        SashForm browserBox = new SashForm(composite, SWT.BORDER | SWT.HORIZONTAL);
+        GridDataFactory.fillDefaults().grab(true, true).align(SWT.FILL, SWT.FILL).applyTo(browserBox);
+        browserBox.setLayout(new FillLayout());
+        
+        navigationTree = new Tree(browserBox, SWT.SINGLE);
+        updateNavigationTree();
+        navigationTree.addSelectionListener(new SelectionAdapter() {
+           @Override
+            public void widgetSelected(SelectionEvent e) {
+               TreeItem[] items = navigationTree.getSelection();
+               if(items.length == 1) {
+                   String documentationName = (String)items[0].getData();
+                   if(documentationName != null)
+                       setLocation(documentationName);
+               }
+            } 
+        });
+        
+        browser = new Browser(browserBox, SWT.BORDER);
+        browserBox.setWeights(new int[] {15, 85});
+        browser.addProgressListener(new ProgressAdapter() {
+            @Override
+            public void completed(ProgressEvent event) {
+                ArrayList<Runnable> rs;
+                synchronized(runWhenCompletedLock) {
+                    rs = runWhenCompleted;
+                    runWhenCompleted = new ArrayList<Runnable>(2);
+                }
+                for(Runnable r : rs)
+                    r.run();
+            }
+        });
+        browser.addLocationListener(new LocationAdapter() {
+            public void changing(LocationEvent event) {
+                String location = event.location;
+                if(location.startsWith("about:blank"))
+                    return;
+                newLocation(location);
+                if(location.startsWith("about:")) {
+                    location = location.substring(6);
+                    setCurrentLocation(location);
+                    int hashPos = location.indexOf('#');
+                    final String fragment;
+                    if(hashPos >= 0) {
+                        fragment = location.substring(hashPos);
+                        location = location.substring(0, hashPos);
+                    }
+                    else
+                        fragment = null;
+                    if(location.endsWith(".html"))
+                        location = location.substring(0, location.length()-5);
+                    
+                    String html = HtmlDocumentationGeneration.generate(SCLOsgi.MODULE_REPOSITORY, location, null);
+                    
+                    browser.setText(html);
+                    if(fragment != null)
+                        executeWhenCompleted("location.hash = \"" + fragment + "\";");
+                    event.doit = false;
+                }
+                else
+                    setCurrentLocation(location);
+            }
+        });
+        
+        KeyListener keyListener = new KeyAdapter() {
+            @Override
+            public void keyPressed(KeyEvent e) {
+                if((e.stateMask & SWT.ALT) != 0) {
+                    if(e.keyCode == SWT.ARROW_LEFT) {
+                        back();
+                        return;
+                    }
+                    else if(e.keyCode == SWT.ARROW_RIGHT) {
+                        forward();
+                        return;
+                    }
+                }
+                else if((e.stateMask & SWT.CTRL) != 0) {
+                    if(e.keyCode == 'r' || e.keyCode == 'R') {
+                        refresh();
+                        return;
+                    }
+                    if(e.keyCode == 'l' || e.keyCode == 'L') {
+                        pageName.selectAll();
+                        pageName.setFocus();
+                        return;
+                    }
+                    if(e.keyCode == 'k' || e.keyCode == 'K') {
+                        pageName.setText("?");
+                        pageName.setSelection(1);
+                        pageName.setFocus();
+                        return;
+                    }
+                    if(e.keyCode == 'h' || e.keyCode == 'H') {
+                        find();
+                        return;
+                    }
+                }
+                if(e.keyCode == SWT.PAGE_DOWN) {
+                    browser.execute("window.scrollBy(0,0.8*(window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight));");
+                }
+                else if(e.keyCode == SWT.PAGE_UP) {
+                    browser.execute("window.scrollBy(0,-0.8*(window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight));");
+                }
+                else if(e.keyCode == SWT.ARROW_DOWN) {
+                    browser.execute("window.scrollBy(0,100);");
+                }
+                else if(e.keyCode == SWT.ARROW_UP) {
+                    browser.execute("window.scrollBy(0,-100);");
+                }
+                else if(e.keyCode == SWT.HOME) {
+                    if(e.widget != pageName)
+                        browser.execute("window.scroll(0,0);");
+                }
+                else if(e.keyCode == SWT.END) {
+                    if(e.widget != pageName)
+                        browser.execute("window.scroll(0,document.body.scrollHeight);");
+                }
+            }
+            
+        };
+        composite.addKeyListener(keyListener);
+        browser.addKeyListener(keyListener);
+        pageName.addKeyListener(keyListener);
+        
+        MouseWheelListener wheelListener = new MouseWheelListener() { 
+            @Override
+            public void mouseScrolled(MouseEvent e) {
+                browser.execute("window.scrollBy(0,"+e.count*(-30)+");");
+            }
+        };
+        composite.addMouseWheelListener(wheelListener);
+        browser.addMouseWheelListener(wheelListener);     
+        backButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                back();
+            }
+        });
+        forwardButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                forward();
+            }
+        });
+        refreshButton.addListener(SWT.Selection, new Listener() {
+            @Override
+            public void handleEvent(Event event) {
+                refresh();
+            }
+        });
+        pageName.addTraverseListener(new TraverseListener() {
+            @Override
+            public void keyTraversed(TraverseEvent e) {
+                if(e.detail == SWT.TRAVERSE_RETURN)
+                    setLocation(pageName.getText());
+                else if(e.detail == SWT.TRAVERSE_ESCAPE)
+                    pageName.setText(currentPageName);
+            }
+        });
+        findButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                find();
+            }
+        });
+        saveButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                DirectoryDialog dialog = new DirectoryDialog(saveButton.getShell());
+                dialog.setText("Select the directory for storing the documentation.");
+                dialog.setMessage("Select a directory where the documentation is generated to.");
+                String directory = dialog.open();
+                if(directory != null) {
+                    try {
+                        GenerateAllHtmlDocumentation.generate(SCLOsgi.MODULE_REPOSITORY, Paths.get(directory));
+                    } catch (IOException ex) {
+                        ex.printStackTrace();
+                        ErrorDialog.openError(saveButton.getShell(), "Documentation generation failed", null,
+                                new Status(Status.ERROR, "org.simantics.scl.ui", 0, ex.toString(), ex));
+                    }
+                }
+            }
+        });
+    }
+    
+    private void find() {
+        SCLDefinitionSelectionDialog dialog = new SCLDefinitionSelectionDialog(findButton.getShell());
+        if(dialog.open() == SCLDefinitionSelectionDialog.OK) {
+            SCLValue value = (SCLValue)dialog.getFirstResult();
+            if(value != null) {
+                setLocation(value.getName().module + "#" + HtmlEscape.escape(value.getName().name));
+            }
+        }
+    }
+    
+    private static class ExpStatus {
+        THashMap<String, ExpStatus> expandedItems = new THashMap<String, ExpStatus>();
+    }
+    
+    private static ExpStatus getExpStatus(Tree tree) {
+        ExpStatus status = new ExpStatus();
+        for(TreeItem child : tree.getItems()) {
+            if(child.getExpanded())
+                status.expandedItems.put(child.getText(), getExpStatus(child));
+        }
+        return status;
+    }
+    
+    private static ExpStatus getExpStatus(TreeItem item) {
+        ExpStatus status = new ExpStatus();
+        for(TreeItem child : item.getItems()) {
+            if(child.getExpanded())
+                status.expandedItems.put(child.getText(), getExpStatus(child));
+        }
+        return status;
+    }
+    
+    private static void setExpStatus(Tree tree, ExpStatus status) {
+        for(TreeItem child : tree.getItems()) {
+            ExpStatus childStatus = status.expandedItems.get(child.getText());
+            if(childStatus != null) {
+                child.setExpanded(true);
+                setExpStatus(child, childStatus);
+            }
+        }
+    }
+    
+    private static void setExpStatus(TreeItem item, ExpStatus status) {
+        for(TreeItem child : item.getItems()) {
+            ExpStatus childStatus = status.expandedItems.get(child.getText());
+            child.setExpanded(true);
+            if (childStatus != null) {
+                setExpStatus(child, childStatus);
+            }
+        }
+    }
+    
+    private void updateNavigationTree() {
+        HierarchicalDocumentationRef root = HierarchicalDocumentationRef.generateTree(SCLOsgi.SOURCE_REPOSITORY);
+
+        ExpStatus status = getExpStatus(navigationTree);
+        navigationTree.removeAll();
+        for(HierarchicalDocumentationRef navItem : root.getChildren()) {
+            TreeItem item = new TreeItem(navigationTree, SWT.NONE);
+            configureTreeItem(navItem, item);
+        }
+        setExpStatus(navigationTree, status);
+    }
+    
+    private void configureTreeItem(HierarchicalDocumentationRef navItem, TreeItem item) {
+        item.setText(navItem.getName());
+        item.setData(navItem.getDocumentationName());
+        for(HierarchicalDocumentationRef childNavItem : navItem.getChildren()) {
+            TreeItem childItem = new TreeItem(item, SWT.NONE);
+            configureTreeItem(childNavItem, childItem);
+        }
+    }
+
+    public void setLocation(String path) {
+        browser.setUrl("about:" + path);
+    }
+}