/* GNU GENERAL LICENSE Copyright (C) 2006 The Lobo Project. Copyright (C) 2014 - 2017 Lobo Evolution This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either verion 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General License for more details. You should have received a copy of the GNU General Public along with this program. If not, see <http://www.gnu.org/licenses/>. Contact info: lobochief@users.sourceforge.net; ivan.difrancesco@yahoo.it */ /* * Created on Nov 19, 2005 */ package org.lobobrowser.html.gui; import java.awt.Insets; import java.awt.Rectangle; import java.io.IOException; import java.io.Reader; import java.io.StringReader; import java.util.ArrayList; import javax.swing.JComponent; import javax.swing.SwingUtilities; import javax.swing.Timer; import org.lobobrowser.html.HtmlRendererContext; import org.lobobrowser.html.dombl.DocumentNotificationListener; import org.lobobrowser.html.domimpl.DOMElementImpl; import org.lobobrowser.html.domimpl.DOMNodeImpl; import org.lobobrowser.html.domimpl.HTMLDocumentImpl; import org.lobobrowser.html.parser.DocumentBuilderImpl; import org.lobobrowser.html.parser.InputSourceImpl; import org.lobobrowser.html.renderer.BoundableRenderable; import org.lobobrowser.html.renderer.FrameContext; import org.lobobrowser.html.renderer.NodeRenderer; import org.lobobrowser.html.renderer.RenderableSpot; import org.lobobrowser.html.renderstate.RenderState; import org.lobobrowser.http.UserAgentContext; import org.lobobrowser.util.EventDispatch2; import org.lobobrowser.util.gui.WrapperLayout; import org.lobobrowser.w3c.html.HTMLFrameSetElement; import org.w3c.dom.Document; import org.w3c.dom.Node; import org.w3c.dom.Text; import org.xml.sax.SAXException; /** * The <code>HtmlPanel</code> class is a Swing component that can render a HTML * DOM. It uses either {@link HtmlBlockPanel} or {@link FrameSetPanel} * internally, depending on whether the document is determined to be a FRAMESET * or not. * <p> * Invoke method {@link #setDocument(Document, HtmlRendererContext)} in order to * schedule a document for rendering. */ public class HtmlPanel extends JComponent implements FrameContext { /** The Constant serialVersionUID. */ private static final long serialVersionUID = 1L; /** The selection dispatch. */ private final EventDispatch2 selectionDispatch = new SelectionDispatch(); /** The notification timer. */ private final Timer notificationTimer; /** The notification listener. */ private final DocumentNotificationListener notificationListener; /** The notification immediate action. */ private final Runnable notificationImmediateAction; /** The Constant NOTIF_TIMER_DELAY. */ private static final int NOTIF_TIMER_DELAY = 300; /** The is frame set. */ private volatile boolean isFrameSet = false; /** The node renderer. */ private volatile NodeRenderer nodeRenderer = null; /** The root node. */ private volatile DOMNodeImpl rootNode; /** The preferred width. */ private volatile int preferredWidth = -1; /** The default margin insets. */ private volatile Insets defaultMarginInsets = new Insets(8, 8, 8, 8); /** The default overflow x. */ private volatile int defaultOverflowX = RenderState.OVERFLOW_AUTO; /** The default overflow y. */ private volatile int defaultOverflowY = RenderState.OVERFLOW_SCROLL; /** The html block panel. */ protected volatile HtmlBlockPanel htmlBlockPanel; /** The frame set panel. */ protected volatile FrameSetPanel frameSetPanel; /** * Constructs an <code>HtmlPanel</code>. */ public HtmlPanel() { super(); this.setLayout(WrapperLayout.getInstance()); this.setOpaque(false); this.notificationTimer = new Timer(NOTIF_TIMER_DELAY, new NotificationTimerAction()); this.notificationTimer.setRepeats(false); this.notificationListener = new LocalDocumentNotificationListener(); this.notificationImmediateAction = new Runnable() { @Override public void run() { processNotifications(); } }; } /** Sets the preferred width. * * @param width * the new preferred width */ public void setPreferredWidth(int width) { this.preferredWidth = width; HtmlBlockPanel htmlBlock = this.htmlBlockPanel; if (htmlBlock != null) { htmlBlock.setPreferredWidth(width); } } /** * If the current document is not a FRAMESET, this method scrolls the body * area to the given location. * <p> * This method should be called from the GUI thread. * * @param bounds * The bounds in the scrollable block area that should become * visible. * @param xIfNeeded * If this parameter is true, scrolling will only occur if the * requested bounds are not currently visible horizontally. * @param yIfNeeded * If this parameter is true, scrolling will only occur if the * requested bounds are not currently visible vertically. */ public void scrollTo(Rectangle bounds, boolean xIfNeeded, boolean yIfNeeded) { HtmlBlockPanel htmlBlock = this.htmlBlockPanel; if (htmlBlock != null) { htmlBlock.scrollTo(bounds, xIfNeeded, yIfNeeded); } } /** * Scrolls the body area to the node given, if it is part of the current * document. * <p> * This method should be called from the GUI thread. * * @param node * A DOM node. */ public void scrollTo(Node node) { HtmlBlockPanel htmlBlock = this.htmlBlockPanel; if (htmlBlock != null) { htmlBlock.scrollTo(node); } } /** Gets the block renderable. * * @return the block renderable */ public BoundableRenderable getBlockRenderable() { HtmlBlockPanel htmlBlock = this.htmlBlockPanel; return htmlBlock == null ? null : htmlBlock.getRootRenderable(); } /** Gets the frame set panel. * * @return the frame set panel */ public FrameSetPanel getFrameSetPanel() { int componentCount = this.getComponentCount(); if (componentCount == 0) { return null; } Object c = this.getComponent(0); if (c instanceof FrameSetPanel) { return (FrameSetPanel) c; } return null; } /** * Sets the up as block. * * @param ucontext * the ucontext * @param rcontext * the rcontext */ private void setUpAsBlock(UserAgentContext ucontext, HtmlRendererContext rcontext) { HtmlBlockPanel shp = this.createHtmlBlockPanel(ucontext, rcontext); shp.setPreferredWidth(this.preferredWidth); shp.setDefaultMarginInsets(this.defaultMarginInsets); shp.setDefaultOverflowX(this.defaultOverflowX); shp.setDefaultOverflowY(this.defaultOverflowY); this.htmlBlockPanel = shp; this.frameSetPanel = null; this.removeAll(); this.add(shp); this.nodeRenderer = shp; } /** Sets the up frame set. * * @param fsrn * the new up frame set */ private void setUpFrameSet(DOMNodeImpl fsrn) { this.isFrameSet = true; this.htmlBlockPanel = null; FrameSetPanel fsp = this.createFrameSetPanel(); this.frameSetPanel = fsp; this.nodeRenderer = fsp; this.removeAll(); this.add(fsp); fsp.setRootNode(fsrn); } /** * Method invoked internally to create a {@link HtmlBlockPanel}. It is made * available so it can be overridden. * * @param ucontext * the ucontext * @param rcontext * the rcontext * @return the html block panel */ protected HtmlBlockPanel createHtmlBlockPanel(UserAgentContext ucontext, HtmlRendererContext rcontext) { return new HtmlBlockPanel(java.awt.Color.WHITE, true, ucontext, rcontext, this); } /** * Method invoked internally to create a {@link FrameSetPanel}. It is made * available so it can be overridden. * * @return the frame set panel */ protected FrameSetPanel createFrameSetPanel() { return new FrameSetPanel(); } /** * Scrolls the document such that x and y coordinates are placed in the * upper-left corner of the panel. * <p> * This method may be called outside of the GUI Thread. * * @param x * The x coordinate. * @param y * The y coordinate. */ public void scroll(final int x, final int y) { if (SwingUtilities.isEventDispatchThread()) { this.scrollImpl(x, y); } else { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { scrollImpl(x, y); } }); } } /** * Scroll by. * * @param x * the x * @param y * the y */ public void scrollBy(final int x, final int y) { if (SwingUtilities.isEventDispatchThread()) { this.scrollByImpl(x, y); } else { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { scrollByImpl(x, y); } }); } } /** * Scroll impl. * * @param x * the x * @param y * the y */ private void scrollImpl(int x, int y) { this.scrollTo(new Rectangle(x, y, 16, 16), false, false); } /** * Scroll by impl. * * @param xOffset * the x offset * @param yOffset * the y offset */ private void scrollByImpl(int xOffset, int yOffset) { HtmlBlockPanel bp = this.htmlBlockPanel; if (bp != null) { bp.scrollBy(xOffset, yOffset); } } /** * Clears the current document if any. If called outside the GUI thread, the * operation will be scheduled to be performed in the GUI thread. */ public void clearDocument() { if (SwingUtilities.isEventDispatchThread()) { this.clearDocumentImpl(); } else { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { HtmlPanel.this.clearDocumentImpl(); } }); } } /** * Clear document impl. */ private void clearDocumentImpl() { HTMLDocumentImpl prevDocument = (HTMLDocumentImpl) this.rootNode; if (prevDocument != null) { prevDocument .removeDocumentNotificationListener(this.notificationListener); } NodeRenderer nr = this.nodeRenderer; if (nr != null) { nr.setRootNode(null); } this.rootNode = null; this.htmlBlockPanel = null; this.nodeRenderer = null; this.isFrameSet = false; this.removeAll(); this.revalidate(); this.repaint(); } /** * Sets an HTML DOM node and invalidates the component so it is rendered as * soon as possible in the GUI thread. * <p> * If this method is called from a thread that is not the GUI dispatch * thread, the document is scheduled to be set later. Note that * {@link #setPreferredWidth(int) preferred size} calculations should be * done in the GUI dispatch thread for this reason. * * @param node * This should normally be a Document instance obtained with * {@link org.lobobrowser.html.parser.DocumentBuilderImpl}. * <p> * @param rcontext * A renderer context. * @see org.lobobrowser.html.parser.DocumentBuilderImpl#parse(org.xml.sax.InputSource) * @see org.lobobrowser.html.test.SimpleHtmlRendererContext */ public void setDocument(final Document node, final HtmlRendererContext rcontext) { if (SwingUtilities.isEventDispatchThread()) { this.setDocumentImpl(node, rcontext); } else { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { HtmlPanel.this.setDocumentImpl(node, rcontext); } }); } } /** * Scrolls to the element identified by the given ID in the current * document. * <p> * If this method is invoked outside the GUI thread, the operation is * scheduled to be performed as soon as possible in the GUI thread. * * @param nameOrId * The name or ID of the element in the document. */ public void scrollToElement(final String nameOrId) { if (SwingUtilities.isEventDispatchThread()) { this.scrollToElementImpl(nameOrId); } else { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { scrollToElementImpl(nameOrId); } }); } } /** * Scroll to element impl. * * @param nameOrId * the name or id */ private void scrollToElementImpl(String nameOrId) { DOMNodeImpl node = this.rootNode; if (node instanceof HTMLDocumentImpl) { HTMLDocumentImpl doc = (HTMLDocumentImpl) node; org.w3c.dom.Element element = doc.getElementById(nameOrId); if (element != null) { this.scrollTo(element); } } } /** * Sets the document impl. * * @param node * the node * @param rcontext * the rcontext */ private void setDocumentImpl(Document node, HtmlRendererContext rcontext) { // Expected to be called in the GUI thread. if (!(node instanceof HTMLDocumentImpl)) { throw new IllegalArgumentException( "Only nodes of type HTMLDocumentImpl are currently supported. Use DocumentBuilderImpl."); } HTMLDocumentImpl prevDocument = (HTMLDocumentImpl) this.rootNode; if (prevDocument != null) { prevDocument .removeDocumentNotificationListener(this.notificationListener); } HTMLDocumentImpl nodeImpl = (HTMLDocumentImpl) node; nodeImpl.addDocumentNotificationListener(this.notificationListener); this.rootNode = nodeImpl; DOMNodeImpl fsrn = this.getFrameSetRootNode(nodeImpl); boolean newIfs = fsrn != null; if ((newIfs != this.isFrameSet) || (this.getComponentCount() == 0)) { this.isFrameSet = newIfs; if (newIfs) { this.setUpFrameSet(fsrn); } else { this.setUpAsBlock(rcontext.getUserAgentContext(), rcontext); } } NodeRenderer nr = this.nodeRenderer; if (nr != null) { // These subcomponents should take care // of revalidation. if (newIfs) { nr.setRootNode(fsrn); } else { nr.setRootNode(nodeImpl); } } else { this.invalidate(); this.validate(); this.repaint(); } } /** * Renders HTML given as a string. * * @param htmlSource * The HTML source code. * @param uri * A base URI used to resolve item URIs. * @param rcontext * The {@link HtmlRendererContext} instance. * @see org.lobobrowser.html.test.SimpleHtmlRendererContext * @see #setDocument(Document, HtmlRendererContext) */ public void setHtml(String htmlSource, String uri, HtmlRendererContext rcontext) { try { DocumentBuilderImpl builder = new DocumentBuilderImpl( rcontext.getUserAgentContext(), rcontext); Reader reader = new StringReader(htmlSource); try { InputSourceImpl is = new InputSourceImpl(reader, uri); Document document = builder.parse(is); this.setDocument(document, rcontext); } finally { reader.close(); } } catch (IOException ioe) { throw new IllegalStateException("Unexpected condition.", ioe); } catch (SAXException se) { throw new IllegalStateException("Unexpected condition.", se); } } /** Gets the root node. * * @return the root node */ public DOMNodeImpl getRootNode() { return this.rootNode; } /** * Reset if frame set. * * @return true, if successful */ private boolean resetIfFrameSet() { DOMNodeImpl dOMNodeImpl = this.rootNode; DOMNodeImpl fsrn = this.getFrameSetRootNode(dOMNodeImpl); boolean newIfs = fsrn != null; if ((newIfs != this.isFrameSet) || (this.getComponentCount() == 0)) { this.isFrameSet = newIfs; if (newIfs) { this.setUpFrameSet(fsrn); NodeRenderer nr = this.nodeRenderer; nr.setRootNode(fsrn); // Set proper bounds and repaint. this.validate(); this.repaint(); return true; } } return false; } /** * Gets the frame set root node. * * @param node * the node * @return the frame set root node */ private DOMNodeImpl getFrameSetRootNode(DOMNodeImpl node) { if (node instanceof Document) { DOMElementImpl element = (DOMElementImpl) ((Document) node) .getDocumentElement(); if ((element != null) && "HTML".equalsIgnoreCase(element.getTagName())) { return this.getFrameSet(element); } else { return this.getFrameSet(node); } } else { return null; } } /** * Gets the frame set. * * @param node * the node * @return the frame set */ private DOMNodeImpl getFrameSet(DOMNodeImpl node) { DOMNodeImpl[] children = node.getChildrenArray(); if (children == null) { return null; } int length = children.length; DOMNodeImpl frameSet = null; for (int i = 0; i < length; i++) { DOMNodeImpl child = children[i]; if (child instanceof Text) { // Ignore } else if (child instanceof DOMElementImpl) { String tagName = child.getNodeName(); if ("HEAD".equalsIgnoreCase(tagName) || "NOFRAMES".equalsIgnoreCase(tagName) || "TITLE".equalsIgnoreCase(tagName) || "META".equalsIgnoreCase(tagName) || "SCRIPT".equalsIgnoreCase(tagName) || "NOSCRIPT".equalsIgnoreCase(tagName)) { // ignore it } else if ("FRAMESET".equalsIgnoreCase(tagName)) { frameSet = child; break; } else { if (this.hasSomeHtml((DOMElementImpl) child)) { return null; } } } } return frameSet; } /** * Checks for some html. * * @param element * the element * @return true, if successful */ private boolean hasSomeHtml(DOMElementImpl element) { String tagName = element.getTagName(); if ("HEAD".equalsIgnoreCase(tagName) || "TITLE".equalsIgnoreCase(tagName) || "META".equalsIgnoreCase(tagName)) { return false; } DOMNodeImpl[] children = element.getChildrenArray(); if (children != null) { int length = children.length; for (int i = 0; i < length; i++) { DOMNodeImpl child = children[i]; if (child instanceof Text) { String textContent = ((Text) child).getTextContent(); if ((textContent != null) && !"".equals(textContent.trim())) { return false; } } else if (child instanceof DOMElementImpl) { if (this.hasSomeHtml((DOMElementImpl) child)) { return false; } } } } return true; } /** * Internal method used to expand the selection to the given point. * <p> * Note: This method should be invoked in the GUI thread. * * @param rpoint * the rpoint */ @Override public void expandSelection(RenderableSpot rpoint) { HtmlBlockPanel block = this.htmlBlockPanel; if (block != null) { block.setSelectionEnd(rpoint); block.repaint(); this.selectionDispatch.fireEvent(new SelectionChangeEvent(this, block.isSelectionAvailable())); } } /** * Internal method used to reset the selection so that it is empty at the * given point. This is what is called when the user clicks on a point in * the document. * <p> * Note: This method should be invoked in the GUI thread. * * @param rpoint * the rpoint */ @Override public void resetSelection(RenderableSpot rpoint) { HtmlBlockPanel block = this.htmlBlockPanel; if (block != null) { block.setSelectionStart(rpoint); block.setSelectionEnd(rpoint); block.repaint(); } this.selectionDispatch.fireEvent(new SelectionChangeEvent(this, false)); } /** Gets the selection text. * * @return the selection text */ public String getSelectionText() { HtmlBlockPanel block = this.htmlBlockPanel; if (block == null) { return null; } else { return block.getSelectionText(); } } /** Gets the selection node. * * @return the selection node */ public Node getSelectionNode() { HtmlBlockPanel block = this.htmlBlockPanel; if (block == null) { return null; } else { return block.getSelectionNode(); } } /** * Returns true only if the current block has a selection. This method has * no effect in FRAMESETs at the moment. * * @return true, if successful */ public boolean hasSelection() { HtmlBlockPanel block = this.htmlBlockPanel; if (block == null) { return false; } else { return block.hasSelection(); } } /** * Copies the current selection, if any, into the clipboard. This method has * no effect in FRAMESETs at the moment. * * @return true, if successful */ public boolean copy() { HtmlBlockPanel block = this.htmlBlockPanel; if (block != null) { return block.copy(); } else { return false; } } /** * Adds listener of selection changes. Note that it does not have any effect * on FRAMESETs. * * @param listener * An instance of {@link SelectionChangeListener}. */ public void addSelectionChangeListener(SelectionChangeListener listener) { this.selectionDispatch.addListener(listener); } /** * Removes a listener of selection changes that was previously added. * * @param listener * the listener */ public void removeSelectionChangeListener(SelectionChangeListener listener) { selectionDispatch.removeListener(listener); } /** Sets the default margin insets. * * @param insets * the new default margin insets */ public void setDefaultMarginInsets(Insets insets) { this.defaultMarginInsets = insets; HtmlBlockPanel block = this.htmlBlockPanel; if (block != null) { block.setDefaultMarginInsets(insets); } } /** Sets the default overflow x. * * @param overflow * the new default overflow x */ public void setDefaultOverflowX(int overflow) { this.defaultOverflowX = overflow; HtmlBlockPanel block = this.htmlBlockPanel; if (block != null) { block.setDefaultOverflowX(overflow); } } /** Sets the default overflow y. * * @param overflow * the new default overflow y */ public void setDefaultOverflowY(int overflow) { this.defaultOverflowY = overflow; HtmlBlockPanel block = this.htmlBlockPanel; if (block != null) { block.setDefaultOverflowY(overflow); } } /** The notifications. */ private ArrayList<DocumentNotification> notifications = new ArrayList<DocumentNotification>( 1); /** * Adds the notification. * * @param notification * the notification */ public void addNotification(DocumentNotification notification) { // This can be called in a random thread. ArrayList<DocumentNotification> notifs = this.notifications; synchronized (notifs) { notifs.add(notification); } if (SwingUtilities.isEventDispatchThread()) { // In this case we want the notification to be processed // immediately. However, we don't want potential recursions // to occur when a Javascript property is set in the GUI thread. // Additionally, many property values may be set in one // event block. SwingUtilities.invokeLater(this.notificationImmediateAction); } else { this.notificationTimer.restart(); } } /** * Invalidates the layout of the given node and schedules it to be layed out * later. Multiple invalidations may be processed in a single document * layout. * * @param node * the node */ @Override public void delayedRelayout(DOMNodeImpl node) { ArrayList<DocumentNotification> notifs = this.notifications; synchronized (notifs) { notifs.add(new DocumentNotification(DocumentNotification.SIZE, node)); } this.notificationTimer.restart(); } /** * Process notifications. */ public void processNotifications() { // This is called in the GUI thread. ArrayList<DocumentNotification> notifs = this.notifications; DocumentNotification[] notifsArray; synchronized (notifs) { int size = notifs.size(); if (size == 0) { return; } notifsArray = new DocumentNotification[size]; notifsArray = notifs.toArray(notifsArray); notifs.clear(); } int length = notifsArray.length; for (int i = 0; i < length; i++) { DocumentNotification dn = notifsArray[i]; if ((dn.node instanceof HTMLFrameSetElement) && (this.htmlBlockPanel != null)) { if (this.resetIfFrameSet()) { // Revalidation already taken care of. return; } } } HtmlBlockPanel blockPanel = this.htmlBlockPanel; if (blockPanel != null) { blockPanel.processDocumentNotifications(notifsArray); } FrameSetPanel frameSetPanel = this.frameSetPanel; if (frameSetPanel != null) { frameSetPanel.processDocumentNotifications(notifsArray); } } }