/*
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);
}
}
}