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"; //$NON-NLS-1$ Browser browser; Button backButton; Text pageName; Button refreshButton; Button forwardButton; String currentPageName = ""; //$NON-NLS-1$ Button saveButton; Button findButton; Tree navigationTree; ArrayList locationHistory = new ArrayList(); int locationHistoryPosition = -1; ArrayList runWhenCompleted = new ArrayList(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;"); //$NON-NLS-1$ if(yOffset != null) executeWhenCompleted("window.scroll(0,"+yOffset+");"); //$NON-NLS-1$ //$NON-NLS-2$ 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(Messages.SCLDocumentationBrowser_BackTT); backButton.setEnabled(false); backButton.setImage(Activator.getInstance().getImageRegistry().get("arrow_left")); //$NON-NLS-1$ forwardButton = new Button(buttons, SWT.PUSH); forwardButton.setToolTipText(Messages.SCLDocumentationBrowser_ForwardTT); forwardButton.setEnabled(false); forwardButton.setImage(Activator.getInstance().getImageRegistry().get("arrow_right")); //$NON-NLS-1$ refreshButton = new Button(buttons, SWT.PUSH); refreshButton.setToolTipText(Messages.SCLDocumentationBrowser_RefreshPageTT); refreshButton.setImage(Activator.getInstance().getImageRegistry().get("arrow_refresh")); //$NON-NLS-1$ 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(Messages.SCLDocumentationBrowser_FindSCLDefinitionTT); findButton.setImage(Activator.getInstance().getImageRegistry().get("find")); //$NON-NLS-1$ saveButton = new Button(buttons, SWT.PUSH); saveButton.setToolTipText(Messages.SCLDocumentationBrowser_SaveDocumentationToDiskTT); saveButton.setImage(Activator.getInstance().getImageRegistry().get("disk")); //$NON-NLS-1$ 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 rs; synchronized(runWhenCompletedLock) { rs = runWhenCompleted; runWhenCompleted = new ArrayList(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")) //$NON-NLS-1$ return; newLocation(location); if(location.startsWith("about:")) { //$NON-NLS-1$ 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")) //$NON-NLS-1$ 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 + "\";"); //$NON-NLS-1$ //$NON-NLS-2$ 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("?"); //$NON-NLS-1$ 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));"); //$NON-NLS-1$ } else if(e.keyCode == SWT.PAGE_UP) { browser.execute("window.scrollBy(0,-0.8*(window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight));"); //$NON-NLS-1$ } else if(e.keyCode == SWT.ARROW_DOWN) { browser.execute("window.scrollBy(0,100);"); //$NON-NLS-1$ } else if(e.keyCode == SWT.ARROW_UP) { browser.execute("window.scrollBy(0,-100);"); //$NON-NLS-1$ } else if(e.keyCode == SWT.HOME) { if(e.widget != pageName) browser.execute("window.scroll(0,0);"); //$NON-NLS-1$ } else if(e.keyCode == SWT.END) { if(e.widget != pageName) browser.execute("window.scroll(0,document.body.scrollHeight);"); //$NON-NLS-1$ } } }; 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)+");"); //$NON-NLS-1$ //$NON-NLS-2$ } }; 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(Messages.SCLDocumentationBrowser_SaveDocumentationDialog_Title); dialog.setMessage(Messages.SCLDocumentationBrowser_SaveDocumentationDialog_Msg); 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, //$NON-NLS-1$ new Status(Status.ERROR, "org.simantics.scl.ui", 0, ex.toString(), ex)); //$NON-NLS-1$ } } } }); } 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)); //$NON-NLS-1$ } } } private static class ExpStatus { THashMap expandedItems = new THashMap(); } 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); //$NON-NLS-1$ } }