/* * Copyright (c) 2016 Helmut Neemann * Use of this source code is governed by the GPL v3 license * that can be found in the LICENSE file. */ package de.neemann.digital.gui.components; import de.neemann.digital.core.NodeException; import de.neemann.digital.core.element.*; import de.neemann.digital.draw.elements.PinException; import de.neemann.digital.draw.elements.VisualElement; import de.neemann.digital.draw.library.ElementLibrary; import de.neemann.digital.draw.shapes.ShapeFactory; import de.neemann.digital.lang.Lang; import de.neemann.gui.ErrorMessage; import de.neemann.gui.Screen; import de.neemann.gui.ToolTipAction; import javax.imageio.ImageIO; import javax.swing.*; import javax.swing.event.HyperlinkEvent; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.image.BufferedImage; import java.io.*; import java.net.*; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.util.ArrayList; import java.util.HashMap; /** * Simple Dialog to show an elements help text. *

*/ public class ElementHelpDialog extends JDialog { private static final int IMAGE_SCALE = 2; private static final int MAX_WIDTH = 600; private static final int MAX_HEIGHT = 800; private final boolean showKeys; private JPanel buttons; /** * Creates a new instance * * @param parent the parents frame * @param elementType the type of the element * @param elementAttributes the attributes of this element * @throws PinException PinException * @throws NodeException NodeException */ public ElementHelpDialog(Window parent, ElementTypeDescription elementType, ElementAttributes elementAttributes) throws NodeException, PinException { this(parent, elementType, elementAttributes, false); } /** * Creates a new instance * * @param parent the parents frame * @param elementType the type of the element * @param elementAttributes the attributes of this element * @param showKeys shows the key strings * @throws PinException PinException * @throws NodeException NodeException */ public ElementHelpDialog(Window parent, ElementTypeDescription elementType, ElementAttributes elementAttributes, boolean showKeys) throws NodeException, PinException { super(parent, Lang.get("attr_help"), ModalityType.MODELESS); this.showKeys = showKeys; setDefaultCloseOperation(DISPOSE_ON_CLOSE); StringWriter w = new StringWriter(); try { writeDetailedDescription(w, elementType, elementAttributes); } catch (IOException e) { // can not happen because of writing to memory } init(parent, w.toString()); } /** * Creates a new instance * * @param parent the parents dialog * @param library the elements library * @param shapeFactory the shape factory used to create the PNGs * @throws PinException PinException * @throws NodeException NodeException */ public ElementHelpDialog(JFrame parent, ElementLibrary library, ShapeFactory shapeFactory) throws NodeException, PinException { super(parent, Lang.get("attr_help"), true); showKeys = false; setDefaultCloseOperation(DISPOSE_ON_CLOSE); MyURLStreamHandlerFactory.setShapeFactory(shapeFactory); StringWriter w = new StringWriter(); try { w.write(""); writeFullHTMLDocumentation(w, library, description -> "image:" + description.getName() + ".png"); w.write(""); } catch (IOException e) { // can not happen because of writing to memory } init(parent, w.toString()); buttons.add( new ToolTipAction(Lang.get("btn_openInBrowser")) { @Override public void actionPerformed(ActionEvent actionEvent) { try { File tmp = Files.createTempDirectory("digital").toFile(); exportHTMLDocumentation(tmp, library); File index = new File(tmp, "index.html"); openWebpage(index.toURI()); } catch (IOException | PinException | NodeException e) { new ErrorMessage(Lang.get("err_openingDocumentation")).addCause(e).show(ElementHelpDialog.this); } } }.setToolTip(Lang.get("btn_openInBrowser_tt")).createJButton(), 0); } private void init(Component parent, String description) { JEditorPane editorPane = new JEditorPane("text/html", description); editorPane.setEditable(false); editorPane.setCaretPosition(0); editorPane.putClientProperty(JEditorPane.HONOR_DISPLAY_PROPERTIES, true); editorPane.addHyperlinkListener(hyperlinkEvent -> { if (HyperlinkEvent.EventType.ACTIVATED == hyperlinkEvent.getEventType()) { String desc = hyperlinkEvent.getDescription(); if (desc == null || !desc.startsWith("#")) return; desc = desc.substring(1); editorPane.scrollToReference(desc); } }); getContentPane().add(new JScrollPane(editorPane)); buttons = new JPanel(new FlowLayout(FlowLayout.RIGHT)); buttons.add(new JButton(new AbstractAction(Lang.get("ok")) { @Override public void actionPerformed(ActionEvent actionEvent) { dispose(); } })); getContentPane().add(buttons, BorderLayout.SOUTH); pack(); Dimension r = getSize(); if (r.width > MAX_WIDTH) r.width = MAX_WIDTH; if (r.height > MAX_HEIGHT) r.height = MAX_HEIGHT; setSize(Screen.getInstance().scale(r)); setLocationRelativeTo(parent); } /** * Creates a full HTML documentation of all elements * * @param library the library which parts are documented * @param imageHandler the imageHandler creates the url to get the image representing a concrete part * @throws IOException IOException * @throws PinException PinException * @throws NodeException NodeException */ private void writeFullHTMLDocumentation(Writer w, ElementLibrary library, ImageHandler imageHandler) throws IOException, NodeException, PinException { ArrayList chapter = new ArrayList<>(); String actPath = null; StringWriter content = new StringWriter(); int chapNum = 0; for (ElementLibrary.ElementContainer e : library) { String p = e.getTreePath(); if (!p.equals(actPath)) { actPath = p; chapter.add(actPath); chapNum++; content.append("

").append(Integer.toString(chapNum)).append(". ").append(actPath).append("

\n"); content.append("
\n"); } String url = imageHandler.getUrl(e.getDescription()); BufferedImage bi = MyURLStreamHandlerFactory.getImage(e.getDescription().getName()); content.append("
\n"); writeHTMLDescription(content, e.getDescription(), new ElementAttributes()); content.append("
\n"); } content.flush(); w.append("

").append(Lang.get("digital")).append("

\n"); w.append("

").append(Lang.get("tableOfContent")).append("

\n"); chapNum = 0; for (String chap : chapter) { chapNum++; w.append(Integer.toString(chapNum)).append(". ").append(chap).append("
\n"); } w.write(content.toString()); } /** * Creates a detailed human readable description of this element * * @param et the element to describe * @param elementAttributes the actual attributes of the element to describe */ private void writeDetailedDescription(Writer w, ElementTypeDescription et, ElementAttributes elementAttributes) throws IOException, NodeException, PinException { w.write(""); writeHTMLDescription(w, et, elementAttributes); w.write(""); } /** * Adds the description of the given element to the given StringBuilder. * * @param w the StringBuilder to use * @param et the element to describe * @param elementAttributes the actual attributes of the element to describe * @throws IOException IOException * @throws PinException PinException * @throws NodeException NodeException */ private void writeHTMLDescription(Writer w, ElementTypeDescription et, ElementAttributes elementAttributes) throws IOException, NodeException, PinException { String translatedName = et.getTranslatedName(); if (translatedName.endsWith(".dig")) translatedName = new File(translatedName).getName(); w.append("

").append(escapeHTML(translatedName)).append("

\n"); String descr = et.getDescription(elementAttributes); if (showKeys) descr += " (" + Lang.get("msg_keyAsGenericAttribute", et.getName()) + ")"; if (!descr.equals(translatedName)) w.append("

").append(escapeHTML(descr)).append("

\n"); PinDescriptions inputs = et.getInputDescription(elementAttributes); if (inputs != null && inputs.size() > 0) { w.append("

").append(Lang.get("elem_Help_inputs")).append(":

\n
\n"); for (PinDescription i : inputs) writeEntry(w, i.getName(), i.getDescription()); w.append("
\n"); } PinDescriptions outputs = et.getOutputDescriptions(elementAttributes); if (outputs != null && outputs.size() > 0) { w.append("

").append(Lang.get("elem_Help_outputs")).append(":

\n
\n"); for (PinDescription i : outputs) { final String description = i.getDescription(); writeEntry(w, i.getName(), description); } w.append("
\n"); } if (et.getAttributeList().size() > 0) { w.append("

").append(Lang.get("elem_Help_attributes")).append(":

\n
\n"); for (Key k : et.getAttributeList()) if (!k.isSecondary()) writeEntry(w, k); for (Key k : et.getAttributeList()) if (k.isSecondary()) writeEntry(w, k); w.append("
\n"); } } private void writeEntry(Writer w, String name, String description) throws IOException { w.append("
").append(escapeHTML(name)).append("
\n"); if (description != null && description.length() > 0 && !name.equals(description)) w.append("
").append(escapeHTML(description)).append("
\n"); } private void writeEntry(Writer w, Key key) throws IOException { final String name = key.getName(); final String description = key.getDescription(); w.append("
").append(escapeHTML(name)).append("
\n"); if (description != null && description.length() > 0 && !name.equals(description)) { w.append("
").append(escapeHTML(description)); if (showKeys) { String keyName = key.getKey(); if (keyName.contains(" ")) keyName = "'" + keyName + "'"; w.append(" (").append(Lang.get("msg_keyAsGenericAttribute", keyName)).append(')'); } w.append("
\n"); } } /** * @return factory which catches 'image' protocol requests to deliver images via an URL. */ public static URLStreamHandlerFactory createURLStreamHandlerFactory() { return new MyURLStreamHandlerFactory(); } private static class MyURLStreamHandlerFactory implements URLStreamHandlerFactory { private static final HashMap IMAGE_MAP = new HashMap<>(); private static ShapeFactory shapeFactory; public static void setShapeFactory(ShapeFactory shapeFactory) { MyURLStreamHandlerFactory.shapeFactory = shapeFactory; IMAGE_MAP.clear(); } @Override public URLStreamHandler createURLStreamHandler(String protocol) { if (protocol.equals("image")) return new URLStreamHandler() { @Override protected URLConnection openConnection(URL u) { return new ImageConnection(u); } }; else return null; } static BufferedImage getImage(String name) { BufferedImage bi = IMAGE_MAP.get(name); if (bi == null) { final float scale = IMAGE_SCALE * Screen.getInstance().getScaling(); bi = new VisualElement(name) .setShapeFactory(shapeFactory) .getBufferedImage(0.75 * scale, (int) (250 * scale)); IMAGE_MAP.put(name, bi); } return bi; } } private static class ImageConnection extends URLConnection { ImageConnection(URL url) { super(url); } @Override public void connect() { } @Override public InputStream getInputStream() throws IOException { String path = url.getPath(); if (path.endsWith(".png")) path = path.substring(0, path.length() - 4); BufferedImage bi = MyURLStreamHandlerFactory.getImage(path); ByteArrayOutputStream baos = new ByteArrayOutputStream(); ImageIO.write(bi, "png", baos); return new ByteArrayInputStream(baos.toByteArray()); } } private interface ImageHandler { String getUrl(ElementTypeDescription description) throws IOException, PinException, NodeException; } /** * Writes the html documentation to a file * * @param targetPath the target folder to store the documentation * @param library the library to use * @throws IOException IOException * @throws PinException PinException * @throws NodeException NodeException */ private void exportHTMLDocumentation(File targetPath, ElementLibrary library) throws IOException, NodeException, PinException { File images = new File(targetPath, "img"); if (!images.mkdir()) throw new IOException("could not create image folder " + images); try (BufferedWriter w = new BufferedWriter( new OutputStreamWriter( new FileOutputStream( new File(targetPath, "index.html")), StandardCharsets.UTF_8))) { w.write("\n" + "\n" + "\n" + "\n" + "\n\n"); writeFullHTMLDocumentation(w, library, description -> { BufferedImage bi = MyURLStreamHandlerFactory.getImage(description.getName()); final String filename = description.getName().replace('\\', '_').replace('/', '_').replace(':', '_'); ImageIO.write(bi, "png", new File(images, filename + ".png")); return "img/" + filename + ".png"; }); w.write("\n"); } } private static void openWebpage(URI uri) throws IOException { Desktop desktop = Desktop.isDesktopSupported() ? Desktop.getDesktop() : null; if (desktop != null && desktop.isSupported(Desktop.Action.BROWSE)) desktop.browse(uri); else throw new IOException("could not open browser"); } private static String escapeHTML(String text) { StringBuilder sb = new StringBuilder(text.length() * 2); for (int i = 0; i < text.length(); i++) { char c = text.charAt(i); switch (c) { case '<': sb.append("<"); break; case '>': sb.append(">"); break; case '&': sb.append("&"); break; case '"': sb.append("""); break; default: sb.append(c); } } return sb.toString(); } }