--- /dev/null
+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);
+ }
+}