package jamel.gui; import java.awt.Color; import java.awt.Dimension; import java.awt.event.AdjustmentEvent; import java.awt.event.AdjustmentListener; import java.io.File; import java.util.LinkedList; import java.util.List; import javax.swing.BorderFactory; import javax.swing.BoundedRangeModel; import javax.swing.JEditorPane; import javax.swing.JScrollBar; import javax.swing.JScrollPane; import javax.swing.SwingUtilities; import javax.swing.border.Border; import javax.swing.event.HyperlinkEvent; import javax.swing.event.HyperlinkListener; import javax.swing.text.DefaultCaret; import javax.xml.parsers.DocumentBuilderFactory; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.w3c.dom.Text; import jamel.data.Expression; import jamel.util.Simulation; /** * A jPanel that contains Html text and dynamical data. */ public class HtmlPanel extends JScrollPane implements AdjustmentListener, Updatable { /** * Error message. */ private static final String ERROR_STRING = "<div style=\"color:red\">ERROR</div>"; /** * Returns a new data element. * * @param expression * the expression. * @return a new data element. */ private static HtmlElement getNewDataElement(final Expression expression) { return new HtmlElement() { @Override public String getText() { return "" + expression.getValue(); } }; } /** * Converts the specified XML element into a new HTML element. * * @param elem * the XML element to be converted. * @param simulation * the parent simulation. * @return a new {@link HtmlElement}. */ private static HtmlElement getNewHtmlElement(final Element elem, final Simulation simulation) { final String tagName = elem.getTagName(); final NodeList list = elem.getChildNodes(); final NamedNodeMap attributes = elem.getAttributes(); String attributes1 = ""; for (int i = 0; i < attributes.getLength(); i++) { final Node item = attributes.item(i); attributes1 += " " + item.getNodeName() + "=\"" + item.getNodeValue() + "\""; } final String attributes2 = attributes1; final List<HtmlElement> htmlElements = new LinkedList<>(); for (int i = 0; i < list.getLength(); i++) { // *** final HtmlElement htmlElement; if (list.item(i).getNodeType() == Node.ELEMENT_NODE) { final Element elem2 = (Element) list.item(i); if (elem2.getNodeName().equals("data")) { Expression expression = null; try { expression = simulation.getExpression(elem2.getTextContent()); } catch (final Exception e) { e.printStackTrace(); } if (expression == null) { htmlElement = getNewTextElement(ERROR_STRING); } else { htmlElement = getNewDataElement(expression); } /* * TODO implement or remove : * } else if (elem2.getNodeName().equals("test")) { result = getNewTestElement(elem2, macroDatabase); } else if (elem2.getNodeName().equals("info")) { result = getNewInfoElement(elem2, macroDatabase); } else if (elem2.getNodeName().equals("jamel.version")) { result = getNewJamelVersionElement();*/ } else { htmlElement = getNewHtmlElement(elem2, simulation); } } else if (list.item(i).getNodeType() == Node.TEXT_NODE) { final String string = ((Text) list.item(i)).getWholeText(); htmlElement = getNewTextElement(string); } else if (list.item(i).getNodeType() == Node.COMMENT_NODE) { htmlElement = null; } else { throw new RuntimeException("not yet implemented"); // htmlElement = getNewErrorElement("Unexpected node type: " + // node.getNodeType()); } // *** if (htmlElement != null) { htmlElements.add(htmlElement); } } return new HtmlElement() { @Override public String getText() { String result; if (htmlElements.size() == 0) { result = "<" + tagName + attributes2 + "/>"; } else { result = "<" + tagName + attributes2 + ">"; for (HtmlElement htmlElement : htmlElements) { result += htmlElement.getText(); } result += "</" + tagName + ">"; } return result; } }; } /** * Returns a new text element. * * @param string * the string to be displayed. * @return a new text element. */ private static HtmlElement getNewTextElement(String string) { return new HtmlElement() { @Override public String getText() { return string; } }; } /** * If the scroll bar has to be adjusted. */ private boolean adjustScrollBar = true; /** * The main html element. */ final private HtmlElement htmlElement; /** * The component that displays the text. */ private JEditorPane jEditorPane; /** * Previous maximum value of the scroll bar. */ private int previousMaximum = -1; /** * Previous value of the scroll bar. */ private int previousValue = -1; /** * The text displayed by this panel. */ private String text = ""; /** * Creates and returns a new HtmlPanel. * * @param elem * the description of the panel to be created. * @param gui * the parent gui. */ public HtmlPanel(final Element elem, final Gui gui) { super(); final Element panelDescription; if (elem.hasAttribute("src")) { /* * Opens and reads the XML file that contains the specification of the panel. */ final String src = elem.getAttribute("src"); final String fileName = gui.getFile().getParent() + "/" + src; final File panelFile = new File(fileName); final Element root; try { root = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(panelFile).getDocumentElement(); } catch (final Exception e) { throw new RuntimeException("Something went wrong while reading \"" + fileName + "\"", e); } if (!root.getTagName().equals("html")) { throw new RuntimeException(fileName + ": Bad element: " + root.getTagName()); } panelDescription = root; } else { panelDescription = elem; } this.jEditorPane = new JEditorPane(); this.jEditorPane.setContentType("text/html"); this.htmlElement = getNewHtmlElement(panelDescription, gui.getSimulation()); this.jEditorPane.setText(this.htmlElement.getText()); this.jEditorPane.setEditable(false); this.jEditorPane.addHyperlinkListener(new HyperlinkListener() { @Override public void hyperlinkUpdate(HyperlinkEvent e) { if (e.getEventType().equals(HyperlinkEvent.EventType.ACTIVATED)) try { java.awt.Desktop.getDesktop().browse(e.getURL().toURI()); } catch (Exception ex) { ex.printStackTrace(); } } }); this.setPreferredSize(new Dimension(660, 400)); final Border border = BorderFactory.createCompoundBorder( BorderFactory.createLineBorder(JamelColor.getColor("background"), 5), BorderFactory.createLineBorder(Color.LIGHT_GRAY)); this.setBorder(border); this.setViewportView(jEditorPane); // Smart scrolling final DefaultCaret caret = (DefaultCaret) jEditorPane.getCaret(); caret.setUpdatePolicy(DefaultCaret.NEVER_UPDATE); this.getVerticalScrollBar().addAdjustmentListener(this); } @Override public void adjustmentValueChanged(AdjustmentEvent e) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { // The scroll bar listModel contains information needed to // determine // whether the viewport should be repositioned or not. final JScrollBar scrollBar = (JScrollBar) e.getSource(); final BoundedRangeModel listModel = scrollBar.getModel(); int value = listModel.getValue(); final int extent = listModel.getExtent(); final int maximum = listModel.getMaximum(); final boolean valueChanged = previousValue != value; final boolean maximumChanged = previousMaximum != maximum; // Check if the user has manually repositioned the scrollbar if (valueChanged && !maximumChanged) { adjustScrollBar = value + extent >= maximum; } // Reset the "value" so we can reposition the viewport and // distinguish between a user scroll and a program scroll. // (ie. valueChanged will be false on a program scroll) if (adjustScrollBar) { // Scroll the viewport to the end. scrollBar.removeAdjustmentListener(HtmlPanel.this); value = maximum - extent; scrollBar.setValue(value); scrollBar.addAdjustmentListener(HtmlPanel.this); } previousValue = value; previousMaximum = maximum; } }); } @Override public void update() { final String newText = this.htmlElement.getText(); if (!this.text.equals(newText)) { this.text = newText; SwingUtilities.invokeLater(new Runnable() { @Override public void run() { HtmlPanel.this.jEditorPane.setText(newText); revalidate(); repaint(); } }); } } }