1 package org.simantics.scl.ui.browser;
3 import gnu.trove.map.hash.THashMap;
5 import java.io.IOException;
6 import java.nio.file.Paths;
7 import java.util.ArrayList;
9 import org.eclipse.core.runtime.Status;
10 import org.eclipse.jface.dialogs.ErrorDialog;
11 import org.eclipse.jface.layout.GridDataFactory;
12 import org.eclipse.jface.layout.GridLayoutFactory;
13 import org.eclipse.swt.SWT;
14 import org.eclipse.swt.browser.Browser;
15 import org.eclipse.swt.browser.LocationAdapter;
16 import org.eclipse.swt.browser.LocationEvent;
17 import org.eclipse.swt.browser.ProgressAdapter;
18 import org.eclipse.swt.browser.ProgressEvent;
19 import org.eclipse.swt.custom.SashForm;
20 import org.eclipse.swt.events.KeyAdapter;
21 import org.eclipse.swt.events.KeyEvent;
22 import org.eclipse.swt.events.KeyListener;
23 import org.eclipse.swt.events.MouseEvent;
24 import org.eclipse.swt.events.MouseWheelListener;
25 import org.eclipse.swt.events.SelectionAdapter;
26 import org.eclipse.swt.events.SelectionEvent;
27 import org.eclipse.swt.events.TraverseEvent;
28 import org.eclipse.swt.events.TraverseListener;
29 import org.eclipse.swt.graphics.Color;
30 import org.eclipse.swt.layout.FillLayout;
31 import org.eclipse.swt.widgets.Button;
32 import org.eclipse.swt.widgets.Composite;
33 import org.eclipse.swt.widgets.DirectoryDialog;
34 import org.eclipse.swt.widgets.Event;
35 import org.eclipse.swt.widgets.Listener;
36 import org.eclipse.swt.widgets.Text;
37 import org.eclipse.swt.widgets.Tree;
38 import org.eclipse.swt.widgets.TreeItem;
39 import org.simantics.scl.compiler.elaboration.modules.SCLValue;
40 import org.simantics.scl.compiler.markdown.html.GenerateAllHtmlDocumentation;
41 import org.simantics.scl.compiler.markdown.html.HierarchicalDocumentationRef;
42 import org.simantics.scl.compiler.markdown.html.HtmlDocumentationGeneration;
43 import org.simantics.scl.compiler.markdown.internal.HtmlEscape;
44 import org.simantics.scl.osgi.SCLOsgi;
45 import org.simantics.scl.ui.Activator;
47 public class SCLDocumentationBrowser {
49 public static final String STANDARD_LIBRARY = "StandardLibrary";
56 String currentPageName = "";
61 ArrayList<String> locationHistory = new ArrayList<String>();
62 int locationHistoryPosition = -1;
64 ArrayList<Runnable> runWhenCompleted = new ArrayList<Runnable>(2);
65 Object runWhenCompletedLock = new Object();
67 private void executeWhenCompleted(final String script) {
68 synchronized (runWhenCompletedLock) {
69 runWhenCompleted.add(new Runnable() {
72 browser.execute(script);
78 private void newLocation(String location) {
79 if(locationHistoryPosition < 0 ||
80 !location.equals(locationHistory.get(locationHistoryPosition))) {
81 ++locationHistoryPosition;
82 while(locationHistory.size() > locationHistoryPosition)
83 locationHistory.remove(locationHistory.size()-1);
84 locationHistory.add(location);
90 if(locationHistoryPosition > 0) {
91 browser.setUrl(locationHistory.get(--locationHistoryPosition));
96 private void refresh() {
97 SCLOsgi.SOURCE_REPOSITORY.checkUpdates();
98 final Object yOffset = browser.evaluate("return window.pageYOffset !== undefined ? window.pageYOffset : ((document.compatMode || \"\") === \"CSS1Compat\") ? document.documentElement.scrollTop : document.body.scrollTop;");
100 executeWhenCompleted("window.scroll(0,"+yOffset+");");
101 browser.setUrl(locationHistory.get(locationHistoryPosition));
102 updateNavigationTree();
105 private void forward() {
106 if(locationHistoryPosition < locationHistory.size()-1) {
107 browser.setUrl(locationHistory.get(++locationHistoryPosition));
112 private void updateButtons() {
113 backButton.setEnabled(locationHistoryPosition > 0);
114 forwardButton.setEnabled(locationHistoryPosition < locationHistory.size()-1);
117 private void setCurrentLocation(String location) {
118 pageName.setText(location);
119 currentPageName = location;
122 public SCLDocumentationBrowser(Composite parent) {
123 Color white = parent.getDisplay().getSystemColor(SWT.COLOR_WHITE);
125 final Composite composite = new Composite(parent, SWT.NONE);
126 GridLayoutFactory.fillDefaults().spacing(0, 0).applyTo(composite);
127 composite.setBackground(white);
129 Composite buttons = new Composite(composite, SWT.NONE);
130 buttons.setBackground(white);
131 GridDataFactory.fillDefaults().grab(true, false).align(SWT.FILL, SWT.CENTER).applyTo(buttons);
132 GridLayoutFactory.fillDefaults().numColumns(6).margins(3, 3).applyTo(buttons);
134 backButton = new Button(buttons, SWT.PUSH);
135 buttons.setBackground(composite.getDisplay().getSystemColor(SWT.COLOR_GRAY));
136 backButton.setToolTipText("Back (Alt-Left)");
137 backButton.setEnabled(false);
138 backButton.setImage(Activator.getInstance().getImageRegistry().get("arrow_left"));
140 forwardButton = new Button(buttons, SWT.PUSH);
141 forwardButton.setToolTipText("Forward (Alt-Right)");
142 forwardButton.setEnabled(false);
143 forwardButton.setImage(Activator.getInstance().getImageRegistry().get("arrow_right"));
145 refreshButton = new Button(buttons, SWT.PUSH);
146 refreshButton.setToolTipText("Refresh page (Ctrl-R)");
147 refreshButton.setImage(Activator.getInstance().getImageRegistry().get("arrow_refresh"));
149 pageName = new Text(buttons, SWT.BORDER | SWT.SINGLE);
150 GridDataFactory.fillDefaults().grab(true, false).align(SWT.FILL, SWT.CENTER).applyTo(pageName);
152 findButton = new Button(buttons, SWT.PUSH);
153 findButton.setToolTipText("Find SCL definitions (Ctrl-H)");
154 findButton.setImage(Activator.getInstance().getImageRegistry().get("find"));
156 saveButton = new Button(buttons, SWT.PUSH);
157 saveButton.setToolTipText("Save documentation to disk");
158 saveButton.setImage(Activator.getInstance().getImageRegistry().get("disk"));
160 SashForm browserBox = new SashForm(composite, SWT.BORDER | SWT.HORIZONTAL);
161 GridDataFactory.fillDefaults().grab(true, true).align(SWT.FILL, SWT.FILL).applyTo(browserBox);
162 browserBox.setLayout(new FillLayout());
164 navigationTree = new Tree(browserBox, SWT.SINGLE);
165 updateNavigationTree();
166 navigationTree.addSelectionListener(new SelectionAdapter() {
168 public void widgetSelected(SelectionEvent e) {
169 TreeItem[] items = navigationTree.getSelection();
170 if(items.length == 1) {
171 String documentationName = (String)items[0].getData();
172 if(documentationName != null)
173 setLocation(documentationName);
178 browser = new Browser(browserBox, SWT.BORDER);
179 browserBox.setWeights(new int[] {15, 85});
180 browser.addProgressListener(new ProgressAdapter() {
182 public void completed(ProgressEvent event) {
183 ArrayList<Runnable> rs;
184 synchronized(runWhenCompletedLock) {
185 rs = runWhenCompleted;
186 runWhenCompleted = new ArrayList<Runnable>(2);
192 browser.addLocationListener(new LocationAdapter() {
193 public void changing(LocationEvent event) {
194 String location = event.location;
195 if(location.startsWith("about:blank"))
197 newLocation(location);
198 if(location.startsWith("about:")) {
199 location = location.substring(6);
200 setCurrentLocation(location);
201 int hashPos = location.indexOf('#');
202 final String fragment;
204 fragment = location.substring(hashPos);
205 location = location.substring(0, hashPos);
209 if(location.endsWith(".html"))
210 location = location.substring(0, location.length()-5);
212 String html = HtmlDocumentationGeneration.generate(SCLOsgi.MODULE_REPOSITORY, location, null);
214 browser.setText(html);
216 executeWhenCompleted("location.hash = \"" + fragment + "\";");
220 setCurrentLocation(location);
224 KeyListener keyListener = new KeyAdapter() {
226 public void keyPressed(KeyEvent e) {
227 if((e.stateMask & SWT.ALT) != 0) {
228 if(e.keyCode == SWT.ARROW_LEFT) {
232 else if(e.keyCode == SWT.ARROW_RIGHT) {
237 else if((e.stateMask & SWT.CTRL) != 0) {
238 if(e.keyCode == 'r' || e.keyCode == 'R') {
242 if(e.keyCode == 'l' || e.keyCode == 'L') {
243 pageName.selectAll();
247 if(e.keyCode == 'k' || e.keyCode == 'K') {
248 pageName.setText("?");
249 pageName.setSelection(1);
253 if(e.keyCode == 'h' || e.keyCode == 'H') {
258 if(e.keyCode == SWT.PAGE_DOWN) {
259 browser.execute("window.scrollBy(0,0.8*(window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight));");
261 else if(e.keyCode == SWT.PAGE_UP) {
262 browser.execute("window.scrollBy(0,-0.8*(window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight));");
264 else if(e.keyCode == SWT.ARROW_DOWN) {
265 browser.execute("window.scrollBy(0,100);");
267 else if(e.keyCode == SWT.ARROW_UP) {
268 browser.execute("window.scrollBy(0,-100);");
270 else if(e.keyCode == SWT.HOME) {
271 if(e.widget != pageName)
272 browser.execute("window.scroll(0,0);");
274 else if(e.keyCode == SWT.END) {
275 if(e.widget != pageName)
276 browser.execute("window.scroll(0,document.body.scrollHeight);");
281 composite.addKeyListener(keyListener);
282 browser.addKeyListener(keyListener);
283 pageName.addKeyListener(keyListener);
285 MouseWheelListener wheelListener = new MouseWheelListener() {
287 public void mouseScrolled(MouseEvent e) {
288 browser.execute("window.scrollBy(0,"+e.count*(-30)+");");
291 composite.addMouseWheelListener(wheelListener);
292 browser.addMouseWheelListener(wheelListener);
293 backButton.addSelectionListener(new SelectionAdapter() {
295 public void widgetSelected(SelectionEvent e) {
299 forwardButton.addSelectionListener(new SelectionAdapter() {
301 public void widgetSelected(SelectionEvent e) {
305 refreshButton.addListener(SWT.Selection, new Listener() {
307 public void handleEvent(Event event) {
311 pageName.addTraverseListener(new TraverseListener() {
313 public void keyTraversed(TraverseEvent e) {
314 if(e.detail == SWT.TRAVERSE_RETURN)
315 setLocation(pageName.getText());
316 else if(e.detail == SWT.TRAVERSE_ESCAPE)
317 pageName.setText(currentPageName);
320 findButton.addSelectionListener(new SelectionAdapter() {
322 public void widgetSelected(SelectionEvent e) {
326 saveButton.addSelectionListener(new SelectionAdapter() {
328 public void widgetSelected(SelectionEvent e) {
329 DirectoryDialog dialog = new DirectoryDialog(saveButton.getShell());
330 dialog.setText("Select the directory for storing the documentation.");
331 dialog.setMessage("Select a directory where the documentation is generated to.");
332 String directory = dialog.open();
333 if(directory != null) {
335 GenerateAllHtmlDocumentation.generate(SCLOsgi.MODULE_REPOSITORY, Paths.get(directory));
336 } catch (IOException ex) {
337 ex.printStackTrace();
338 ErrorDialog.openError(saveButton.getShell(), "Documentation generation failed", null,
339 new Status(Status.ERROR, "org.simantics.scl.ui", 0, ex.toString(), ex));
346 private void find() {
347 SCLDefinitionSelectionDialog dialog = new SCLDefinitionSelectionDialog(findButton.getShell());
348 if(dialog.open() == SCLDefinitionSelectionDialog.OK) {
349 SCLValue value = (SCLValue)dialog.getFirstResult();
351 setLocation(value.getName().module + "#" + HtmlEscape.escape(value.getName().name));
356 private static class ExpStatus {
357 THashMap<String, ExpStatus> expandedItems = new THashMap<String, ExpStatus>();
360 private static ExpStatus getExpStatus(Tree tree) {
361 ExpStatus status = new ExpStatus();
362 for(TreeItem child : tree.getItems()) {
363 if(child.getExpanded())
364 status.expandedItems.put(child.getText(), getExpStatus(child));
369 private static ExpStatus getExpStatus(TreeItem item) {
370 ExpStatus status = new ExpStatus();
371 for(TreeItem child : item.getItems()) {
372 if(child.getExpanded())
373 status.expandedItems.put(child.getText(), getExpStatus(child));
378 private static void setExpStatus(Tree tree, ExpStatus status) {
379 for(TreeItem child : tree.getItems()) {
380 ExpStatus childStatus = status.expandedItems.get(child.getText());
381 if(childStatus != null) {
382 child.setExpanded(true);
383 setExpStatus(child, childStatus);
388 private static void setExpStatus(TreeItem item, ExpStatus status) {
389 for(TreeItem child : item.getItems()) {
390 ExpStatus childStatus = status.expandedItems.get(child.getText());
391 child.setExpanded(true);
392 if (childStatus != null) {
393 setExpStatus(child, childStatus);
398 private void updateNavigationTree() {
399 HierarchicalDocumentationRef root = HierarchicalDocumentationRef.generateTree(SCLOsgi.SOURCE_REPOSITORY);
401 ExpStatus status = getExpStatus(navigationTree);
402 navigationTree.removeAll();
403 for(HierarchicalDocumentationRef navItem : root.getChildren()) {
404 TreeItem item = new TreeItem(navigationTree, SWT.NONE);
405 configureTreeItem(navItem, item);
407 setExpStatus(navigationTree, status);
410 private void configureTreeItem(HierarchicalDocumentationRef navItem, TreeItem item) {
411 item.setText(navItem.getName());
412 item.setData(navItem.getDocumentationName());
413 for(HierarchicalDocumentationRef childNavItem : navItem.getChildren()) {
414 TreeItem childItem = new TreeItem(item, SWT.NONE);
415 configureTreeItem(childNavItem, childItem);
419 public void setLocation(String path) {
420 browser.setUrl("about:" + path);