/* GNU LESSER GENERAL PUBLIC LICENSE Copyright (C) 2006 The Lobo Project This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA Contact info: lobochief@users.sourceforge.net */ /* * Created on Apr 16, 2005 */ package org.lobobrowser.html.gui; import java.awt.Adjustable; import java.awt.Color; import java.awt.Component; import java.awt.Container; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.GraphicsEnvironment; import java.awt.Insets; import java.awt.Point; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.Toolkit; import java.awt.datatransfer.Clipboard; import java.awt.datatransfer.ClipboardOwner; import java.awt.datatransfer.StringSelection; import java.awt.datatransfer.Transferable; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import java.awt.event.MouseWheelEvent; import java.awt.event.MouseWheelListener; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.JComponent; import javax.swing.KeyStroke; import javax.swing.SwingUtilities; import org.eclipse.jdt.annotation.Nullable; import org.lobobrowser.html.HtmlRendererContext; import org.lobobrowser.html.domimpl.HTMLDocumentImpl; import org.lobobrowser.html.domimpl.HTMLElementImpl; import org.lobobrowser.html.domimpl.ModelNode; import org.lobobrowser.html.domimpl.NodeImpl; import org.lobobrowser.html.domimpl.UINode; import org.lobobrowser.html.renderer.BoundableRenderable; import org.lobobrowser.html.renderer.DelayedPair; import org.lobobrowser.html.renderer.FrameContext; import org.lobobrowser.html.renderer.NodeRenderer; import org.lobobrowser.html.renderer.PositionedRenderable; import org.lobobrowser.html.renderer.RBlock; import org.lobobrowser.html.renderer.RCollection; import org.lobobrowser.html.renderer.RElement; import org.lobobrowser.html.renderer.Renderable; import org.lobobrowser.html.renderer.RenderableContainer; import org.lobobrowser.html.renderer.RenderableSpot; import org.lobobrowser.html.renderer.TranslatedRenderable; import org.lobobrowser.html.style.RenderState; import org.lobobrowser.ua.UserAgentContext; import org.lobobrowser.util.Nodes; import org.lobobrowser.util.gui.ColorFactory; import org.w3c.dom.Node; /** * A Swing component that renders a HTML block, given by a DOM root or an * internal element, typically a DIV. This component <i>cannot</i> render * FRAMESETs. <code>HtmlBlockPanel</code> is used by {@link HtmlPanel} whenever * the DOM is determined <i>not</i> to be a FRAMESET. * * @see HtmlPanel * @see FrameSetPanel * @author J. H. S. */ public class HtmlBlockPanel extends JComponent implements NodeRenderer, RenderableContainer, ClipboardOwner { private static final long serialVersionUID = 7851587340938903001L; private static final Logger logger = Logger.getLogger(HtmlBlockPanel.class.getName()); private static final boolean loggableInfo = logger.isLoggable(Level.INFO); protected final FrameContext frameContext; protected final UserAgentContext ucontext; protected final HtmlRendererContext rcontext; protected RenderableSpot startSelection; protected RenderableSpot endSelection; protected RBlock rblock; protected int preferredWidth = -1; protected int defaultOverflowX = RenderState.OVERFLOW_AUTO; protected int defaultOverflowY = RenderState.OVERFLOW_SCROLL; private volatile boolean scrollCompleted = false; public HtmlBlockPanel(final UserAgentContext pcontext, final HtmlRendererContext rcontext, final FrameContext frameContext) { this(ColorFactory.TRANSPARENT, false, pcontext, rcontext, frameContext); } public HtmlBlockPanel(final Color background, final boolean opaque, final UserAgentContext pcontext, final HtmlRendererContext rcontext, final FrameContext frameContext) { this.setLayout(null); this.setAutoscrolls(true); this.frameContext = frameContext; this.ucontext = pcontext; this.rcontext = rcontext; this.setOpaque(opaque); this.setBackground(background); final ActionListener actionListener = new ActionListener() { public void actionPerformed(final ActionEvent e) { final String command = e.getActionCommand(); if ("copy".equals(command)) { copy(); } } }; if (!GraphicsEnvironment.isHeadless()) { this.registerKeyboardAction(actionListener, "copy", KeyStroke.getKeyStroke(KeyEvent.VK_COPY, 0), JComponent.WHEN_FOCUSED); this.registerKeyboardAction(actionListener, "copy", KeyStroke.getKeyStroke(KeyEvent.VK_C, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()), JComponent.WHEN_FOCUSED); } this.addMouseListener(new MouseListener() { public void mouseClicked(final MouseEvent e) { onMouseClick(e); } public void mouseEntered(final MouseEvent e) { } public void mouseExited(final MouseEvent e) { onMouseExited(e); } public void mousePressed(final MouseEvent e) { onMousePressed(e); } public void mouseReleased(final MouseEvent e) { onMouseReleased(e); } }); this.addMouseMotionListener(new MouseMotionListener() { /* * (non-Javadoc) * * @see * java.awt.event.MouseMotionListener#mouseDragged(java.awt.event.MouseEvent * ) */ public void mouseDragged(final MouseEvent e) { onMouseDragged(e); } /* * (non-Javadoc) * * @see * java.awt.event.MouseMotionListener#mouseMoved(java.awt.event.MouseEvent * ) */ public void mouseMoved(final MouseEvent arg0) { onMouseMoved(arg0); } }); this.addMouseWheelListener(new MouseWheelListener() { public void mouseWheelMoved(final MouseWheelEvent e) { onMouseWheelMoved(e); } }); } /** * 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(final Rectangle bounds, final boolean xIfNeeded, final boolean yIfNeeded) { final @Nullable HTMLDocumentImpl doc = (HTMLDocumentImpl) getRootNode(); if (doc != null) { RBlock bodyBlock = (RBlock)((HTMLElementImpl) doc.getBody()).getUINode(); bodyBlock.scrollTo(bounds, xIfNeeded, yIfNeeded); } } public void scrollBy(final int xOffset, final int yOffset) { final RBlock block = this.rblock; if (block != null) { if (xOffset != 0) { block.scrollBy(Adjustable.HORIZONTAL, xOffset); } if (yOffset != 0) { block.scrollBy(Adjustable.VERTICAL, yOffset); } } } /** * 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(final Node node) { final Rectangle bounds = this.getNodeBounds(node, true); if (bounds == null) { return; } this.scrollTo(bounds, true, false); } /** * Gets the rectangular bounds of the given node. * <p> * This method should be called from the GUI thread. * * @param node * A node in the current document. * @param relativeToScrollable * Whether the bounds should be relative to the scrollable body area. * Otherwise, they are relative to the root block (which is the * essentially the same as being relative to this * <code>HtmlBlockPanel</code> minus Swing borders). */ public Rectangle getNodeBounds(final Node node, final boolean relativeToScrollable) { final RBlock block = this.rblock; if (block == null) { return null; } // Find UINode first Node currentNode = node; UINode uiNode = null; while (currentNode != null) { if (currentNode instanceof HTMLElementImpl) { final HTMLElementImpl element = (HTMLElementImpl) currentNode; uiNode = element.getUINode(); if (uiNode != null) { break; } } currentNode = currentNode.getParentNode(); } if (uiNode == null) { return null; } final RCollection relativeTo = relativeToScrollable ? (RCollection) block.getRBlockViewport() : (RCollection) block; if (node == currentNode) { final BoundableRenderable br = (BoundableRenderable) uiNode; final Point guiPoint = br.getOriginRelativeTo(relativeTo); final Dimension size = br.getSize(); return new Rectangle(guiPoint, size); } else { return this.scanNodeBounds((RCollection) uiNode, node, relativeTo); } } /** * Gets an aggregate of the bounds of renderer leaf nodes. */ private Rectangle scanNodeBounds(final RCollection root, final Node node, final RCollection relativeTo) { final Iterator<? extends Renderable> i = root.getRenderables(false); Rectangle resultBounds = null; BoundableRenderable prevBoundable = null; if (i != null) { while (i.hasNext()) { final Renderable rn = i.next(); final Renderable r = rn instanceof PositionedRenderable ? (((PositionedRenderable)rn).renderable) : rn; Rectangle subBounds = null; if (r instanceof RCollection) { final RCollection rc = (RCollection) r; prevBoundable = rc; subBounds = this.scanNodeBounds(rc, node, relativeTo); } else if (r instanceof BoundableRenderable) { final BoundableRenderable br = (BoundableRenderable) r; prevBoundable = br; if (Nodes.isSameOrAncestorOf(node, (Node) r.getModelNode())) { final Point origin = br.getOriginRelativeTo(relativeTo); final Dimension size = br.getSize(); subBounds = new Rectangle(origin, size); } } else { // This would have to be a RStyleChanger. We rely on these // when the target node has blank content. if (Nodes.isSameOrAncestorOf(node, (Node) r.getModelNode())) { final int xInRoot = prevBoundable == null ? 0 : prevBoundable.getVisualX() + prevBoundable.getVisualWidth(); final Point rootOrigin = root.getOriginRelativeTo(relativeTo); subBounds = new Rectangle(rootOrigin.x + xInRoot, rootOrigin.y, 0, root.getVisualHeight()); } } if (subBounds != null) { if (resultBounds == null) { resultBounds = subBounds; } else { resultBounds = subBounds.union(resultBounds); } } } } return resultBounds; } public BoundableRenderable getRootRenderable() { return this.rblock; } /** * Allows {@link #getPreferredSize()} to render the HTML block in order to * determine the preferred size of this component. Note that * <code>getPreferredSize()<code> is a potentially time-consuming * operation if the preferred width is set. * * @param width * The preferred blocked width. Use <code>-1</code> to unset. */ public void setPreferredWidth(final int width) { this.preferredWidth = width; } /** * If the preferred size has been set with * {@link #setPreferredSize(Dimension)}, then that size is returned. Otherwise * a preferred size is calculated by rendering the HTML DOM, provided one is * available and a preferred width other than <code>-1</code> has been set * with {@link #setPreferredWidth(int)}. An arbitrary preferred size is * returned in other scenarios. */ @Override public Dimension getPreferredSize() { // Expected to be invoked in the GUI thread. if (this.isPreferredSizeSet()) { return super.getPreferredSize(); } final int pw = this.preferredWidth; if (pw != -1) { final RBlock block = this.rblock; if (block != null) { // Layout should always be done in the GUI thread. if (SwingUtilities.isEventDispatchThread()) { block.layout(pw, 0, false, false, RenderState.OVERFLOW_VISIBLE, RenderState.OVERFLOW_VISIBLE, true); } else { try { SwingUtilities.invokeAndWait(new Runnable() { public void run() { block.layout(pw, 0, false, false, RenderState.OVERFLOW_VISIBLE, RenderState.OVERFLOW_VISIBLE, true); } }); } catch (final Exception err) { logger.log(Level.SEVERE, "Unable to do preferred size layout.", err); } } // Adjust for permanent vertical scrollbar. final int newPw = Math.max(block.width + block.getVScrollBarWidth(), pw); return new Dimension(newPw, block.height); } } return new Dimension(600, 400); } @Override public void finalize() throws Throwable { super.finalize(); } public boolean copy() { final String selection = HtmlBlockPanel.this.getSelectionText(); if (selection != null) { final Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); clipboard.setContents(new StringSelection(selection), HtmlBlockPanel.this); return true; } else { return false; } } public int getFirstLineHeight() { final RBlock block = this.rblock; return block == null ? 0 : block.getFirstLineHeight(); } public void setSelectionEnd(final RenderableSpot rpoint) { this.endSelection = rpoint; } public void setSelectionStart(final RenderableSpot rpoint) { this.startSelection = rpoint; } public boolean isSelectionAvailable() { final RenderableSpot start = this.startSelection; final RenderableSpot end = this.endSelection; return (start != null) && (end != null) && !start.equals(end); } public org.w3c.dom.Node getSelectionNode() { final RenderableSpot start = this.startSelection; final RenderableSpot end = this.endSelection; if ((start != null) && (end != null)) { return Nodes.getCommonAncestor((Node) start.renderable.getModelNode(), (Node) end.renderable.getModelNode()); } else { return null; } } /** * Sets the root node to render. This method should be invoked in the GUI * dispatch thread. */ public void setRootNode(final NodeImpl node) { scrollCompleted = false; layoutCompleted = new CompletableFuture<>(); if (node != null) { final RBlock block = new RBlock(node, 0, this.ucontext, this.rcontext, this.frameContext, this); block.setDefaultOverflowX(this.defaultOverflowX); block.setDefaultOverflowY(this.defaultOverflowY); node.setUINode(block); this.rblock = block; } else { this.rblock = null; } this.invalidate(); this.validateAll(); this.repaint(); } protected void validateAll() { Component toValidate = this; for (;;) { final Container parent = toValidate.getParent(); if ((parent == null) || parent.isValid()) { break; } toValidate = parent; } toValidate.validate(); } protected void revalidatePanel() { // Called in the GUI thread. this.invalidate(); this.validate(); // TODO: Could be paintImmediately. this.repaint(); } private @Nullable NodeImpl getRootNode() { final RBlock block = this.rblock; return block == null ? null : (NodeImpl) block.getModelNode(); } private void onMouseClick(final MouseEvent event) { // Rely on AWT mouse-click only for double-clicks final RBlock block = this.rblock; if (block != null) { final int clickCount = event.getClickCount(); if (SwingUtilities.isLeftMouseButton(event) && (clickCount > 1)) { // TODO: Double-click must be revised. It generates // a single click via mouse release. final Point point = event.getPoint(); block.onDoubleClick(event, point.x, point.y); } else if (SwingUtilities.isMiddleMouseButton(event) && (clickCount == 1)) { block.onMiddleClick(event, event.getX(), event.getY()); } else if (SwingUtilities.isRightMouseButton(event) && (clickCount == 1)) { block.onRightClick(event, event.getX(), event.getY()); } } } private BoundableRenderable mousePressTarget; private Map<?, ?> desktopHints = null; private void onMousePressed(final MouseEvent event) { this.requestFocus(); final RBlock block = this.rblock; if (block != null) { final Point point = event.getPoint(); this.mousePressTarget = block; final int rx = point.x; final int ry = point.y; block.onMousePressed(event, rx, ry); final RenderableSpot rp = block.getLowestRenderableSpot(rx, ry); if (rp != null) { this.frameContext.resetSelection(rp); } else { this.frameContext.resetSelection(null); } } } private void onMouseReleased(final MouseEvent event) { final RBlock block = this.rblock; if (block != null) { final Point point = event.getPoint(); final int rx = point.x; final int ry = point.y; if (SwingUtilities.isLeftMouseButton(event)) { // TODO: This will be raised twice on a double-click. if (event.isControlDown()) { block.onMiddleClick(event, rx, ry); } else { block.onMouseClick(event, rx, ry); } } else if (SwingUtilities.isRightMouseButton(event)) { block.onRightClick(event, rx, ry); } block.onMouseReleased(event, rx, ry); final BoundableRenderable oldTarget = this.mousePressTarget; if (oldTarget != null) { this.mousePressTarget = null; if (oldTarget != block) { oldTarget.onMouseDisarmed(event); } } } else { this.mousePressTarget = null; } } private void onMouseExited(final MouseEvent event) { final BoundableRenderable oldTarget = this.mousePressTarget; if (oldTarget != null) { this.mousePressTarget = null; oldTarget.onMouseDisarmed(event); } } private Renderable getInnerMostRenderable(final int x, final int y) { final RBlock block = this.rblock; BoundableRenderable r = block.getRenderable(x - block.getVisualX(), y - block.getVisualY()); int xi = x, yi = y; BoundableRenderable inner = null; BoundableRenderable prevR = null; do { if (r instanceof RCollection) { RCollection rc = (RCollection) r; if (prevR != null) { final Point oi = prevR.getOriginRelativeTo(rc); xi -= oi.x; yi -= oi.y; } // xi -= rc.getVisualX(); // yi -= rc.getVisualY(); inner = rc.getRenderable(xi, yi); if (inner != null) { prevR = r; r = inner; } } else { inner = null; } } while (inner != null); return r; } private RBlock getContainingBlock(final Renderable r) { if (r instanceof RBlock) { return (RBlock) r; } else if (r instanceof TranslatedRenderable) { return getContainingBlock(((TranslatedRenderable) r).getChild()); } else if (r == null) { return null; } else if (r instanceof BoundableRenderable) { return getContainingBlock(((BoundableRenderable)r).getParent()); } else { return null; } } private void onMouseWheelMoved(final MouseWheelEvent mwe) { final RBlock block = this.rblock; if (block != null) { switch (mwe.getScrollType()) { case MouseWheelEvent.WHEEL_UNIT_SCROLL: final int factor = mwe.isShiftDown() ? 2 : 1; final int units = mwe.getWheelRotation() * mwe.getScrollAmount() * factor; final Renderable innerMostRenderable = getInnerMostRenderable(mwe.getX(), mwe.getY()); boolean consumed = false; RBlock innerBlock = getContainingBlock(innerMostRenderable); do { if (innerBlock != null) { consumed = innerBlock.scrollByUnits(Adjustable.VERTICAL, units); innerBlock = getContainingBlock(innerBlock.getParent()); } } while ((!consumed) && (innerBlock != null)); break; } } } private void onMouseDragged(final MouseEvent event) { final RBlock block = this.rblock; if (block != null) { final Point point = event.getPoint(); final RenderableSpot rp = block.getLowestRenderableSpot(point.x, point.y); if (rp != null) { this.frameContext.expandSelection(rp); } block.ensureVisible(point); } } private void onMouseMoved(final MouseEvent event) { final RBlock block = this.rblock; if (block != null) { final Point point = event.getPoint(); block.onMouseMoved(event, point.x, point.y, false, null); } } private boolean applyRenderHints = true; void disableRenderHints() { this.applyRenderHints = false; } /* * (non-Javadoc) * * @see javax.swing.JComponent#paintComponent(java.awt.Graphics) */ // protected void paintComponent(Graphics g) { @Override public void paint(final Graphics g) { // We go against Sun's advice and override // paint() instead of paintComponent(). Scrollbars // do not repaint correctly if we use // paintComponent. if (this.isOpaque()) { // Background not painted by default in JComponent. final Rectangle clipBounds = g.getClipBounds(); g.setColor(this.getBackground()); g.fillRect(clipBounds.x, clipBounds.y, clipBounds.width, clipBounds.height); } if (applyRenderHints && g instanceof Graphics2D) { final Graphics2D g2 = (Graphics2D) g; if (desktopHints == null) { desktopHints = (Map<?, ?>) (Toolkit.getDefaultToolkit().getDesktopProperty("awt.font.desktophints")); } if (desktopHints != null) { g2.addRenderingHints(desktopHints); } else { try { g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_GASP); } catch (final NoSuchFieldError e) { g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); } } g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); } final RBlock block = this.rblock; if (block != null) { block.paint(g); // Paint FrameContext selection final RenderableSpot start = this.startSelection; final RenderableSpot end = this.endSelection; if ((start != null) && (end != null) && !start.equals(end)) { block.paintSelection(g, false, start, end); } } // For debugging // drawGrid(g); } private void drawGrid(final Graphics g) { final int GRID_SIZE = 50; final int OFFSET_X = 0; final int OFFSET_Y = 0; // Draw grid for debug final Rectangle clipBounds = g.getClipBounds(); g.setColor(new Color(0, 0, 0, 30)); for (int i = 0; i < clipBounds.width; i+= GRID_SIZE) { g.drawLine(i + OFFSET_X, 0, i + OFFSET_X, clipBounds.height); } for (int j = 0; j < clipBounds.height; j+= GRID_SIZE) { g.drawLine(0, j + OFFSET_Y, clipBounds.width, j + OFFSET_Y); } } @Override public void doLayout() { final NodeImpl rootNode = getRootNode(); if (rootNode instanceof HTMLDocumentImpl) { final HTMLDocumentImpl doc = (HTMLDocumentImpl) rootNode; final boolean layoutBlocked = doc.layoutBlocked.get(); if (layoutBlocked) { return; } // Note: There were issues with this previously. See GH #147 doc.primeNodeData(); } try { final Dimension size = this.getSize(); this.clearComponents(); final RBlock block = this.rblock; if (block != null) { block.layout(size.width, size.height, true, true, null, false); // Only set origin block.setOrigin(0, 0); block.updateWidgetBounds(0, 0); this.updateGUIComponents(); // dumpRndTree(block); if (!scrollCompleted) { scrollCompleted = true; if (rootNode instanceof HTMLDocumentImpl) { final HTMLDocumentImpl doc = (HTMLDocumentImpl) rootNode; final String ref = doc.getDocumentURL().getRef(); if (ref != null && ref.length() > 0) { scrollTo(doc.getElementById(ref)); } } } layoutCompleted.complete(true); } else { if (this.getComponentCount() > 0) { this.removeAll(); } } } catch (final Exception thrown) { logger.log(Level.SEVERE, "Unexpected error in layout engine. Document is " + this.getRootNode(), thrown); } } /** * Implementation of UINode.repaint(). */ public void repaint(final ModelNode modelNode) { // this.rblock.invalidateRenderStyle(); this.repaint(); } public String getSelectionText() { final RenderableSpot start = this.startSelection; final RenderableSpot end = this.endSelection; if ((start != null) && (end != null)) { final StringBuffer buffer = new StringBuffer(); this.rblock.extractSelectionText(buffer, false, start, end); return buffer.toString(); } else { return null; } } public boolean hasSelection() { final RenderableSpot start = this.startSelection; final RenderableSpot end = this.endSelection; if ((start != null) && (end != null) && !start.equals(end)) { return true; } else { return false; } } @Override protected void paintChildren(final Graphics g) { // Overridding with NOP. For various reasons, // the regular mechanism for painting children // needs to be handled by Cobra. } public Color getPaintedBackgroundColor() { return this.isOpaque() ? this.getBackground() : null; } /* * (non-Javadoc) * * @see * java.awt.datatransfer.ClipboardOwner#lostOwnership(java.awt.datatransfer * .Clipboard, java.awt.datatransfer.Transferable) */ public void lostOwnership(final Clipboard arg0, final Transferable arg1) { } public void relayout() { // Expected to be called in the GUI thread. // Renderable branch should be invalidated at this // point, but this GUI component not necessarily. this.revalidatePanel(); } public void invalidateLayoutUpTree() { // Called when renderable branch is invalidated. // We shouldn't do anything here. Changes in renderer // tree do not have any bearing on validity of GUI // component. } public void updateAllWidgetBounds() { this.rblock.updateWidgetBounds(0, 0); } public Point getGUIPoint(final int clientX, final int clientY) { // This is the GUI! return new Point(clientX, clientY); } public void focus() { this.grabFocus(); } private boolean processingDocumentNotification = false; void processDocumentNotifications(final DocumentNotification[] notifications) { // Called in the GUI thread. if (this.processingDocumentNotification) { // This should not be possible. Even if // Javascript modifies the DOM during // parsing, this should be executed in // the GUI thread, not the parser thread. throw new IllegalStateException("Recursive"); } this.processingDocumentNotification = true; try { // Note: It may be assumed that usually only generic // notifications come in batches. Other types // of noitifications probably come one by one. boolean topLayout = false; java.util.ArrayList<RElement> repainters = null; final int length = notifications.length; for (int i = 0; i < length; i++) { final DocumentNotification dn = notifications[i]; final int type = dn.type; switch (type) { case DocumentNotification.GENERIC: case DocumentNotification.SIZE: { final NodeImpl node = dn.node; if (node == null) { // This is all-invalidate (new style sheet) if (loggableInfo) { logger.info("processDocumentNotifications(): Calling invalidateLayoutDeep()."); } this.rblock.invalidateLayoutDeep(); // this.rblock.invalidateRenderStyle(); } else { final UINode uiNode = node.findUINode(); if (uiNode != null) { final RElement relement = (RElement) uiNode; relement.invalidateLayoutUpTree(); relement.invalidateLayoutDeep(); // if(type == DocumentNotification.GENERIC) { // relement.invalidateRenderStyle(); // } } else { if (loggableInfo) { logger.info("processDocumentNotifications(): Unable to find UINode for " + node); } } } topLayout = true; break; } case DocumentNotification.POSITION: { // TODO: Could be more efficient. final NodeImpl node = dn.node; final NodeImpl parent = (NodeImpl) node.getParentNode(); if (parent != null) { final UINode uiNode = parent.findUINode(); if (uiNode != null) { final RElement relement = (RElement) uiNode; relement.invalidateLayoutUpTree(); } } topLayout = true; break; } case DocumentNotification.LOOK: { final NodeImpl node = dn.node; final UINode uiNode = node.findUINode(); if (uiNode != null) { if (repainters == null) { repainters = new ArrayList<>(1); } final RElement relement = (RElement) uiNode; relement.invalidateRenderStyle(); repainters.add(relement); } break; } default: break; } } if (topLayout) { this.revalidatePanel(); } else { if (repainters != null) { final Iterator<RElement> i = repainters.iterator(); while (i.hasNext()) { final RElement element = i.next(); element.repaint(); } } } } finally { this.processingDocumentNotification = false; } } public RenderableContainer getParentContainer() { return null; } public void addDelayedPair(final DelayedPair pair) { throw new UnsupportedOperationException("Delayed pairs are not being handled at this level."); } public Collection<DelayedPair> getDelayedPairs() { throw new UnsupportedOperationException("Delayed pairs are not being handled at this level."); } public void clearDelayedPairs() { throw new UnsupportedOperationException("Delayed pairs are not being handled at this level."); } private Set<Component> components; private void clearComponents() { final Set<Component> c = this.components; if (c != null) { c.clear(); } } public Component addComponent(final Component component) { Set<Component> c = this.components; if (c == null) { c = new HashSet<>(); this.components = c; } if (c.add(component)) { return component; } else { return null; } } private void updateGUIComponents() { // We use this method, instead of removing all components and // adding them back, because removal of components can cause // them to lose focus. final Set<Component> c = this.components; if (c == null) { if (this.getComponentCount() != 0) { this.removeAll(); } } else { // Remove children not in the set. final Set<Component> workingSet = new HashSet<>(); workingSet.addAll(c); int count = this.getComponentCount(); for (int i = 0; i < count;) { final Component component = this.getComponent(i); if (!c.contains(component)) { this.remove(i); count = this.getComponentCount(); } else { i++; workingSet.remove(component); } } // Add components in set that were not previously children. final Iterator<Component> wsi = workingSet.iterator(); while (wsi.hasNext()) { final Component component = wsi.next(); this.add(component); } } } public int getDefaultOverflowX() { return defaultOverflowX; } public void setDefaultOverflowX(final int defaultOverflowX) { if (defaultOverflowX != this.defaultOverflowX) { this.defaultOverflowX = defaultOverflowX; final RBlock block = this.rblock; if (block != null) { block.setDefaultOverflowX(defaultOverflowX); block.relayoutIfValid(); } } } public int getDefaultOverflowY() { return defaultOverflowY; } public void setDefaultOverflowY(final int defaultOverflowY) { if (this.defaultOverflowY != defaultOverflowY) { this.defaultOverflowY = defaultOverflowY; final RBlock block = this.rblock; if (block != null) { block.setDefaultOverflowY(defaultOverflowY); block.relayoutIfValid(); } } } @Override public Insets getInsets(final boolean hscroll, final boolean vscroll) { throw new UnsupportedOperationException( "Method added while implementing absolute positioned elements inside relative elements. But not implemented yet."); } @Override public Insets getInsetsMarginBorder(final boolean hscroll, final boolean vscroll) { throw new UnsupportedOperationException("Method added while fixing #32. Not implemented yet."); } @Override public int getVisualHeight() { return rblock.getVisualHeight(); } @Override public int getVisualWidth() { return rblock.getVisualWidth(); } @Override public Rectangle getVisualBounds() { return new Rectangle(getX(), getY(), getVisualWidth(), getVisualHeight()); } public java.awt.Point translateDescendentPoint(BoundableRenderable descendent, int x, int y) { return rblock.translateDescendentPoint(descendent, x, y); } @Override public Point getOriginRelativeTo(RCollection bodyLayout) { // TODO Auto-generated method stub return null; } @Override public Point getOriginRelativeToAbs(RCollection bodyLayout) { // TODO Auto-generated method stub return null; } @SuppressWarnings("unused") private static void dumpRndTree(final Renderable root) { System.out.println("------------------------------"); RBlock.dumpRndTree("", true, root, true); System.out.println("------------------------------"); } private CompletableFuture<Boolean> layoutCompleted = new CompletableFuture<>(); public Future<Boolean> layoutCompletion() { return layoutCompleted; } public boolean isReadyToPaint() { final RBlock block = this.rblock; if (block != null) { final HTMLDocumentImpl doc = (HTMLDocumentImpl) block.getModelNode(); return (!doc.getWindow().hasPendingTasks()) && block.isReadyToPaint(); } return false; } }