package zander.ui.docs; import java.awt.BorderLayout; import java.awt.Component; import java.awt.Desktop; import java.awt.Dialog; import java.awt.Window; import java.awt.event.KeyEvent; import java.io.IOException; import java.io.InputStream; import java.net.URISyntaxException; import java.net.URL; import java.util.Arrays; import java.util.NoSuchElementException; import javax.swing.JCheckBox; import javax.swing.JEditorPane; import javax.swing.JFrame; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JSplitPane; import javax.swing.JTree; import javax.swing.SwingConstants; import javax.swing.SwingUtilities; import javax.swing.event.HyperlinkEvent; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.DefaultTreeModel; import javax.swing.tree.TreePath; import javax.swing.tree.TreeSelectionModel; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import zander.ui.ErrorDialog; import zander.ui.UIUtils; public class DocumentationViewer extends JFrame { private static final long serialVersionUID = -129157128310235L; private static final Logger LOGGER = LoggerFactory.getLogger(DocumentationViewer.class); private static final DocumentBuilderFactory DOCUMENT_BUILDER_FACTORY = DocumentBuilderFactory.newInstance(); private static final DocumentBuilder DOCUMENT_BUILDER; static { try { DOCUMENT_BUILDER = DOCUMENT_BUILDER_FACTORY.newDocumentBuilder(); } catch (ParserConfigurationException e) { throw new Error("Could not create XML parser!", e); } } private static class DocumentationEntry { public String name; public URL url; public DocumentationEntry(String name, URL url) { this.name = name; this.url = url; } @Override public String toString() { return name; } } private final DefaultMutableTreeNode treeRoot; private final DefaultMutableTreeNode aboutNode; private final CuratorAboutPanel aboutPanel; private final DefaultTreeModel treeModel; private final JTree tree; private final JScrollPane treeScroll; private final JCheckBox onTopCheck; private final JPanel treePanel; private final JEditorPane viewer; private final JScrollPane viewerScroll; private final JSplitPane content; public DocumentationViewer(String structureResource) { super("Curator Help"); treeRoot = getDocumentFromString(structureResource); aboutNode = new DefaultMutableTreeNode("About"); treeRoot.add(aboutNode); aboutPanel = new CuratorAboutPanel(); treeModel = new DefaultTreeModel(treeRoot); tree = new JTree(treeModel); tree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); tree.setRootVisible(false); tree.setExpandsSelectedPaths(true); tree.addTreeSelectionListener((e) -> { if (e.getNewLeadSelectionPath() != null) { DefaultMutableTreeNode node = (DefaultMutableTreeNode) e.getNewLeadSelectionPath().getLastPathComponent(); if (node == aboutNode) { openAbout(); } else { openHelp((DefaultMutableTreeNode) e.getNewLeadSelectionPath().getLastPathComponent()); } } }); treeScroll = new JScrollPane(tree); onTopCheck = new JCheckBox("Stay above other windows"); onTopCheck.setHorizontalAlignment(SwingConstants.CENTER); onTopCheck.setMnemonic(KeyEvent.VK_S); onTopCheck.addActionListener((e) -> { setAlwaysOnTop(onTopCheck.isSelected()); }); treePanel = new JPanel(new BorderLayout()); treePanel.add(treeScroll, BorderLayout.CENTER); treePanel.add(onTopCheck, BorderLayout.PAGE_END); viewer = new JEditorPane(); viewer.setContentType("text/html"); viewer.setEditable(false); viewer.addHyperlinkListener((e) -> { if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED) { String tu = e.getDescription(); if (tu.startsWith("help:")) { if (tu.length() <= 6) { return; } String path = tu.substring(5); openHelp(path); } else if (tu.startsWith("http://") || tu.startsWith("https://")) { URL u = e.getURL(); try { Desktop.getDesktop().browse(u.toURI()); } catch (URISyntaxException | IOException ex) { LOGGER.error("Could not open url: {}", u.toExternalForm(), ex); ErrorDialog ed = new ErrorDialog("Link Error", "Could not open link: " + u.toExternalForm(), ex); ed.setVisible(true); } } } }); viewerScroll = new JScrollPane(viewer); content = new JSplitPane(); content.setOneTouchExpandable(true); content.setLeftComponent(treePanel); content.setRightComponent(viewerScroll); setContentPane(content); setModalExclusionType(Dialog.ModalExclusionType.APPLICATION_EXCLUDE); setResizable(true); setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE); setSize((2 * UIUtils.SCREEN_SIZE.width) / 3, (2 * UIUtils.SCREEN_SIZE.height) / 3); expandFirstLevelNodes(); } public void openHelp(String page) { String[] path = page.split("/"); if (path.length != 0) { openHelp(path, treeRoot); } setVisible(true); } private void openHelp(String[] path, DefaultMutableTreeNode node) { int ns = node.getChildCount(); for (int i = 0; i < ns; ++i) { DefaultMutableTreeNode c = (DefaultMutableTreeNode) node.getChildAt(i); if (path[0].equals(String.valueOf(c.getUserObject()))) { openHelp(Arrays.copyOfRange(path, 1, path.length), c); return; } } tree.setSelectionPath(new TreePath(node.getPath())); openHelp(node); } private void openHelp(DefaultMutableTreeNode node) { setTitle("Curator Help"); Object uo = node.getUserObject(); if (uo instanceof DocumentationEntry) { DocumentationEntry e = (DocumentationEntry) uo; try { if (content.getRightComponent() != viewerScroll) { int div = content.getDividerLocation(); content.setRightComponent(viewerScroll); content.setDividerLocation(div); revalidate(); } viewer.setPage(e.url); return; } catch (IOException ex) { // ignore } } } private void openAbout() { tree.setSelectionPath(new TreePath(aboutNode.getPath())); if (content.getRightComponent() != aboutPanel) { int div = content.getDividerLocation(); content.setRightComponent(aboutPanel); content.setDividerLocation(div); revalidate(); } setTitle("About Curator"); LOGGER.info("Oppened about"); setVisible(true); } private DefaultMutableTreeNode getDocumentFromString(String resource) { try { try (InputStream in = ClassLoader.getSystemResourceAsStream(resource)) { Document doc = DOCUMENT_BUILDER.parse(in); Element root = doc.getDocumentElement(); return getDocumentNode(root); } } catch (Throwable e) { LOGGER.error("Error parsing json while initializing DocumentationFrame", e); System.exit(1); return null; } } private DefaultMutableTreeNode getDocumentNode(Node rn) { if (rn.getNodeType() == Node.ELEMENT_NODE) { Element element = (Element) rn; String name = element.getAttribute("name"); DefaultMutableTreeNode tn = new DefaultMutableTreeNode(); if (element.getTagName().equalsIgnoreCase("dir")) { tn.setUserObject(name); NodeList nl = element.getChildNodes(); for (int i = 0; i < nl.getLength(); ++i) { DefaultMutableTreeNode child = getDocumentNode(nl.item(i)); if (child != null) { tn.add(child); } } } else if (element.getTagName().equalsIgnoreCase("file")) { String value = element.getTextContent(); URL url = ClassLoader.getSystemResource(value); DocumentationEntry entry = new DocumentationEntry(name, url); tn.setUserObject(entry); } return tn; } return null; } private void expandFirstLevelNodes() { try { DefaultMutableTreeNode fn = (DefaultMutableTreeNode) treeRoot.getFirstChild(); tree.setSelectionPath(new TreePath(fn.getPath())); } catch (NoSuchElementException e) { // do nothing } } private static final String DOCS_PATH = "docs.xml"; private static DocumentationViewer FRAME; static { if (!SwingUtilities.isEventDispatchThread()) { try { SwingUtilities.invokeAndWait(() -> FRAME = new DocumentationViewer(DOCS_PATH)); } catch (Throwable e) { LOGGER.error("Could not create documentation viewer!", e); System.exit(1); } } else { FRAME = new DocumentationViewer(DOCS_PATH); } } public static void show(String path) { show((Window) null, path); } public static void show(Component comp, String path) { show(SwingUtilities.getWindowAncestor(comp), path); } public static void show(Window parent, String path) { if (!FRAME.isVisible()) { UIUtils.centerWindow(FRAME, parent); } FRAME.openHelp(path); } public static void showAbout() { FRAME.openAbout(); } public static void showAbout(Window parent) { if (!FRAME.isVisible()) { UIUtils.centerWindow(FRAME, parent); } showAbout(); } }