/* 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 Apr 16, 2005 */ package org.lobobrowser.html.renderer; import java.awt.Adjustable; import java.awt.Color; import java.awt.Font; import java.awt.Graphics; import java.awt.Insets; import java.awt.Point; import java.awt.Rectangle; import java.awt.event.AdjustmentEvent; import java.awt.event.AdjustmentListener; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.awt.image.ImageObserver; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.NoSuchElementException; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import javax.swing.JScrollBar; import org.lobobrowser.html.HtmlRendererContext; import org.lobobrowser.html.dombl.ModelNode; import org.lobobrowser.html.domimpl.DOMNodeImpl; import org.lobobrowser.html.info.FloatingInfo; import org.lobobrowser.html.layout.LayoutKey; import org.lobobrowser.html.layout.LayoutValue; import org.lobobrowser.html.renderstate.BlockRenderState; import org.lobobrowser.html.renderstate.RenderState; import org.lobobrowser.html.style.RenderThreadState; import org.lobobrowser.http.UserAgentContext; import org.lobobrowser.util.Objects; /** * Represents a HTML block in a rendered document, typically a DIV. The root * renderer node is of this type as well. * <p> * Immediately below an <code>RBlock</code> you will find a node of type * {@link RBlockViewport}. */ public class RBlock extends BaseElementRenderable implements RenderableContainer, ImageObserver { /** The Constant logger. */ protected static final Logger logger = LogManager.getLogger(RBlock.class .getName()); /** The Constant loggableInfo. */ private static final boolean loggableInfo = logger.isEnabled(Level.INFO); /** The Constant MAX_CACHE_SIZE. */ private static final int MAX_CACHE_SIZE = 10; /** The frame context. */ protected final FrameContext frameContext; /** The list nesting. */ protected final int listNesting; /** The renderer context. */ protected final HtmlRendererContext rendererContext; /** The body layout. */ protected final RBlockViewport bodyLayout; /** The cached layout. */ protected final Map<LayoutKey, LayoutValue> cachedLayout = Collections.synchronizedMap(new HashMap<LayoutKey, LayoutValue>(5)); /** The start selection. */ protected RenderableSpot startSelection; /** The end selection. */ protected RenderableSpot endSelection; /** The v scroll bar. */ protected JScrollBar vScrollBar; /** The h scroll bar. */ protected JScrollBar hScrollBar; /** The has h scroll bar. */ protected boolean hasHScrollBar = false; /** The has v scroll bar. */ protected boolean hasVScrollBar = false; // Validation-dependent variables... // private Dimension layoutSize = null; /** The default overflow x. */ protected int defaultOverflowX = RenderState.OVERFLOW_NONE; /** The default overflow y. */ protected int defaultOverflowY = RenderState.OVERFLOW_NONE; /** The last layout value. */ private LayoutValue lastLayoutValue = null; /** The last layout key. */ private LayoutKey lastLayoutKey = null; /** * Instantiates a new r block. * * @param modelNode * the model node * @param listNesting * the list nesting * @param pcontext * the pcontext * @param rcontext * the rcontext * @param frameContext * the frame context * @param parentContainer * the parent container */ public RBlock(DOMNodeImpl modelNode, int listNesting, UserAgentContext pcontext, HtmlRendererContext rcontext, FrameContext frameContext, RenderableContainer parentContainer) { super(parentContainer, modelNode, pcontext); this.listNesting = listNesting; this.frameContext = frameContext; this.rendererContext = rcontext; RBlockViewport bl = new RBlockViewport(modelNode, this, this.getViewportListNesting(listNesting), pcontext, rcontext, frameContext, this); this.bodyLayout = bl; bl.setOriginalParent(this); // Initialize origin of RBlockViewport to be as far top-left as // possible. // This will be corrected on first layout. bl.setX(Short.MAX_VALUE); bl.setY(Short.MAX_VALUE); } /** Gets the v scroll bar width. * * @return the v scroll bar width */ public int getVScrollBarWidth() { return SCROLL_BAR_THICKNESS; } /* * (non-Javadoc) * @see java.lang.Object#finalize() */ @Override public void finalize() throws Throwable { super.finalize(); } /* * (non-Javadoc) * @see org.lobobrowser.html.renderer.RElement#getVAlign() */ @Override public int getVAlign() { // Not used return VALIGN_BASELINE; } /** * Ensure visible. * * @param point * the point */ public void ensureVisible(Point point) { RBlockViewport bodyLayout = this.bodyLayout; if (bodyLayout != null) { boolean hscroll = this.hasHScrollBar; boolean vscroll = this.hasVScrollBar; int origX = bodyLayout.x; int origY = bodyLayout.y; Insets insets = this.getInsets(hscroll, vscroll); if (hscroll) { if (point.x < insets.left) { bodyLayout.x += (insets.left - point.x); } else if (point.x > (this.width - insets.right)) { bodyLayout.x -= ((point.x - this.width) + insets.right); } } if (vscroll) { if (point.y < insets.top) { bodyLayout.y += (insets.top - point.y); } else if (point.y > (this.height - insets.bottom)) { bodyLayout.y -= ((point.y - this.height) + insets.bottom); } } if (hscroll || vscroll) { this.correctViewportOrigin(insets, this.width, this.height); if ((origX != bodyLayout.x) || (origY != bodyLayout.y)) { this.resetScrollBars(null); // TODO: This could be paintImmediately. this.repaint(); } } } } /** Gets the h scroll bar. * * @return the h scroll bar */ private JScrollBar getHScrollBar() { JScrollBar sb = this.hScrollBar; if (sb == null) { // Should never go back to null sb = new JScrollBar(Adjustable.HORIZONTAL); sb.addAdjustmentListener(new LocalAdjustmentListener( Adjustable.HORIZONTAL)); this.hScrollBar = sb; } return sb; } /** Gets the v scroll bar. * * @return the v scroll bar */ private JScrollBar getVScrollBar() { JScrollBar sb = this.vScrollBar; if (sb == null) { // Should never go back to null sb = new JScrollBar(Adjustable.VERTICAL); sb.addAdjustmentListener(new LocalAdjustmentListener( Adjustable.VERTICAL)); this.vScrollBar = sb; } return sb; } /** Checks if is overflow visible x. * * @return true, if is overflow visible x */ public final boolean isOverflowVisibleX() { int overflow = this.overflowX; return (overflow == RenderState.OVERFLOW_NONE) || (overflow == RenderState.OVERFLOW_VISIBLE); } /** Checks if is overflow visible y. * * @return true, if is overflow visible y */ public final boolean isOverflowVisibleY() { int overflow = this.overflowY; return (overflow == RenderState.OVERFLOW_NONE) || (overflow == RenderState.OVERFLOW_VISIBLE); } /** Gets the first line height. * * @return the first line height */ public int getFirstLineHeight() { return this.bodyLayout.getFirstLineHeight(); } /** Gets the first baseline offset. * * @return the first baseline offset */ public int getFirstBaselineOffset() { return this.bodyLayout.getFirstBaselineOffset(); } /** Sets the selection end. * * @param rpoint * the new selection end */ public void setSelectionEnd(RenderableSpot rpoint) { this.endSelection = rpoint; } /** Sets the selection start. * * @param rpoint * the new selection start */ public void setSelectionStart(RenderableSpot rpoint) { this.startSelection = rpoint; } /** * Gets the viewport list nesting. * * @param blockNesting * the block nesting * @return the viewport list nesting */ public int getViewportListNesting(int blockNesting) { return blockNesting; } /* * (non-Javadoc) * @see * org.lobobrowser.html.renderer.BaseElementRenderable#paint(java.awt.Graphics) */ @Override public void paint(Graphics g) { RenderState rs = this.modelNode.getRenderState(); if ((rs != null) && (rs.getVisibility() != RenderState.VISIBILITY_VISIBLE)) { // Just don't paint it. return; } boolean linfo = loggableInfo; long time1 = linfo ? System.currentTimeMillis() : 0; this.prePaint(g); long time2 = linfo ? System.currentTimeMillis() : 0; long time3 = 0; try { Insets insets = this.getInsets(this.hasHScrollBar, this.hasVScrollBar); RBlockViewport bodyLayout = this.bodyLayout; if (bodyLayout != null) { int overflowX = this.overflowX; int overflowY = this.overflowY; if (((overflowX == RenderState.OVERFLOW_NONE) || (overflowX == RenderState.OVERFLOW_VISIBLE)) && ((overflowY == RenderState.OVERFLOW_NONE) || (overflowY == RenderState.OVERFLOW_VISIBLE))) { // Simply translate. int bx = bodyLayout.x; int by = bodyLayout.y; g.translate(bx, by); try { bodyLayout.paint(g); } finally { g.translate(-bx, -by); } } else { // Clip when there potential scrolling or hidden overflow // was requested. Graphics newG = g.create(insets.left, insets.top, this.width - insets.left - insets.right, this.height - insets.top - insets.bottom); try { // Second, translate newG.translate(bodyLayout.x - insets.left, bodyLayout.y - insets.top); // Third, paint in clipped + translated region. bodyLayout.paint(newG); } finally { newG.dispose(); } } if (linfo) { time3 = System.currentTimeMillis(); } } else { // nop } // Paint FrameContext selection. // This is only done by root RBlock. RenderableSpot start = this.startSelection; RenderableSpot end = this.endSelection; boolean inSelection = false; if ((start != null) && (end != null) && !start.equals(end)) { this.paintSelection(g, inSelection, start, end); } // Must paint scrollbars too. JScrollBar hsb = this.hScrollBar; if (hsb != null) { Graphics sbg = g.create(insets.left, this.height - insets.bottom, this.width - insets.left - insets.right, SCROLL_BAR_THICKNESS); try { hsb.paint(sbg); } finally { sbg.dispose(); } } JScrollBar vsb = this.vScrollBar; if (vsb != null) { Graphics sbg = g.create(this.width - insets.right, insets.top, SCROLL_BAR_THICKNESS, this.height - insets.top - insets.bottom); try { vsb.paint(sbg); } finally { sbg.dispose(); } } } finally { // Must always call super implementation super.paint(g); } if (linfo) { long time4 = System.currentTimeMillis(); if ((time4 - time1) > 100) { logger.info("paint(): Elapsed: " + (time4 - time1) + " ms. Prepaint: " + (time2 - time1) + " ms. Viewport: " + (time3 - time2) + " ms. RBlock: " + this + "."); } } } /** * Layout. * * @param availWidth * the avail width * @param availHeight * the avail height * @param expandWidth * the expand width * @param expandHeight * the expand height * @param defaultOverflowX * the default overflow x * @param defaultOverflowY * the default overflow y * @param sizeOnly * the size only */ public final void layout(int availWidth, int availHeight, boolean expandWidth, boolean expandHeight, int defaultOverflowX, int defaultOverflowY, boolean sizeOnly) { this.layout(availWidth, availHeight, expandWidth, expandHeight, null, defaultOverflowX, defaultOverflowY, sizeOnly); } /** * Layout. * * @param availWidth * the avail width * @param availHeight * the avail height * @param expandWidth * the expand width * @param expandHeight * the expand height * @param floatBoundsSource * the float bounds source * @param sizeOnly * the size only */ public final void layout(int availWidth, int availHeight, boolean expandWidth, boolean expandHeight, FloatingBoundsSource floatBoundsSource, boolean sizeOnly) { this.layout(availWidth, availHeight, expandWidth, expandHeight, floatBoundsSource, this.defaultOverflowX, this.defaultOverflowY, sizeOnly); } /** * Layout. * * @param availWidth * the avail width * @param availHeight * the avail height * @param expandWidth * the expand width * @param expandHeight * the expand height * @param floatBoundsSource * the float bounds source * @param defaultOverflowX * the default overflow x * @param defaultOverflowY * the default overflow y * @param sizeOnly * the size only */ public final void layout(int availWidth, int availHeight, boolean expandWidth, boolean expandHeight, FloatingBoundsSource floatBoundsSource, int defaultOverflowX, int defaultOverflowY, boolean sizeOnly) { try { this.doLayout(availWidth, availHeight, expandWidth, expandHeight, floatBoundsSource, defaultOverflowX, defaultOverflowY, sizeOnly); } finally { this.layoutUpTreeCanBeInvalidated = true; this.layoutDeepCanBeInvalidated = true; // this.renderStyleCanBeInvalidated = true; } } /* * (non-Javadoc) * @see org.lobobrowser.html.renderer.BaseElementRenderable#doLayout(int, int, * boolean) */ @Override public final void doLayout(int availWidth, int availHeight, boolean sizeOnly) { // This is an override of an abstract method. this.doLayout(availWidth, availHeight, true, false, null, this.defaultOverflowX, this.defaultOverflowY, sizeOnly); } /** * Do layout. * * @param availWidth * the avail width * @param availHeight * the avail height * @param expandWidth * the expand width * @param expandHeight * the expand height * @param floatBoundsSource * the float bounds source * @param defaultOverflowX * the default overflow x * @param defaultOverflowY * the default overflow y * @param sizeOnly * the size only */ public void doLayout(int availWidth, int availHeight, boolean expandWidth, boolean expandHeight, FloatingBoundsSource floatBoundsSource, int defaultOverflowX, int defaultOverflowY, boolean sizeOnly) { this.doLayout(availWidth, availHeight, expandWidth, expandHeight, floatBoundsSource, defaultOverflowX, defaultOverflowY, sizeOnly, true); } /** * Lays out and sets dimensions only if RBlock is invalid (or never before * layed out), if the parameters passed differ from the last layout, or if * the current font differs from the font for the last layout. * * @param availWidth * the avail width * @param availHeight * the avail height * @param expandWidth * the expand width * @param expandHeight * the expand height * @param floatBoundsSource * the float bounds source * @param defaultOverflowX * the default overflow x * @param defaultOverflowY * the default overflow y * @param sizeOnly * the size only * @param useCache * For testing. Should always be true. */ public void doLayout(int availWidth, int availHeight, boolean expandWidth, boolean expandHeight, FloatingBoundsSource floatBoundsSource, int defaultOverflowX, int defaultOverflowY, boolean sizeOnly, boolean useCache) { // Expected to be invoked in the GUI thread. RenderState renderState = this.modelNode.getRenderState(); Font font = renderState == null ? null : renderState.getFont(); int whiteSpace = renderState == null ? RenderState.WS_NORMAL : renderState.getWhiteSpace(); // Having whiteSpace == NOWRAP and having a NOWRAP override // are not exactly the same thing. boolean overrideNoWrap = RenderThreadState.getState().overrideNoWrap; LayoutKey key = new LayoutKey(availWidth, availHeight, expandWidth, expandHeight, floatBoundsSource, defaultOverflowX, defaultOverflowY, whiteSpace, font, overrideNoWrap); Map<LayoutKey, LayoutValue> cachedLayout = this.cachedLayout; LayoutValue value; if (sizeOnly) { value = useCache ? (LayoutValue) cachedLayout.get(key) : null; } else { if (Objects.equals(key, this.lastLayoutKey)) { value = this.lastLayoutValue; } else { value = null; } } if (value == null) { value = this.forceLayout(renderState, availWidth, availHeight, expandWidth, expandHeight, floatBoundsSource, defaultOverflowX, defaultOverflowY, sizeOnly); if (sizeOnly) { this.lastLayoutKey = null; this.lastLayoutValue = null; if (cachedLayout.size() > MAX_CACHE_SIZE) { // Unlikely, but we should keep it bounded. cachedLayout.clear(); } cachedLayout.put(key, value); } else { this.lastLayoutKey = key; this.lastLayoutValue = value; } } this.width = value.getWidth(); this.height = value.getHeight(); this.hasHScrollBar = value.isHasHScrollBar(); this.hasVScrollBar = value.isHasVScrollBar(); bodyLayout.positionDelayed(); // Even if we didn't do layout, the parent is // expected to have removed its GUI components. this.sendGUIComponentsToParent(); // Even if we didn't do layout, the parent is // expected to have removed its delayed pairs. this.sendDelayedPairsToParent(); } /** * Correct viewport origin. * * @param insets * the insets * @param blockWidth * the block width * @param blockHeight * the block height * @return true, if successful */ private final boolean correctViewportOrigin(Insets insets, int blockWidth, int blockHeight) { RBlockViewport bodyLayout = this.bodyLayout; int viewPortX = bodyLayout.x; int viewPortY = bodyLayout.y; boolean corrected = false; if (viewPortX > insets.left) { bodyLayout.x = insets.left; corrected = true; } else if (viewPortX < (blockWidth - insets.right - bodyLayout.width)) { bodyLayout.x = Math.min(insets.left, blockWidth - insets.right - bodyLayout.width); corrected = true; } if (viewPortY > insets.top) { bodyLayout.y = insets.top; corrected = true; } else if (viewPortY < (blockHeight - insets.bottom - bodyLayout.height)) { bodyLayout.y = Math.min(insets.top, blockHeight - insets.bottom - bodyLayout.height); corrected = true; } return corrected; } /** * Lays out the block without checking for prior dimensions. * * @param renderState * the render state * @param availWidth * the avail width * @param availHeight * the avail height * @param expandWidth * the expand width * @param expandHeight * the expand height * @param blockFloatBoundsSource * the block float bounds source * @param defaultOverflowX * the default overflow x * @param defaultOverflowY * the default overflow y * @param sizeOnly * the size only * @return the layout value */ private final LayoutValue forceLayout(RenderState renderState, int availWidth, int availHeight, boolean expandWidth, boolean expandHeight, FloatingBoundsSource blockFloatBoundsSource, int defaultOverflowX, int defaultOverflowY, boolean sizeOnly) { // Expected to be invoked in the GUI thread. // TODO: Not necessary to do full layout if only expandWidth or // expandHeight change (specifically in tables). RenderState rs = renderState; if (rs == null) { rs = new BlockRenderState(null); } // // Clear adjust() cache. // this.cachedAdjust.clear(); // We reprocess the rendering state. // Probably doesn't need to be done in its entirety every time. this.applyStyle(availWidth, availHeight); RBlockViewport bodyLayout = this.bodyLayout; DOMNodeImpl node = (DOMNodeImpl) this.modelNode; if ((node == null) || (bodyLayout == null)) { Insets insets = this.getInsets(false, false); return new LayoutValue(insets.left + insets.right, insets.bottom + insets.top, false, false); } Insets paddingInsets = this.paddingInsets; if (paddingInsets == null) { paddingInsets = RBlockViewport.ZERO_INSETS; } Insets borderInsets = this.borderInsets; if (borderInsets == null) { borderInsets = RBlockViewport.ZERO_INSETS; } Insets marginInsets = this.marginInsets; if (marginInsets == null) { marginInsets = RBlockViewport.ZERO_INSETS; } int paddingTotalWidth = paddingInsets.left + paddingInsets.right; int paddingTotalHeight = paddingInsets.top + paddingInsets.bottom; int overflowX = this.overflowX; if (overflowX == RenderState.OVERFLOW_NONE) { overflowX = defaultOverflowX; } int overflowY = this.overflowY; if (overflowY == RenderState.OVERFLOW_NONE) { overflowY = defaultOverflowY; } boolean vauto = overflowY == RenderState.OVERFLOW_AUTO; boolean hscroll = overflowX == RenderState.OVERFLOW_SCROLL; boolean hauto = overflowX == RenderState.OVERFLOW_AUTO; boolean vscroll = overflowY == RenderState.OVERFLOW_SCROLL; Insets insets = this.getInsets(hscroll, vscroll); int insetsTotalWidth = insets.left + insets.right; int insetsTotalHeight = insets.top + insets.bottom; int actualAvailWidth = availWidth - paddingTotalWidth - insetsTotalWidth; int actualAvailHeight = availHeight - paddingTotalHeight - insetsTotalHeight; Integer dw = this.getDeclaredWidth(renderState, actualAvailWidth); Integer dh = this.getDeclaredHeight(renderState, actualAvailHeight); int declaredWidth = -1; int declaredHeight = -1; if (dw != null) { declaredWidth = dw.intValue(); } if (dh != null) { declaredHeight = dh.intValue(); } // Remove all GUI components previously added by descendents // The RBlockViewport.layout() method is expected to add all of them // back. this.clearGUIComponents(); int tentativeWidth; int tentativeHeight; // Step # 1: If there's no declared width and no width // expansion has been requested, do a preliminary layout // assuming that the scrollable region has width=0 and // there's no wrapping. tentativeWidth = declaredWidth == -1 ? availWidth : declaredWidth + insetsTotalWidth + paddingTotalWidth; tentativeHeight = declaredHeight == -1 ? availHeight : declaredHeight + insetsTotalHeight + paddingTotalHeight; if ((declaredWidth == -1) && !expandWidth && (availWidth > (insetsTotalWidth + paddingTotalWidth))) { RenderThreadState state = RenderThreadState.getState(); boolean prevOverrideNoWrap = state.overrideNoWrap; if (!prevOverrideNoWrap) { state.overrideNoWrap = true; try { int desiredViewportWidth = paddingTotalWidth; int desiredViewportHeight = paddingTotalHeight; bodyLayout.layout(desiredViewportWidth, desiredViewportHeight, paddingInsets, -1, null, true); // If we find that the viewport is not as wide as we // presumed, then we'll use that as a new tentative width. if ((bodyLayout.width + insetsTotalWidth) < tentativeWidth) { tentativeWidth = bodyLayout.width + insetsTotalWidth; tentativeHeight = bodyLayout.height + insetsTotalHeight; } } finally { state.overrideNoWrap = false; } } } // Step # 2: Do a layout with the tentativeWidth (adjusted if Step # 1 // was done), // but in case overflow-y is "auto", then we check for possible // overflow. FloatingBounds viewportFloatBounds = null; FloatingBounds blockFloatBounds = null; if (blockFloatBoundsSource != null) { blockFloatBounds = blockFloatBoundsSource .getChildBlockFloatingBounds(tentativeWidth); viewportFloatBounds = new ShiftedFloatingBounds(blockFloatBounds, -insets.left, -insets.right, -insets.top); } int desiredViewportWidth = tentativeWidth - insetsTotalWidth; int desiredViewportHeight = tentativeHeight - insets.top - insets.bottom; int maxY = vauto ? (declaredHeight == -1 ? -1 : declaredHeight + paddingInsets.top) : -1; try { bodyLayout.layout(desiredViewportWidth, desiredViewportHeight, paddingInsets, maxY, viewportFloatBounds, sizeOnly); } catch (SizeExceededException see) { // Getting this exception means that we need to add a vertical // scrollbar. // Wee need to relayout and adjust insets and widths for scrollbar. vscroll = true; insets = this.getInsets(hscroll, vscroll); insetsTotalWidth = insets.left + insets.right; actualAvailWidth = availWidth - paddingTotalWidth - insetsTotalWidth; dw = this.getDeclaredWidth(renderState, actualAvailWidth); declaredWidth = dw == null ? -1 : dw.intValue(); desiredViewportWidth = tentativeWidth - insetsTotalWidth; if (blockFloatBounds != null) { viewportFloatBounds = new ShiftedFloatingBounds( blockFloatBounds, -insets.left, -insets.right, -insets.top); } bodyLayout.layout(desiredViewportWidth, desiredViewportHeight, paddingInsets, -1, viewportFloatBounds, sizeOnly); } int bodyWidth = bodyLayout.width; int bodyHeight = bodyLayout.height; int prelimBlockWidth = bodyWidth + insetsTotalWidth; int prelimBlockHeight = bodyHeight + insetsTotalHeight; int adjDeclaredWidth = declaredWidth == -1 ? -1 : declaredWidth + insets.left + insets.right + paddingInsets.left + paddingInsets.right; int adjDeclaredHeight = declaredHeight == -1 ? -1 : declaredHeight + insets.top + insets.bottom + paddingInsets.top + paddingInsets.bottom; // Adjust insets and other dimensions base on overflow-y=auto. if (hauto && (((adjDeclaredWidth != -1) && (prelimBlockWidth > adjDeclaredWidth)) || (prelimBlockWidth > tentativeWidth))) { hscroll = true; insets = this.getInsets(hscroll, vscroll); insetsTotalHeight = insets.top + insets.bottom; prelimBlockHeight = bodyHeight + insetsTotalHeight; } boolean visibleX = (overflowX == RenderState.OVERFLOW_VISIBLE) || (overflowX == RenderState.OVERFLOW_NONE); boolean visibleY = (overflowY == RenderState.OVERFLOW_VISIBLE) || (overflowY == RenderState.OVERFLOW_NONE); int resultingWidth; int resultingHeight; if (adjDeclaredWidth == -1) { resultingWidth = expandWidth ? Math.max(prelimBlockWidth, tentativeWidth) : prelimBlockWidth; if (hscroll && (resultingWidth > tentativeWidth)) { resultingWidth = Math.max(tentativeWidth, SCROLL_BAR_THICKNESS); } } else { resultingWidth = visibleX ? Math.max(prelimBlockWidth, adjDeclaredWidth) : adjDeclaredWidth; } if (!sizeOnly) { // Align horizontally now. This may change canvas height. int alignmentXPercent = rs.getAlignXPercent(); if (alignmentXPercent > 0) { // TODO: OPTIMIZATION: alignment should not be done in table // cell // sizing determination. int canvasWidth = Math.max(bodyLayout.width, resultingWidth - insets.left - insets.right); // Alignment is done afterwards because canvas dimensions might // have // changed. bodyLayout .alignX(alignmentXPercent, canvasWidth, paddingInsets); } } if (adjDeclaredHeight == -1) { resultingHeight = expandHeight ? Math.max(prelimBlockHeight, tentativeHeight) : prelimBlockHeight; if (vscroll && (resultingHeight > tentativeHeight)) { resultingHeight = Math.max(tentativeHeight, SCROLL_BAR_THICKNESS); } } else { resultingHeight = visibleY ? Math.max(prelimBlockHeight, adjDeclaredHeight) : adjDeclaredHeight; } if (!sizeOnly) { // Align vertically now int alignmentYPercent = rs.getAlignYPercent(); if (alignmentYPercent > 0) { // TODO: OPTIMIZATION: alignment should not be done in table // cell // sizing determination. int canvasHeight = Math.max(bodyLayout.height, resultingHeight - insets.top - insets.bottom); // Alignment is done afterwards because canvas dimensions might // have // changed. bodyLayout.alignY(alignmentYPercent, canvasHeight, paddingInsets); } } if (vscroll) { JScrollBar sb = this.getVScrollBar(); this.addComponent(sb); // Bounds set by updateWidgetBounds } if (hscroll) { JScrollBar sb = this.getHScrollBar(); this.addComponent(sb); // Bounds set by updateWidgetBounds } if (hscroll || vscroll) { // In this case, viewport origin should not be reset. // We don't want to cause the document to scroll back // up while rendering. this.correctViewportOrigin(insets, resultingWidth, resultingHeight); // Now reset the scrollbar state. Depends // on block width and height. this.width = resultingWidth; this.height = resultingHeight; this.resetScrollBars(rs); } else { bodyLayout.x = insets.left; bodyLayout.y = insets.top; } return new LayoutValue(resultingWidth, resultingHeight, hscroll, vscroll); } /** * Gets the v unit increment. * * @param renderState * the render state * @return the v unit increment */ private int getVUnitIncrement(RenderState renderState) { if (renderState != null) { return renderState.getFontMetrics().getHeight(); } else { return new BlockRenderState(null).getFontMetrics().getHeight(); } } /** The resetting scroll bars. */ private boolean resettingScrollBars = false; /** * Changes scroll bar state to match viewport origin. * * @param renderState * the render state */ private void resetScrollBars(RenderState renderState) { // Expected to be called only in the GUI thread. this.resettingScrollBars = true; try { RBlockViewport bodyLayout = this.bodyLayout; if (bodyLayout != null) { Insets insets = this.getInsets(this.hasHScrollBar, this.hasVScrollBar); JScrollBar vsb = this.vScrollBar; if (vsb != null) { int newValue = insets.top - bodyLayout.y; int newExtent = this.height - insets.top - insets.bottom; int newMin = 0; int newMax = bodyLayout.height; vsb.setValues(newValue, newExtent, newMin, newMax); vsb.setUnitIncrement(this.getVUnitIncrement(renderState)); vsb.setBlockIncrement(newExtent); } JScrollBar hsb = this.hScrollBar; if (hsb != null) { int newValue = insets.left - bodyLayout.x; int newExtent = this.width - insets.left - insets.right; int newMin = 0; int newMax = bodyLayout.width; hsb.setValues(newValue, newExtent, newMin, newMax); } } } finally { this.resettingScrollBars = false; } } /* * (non-Javadoc) * @see org.lobobrowser.html.render.UIControl#paintSelection(java.awt.Graphics, * boolean, org.lobobrowser.html.render.RenderablePoint, * org.lobobrowser.html.render.RenderablePoint) */ @Override public boolean paintSelection(Graphics g, boolean inSelection, RenderableSpot startPoint, RenderableSpot endPoint) { Graphics newG = g.create(); try { Insets insets = this.getInsets(this.hasHScrollBar, this.hasVScrollBar); // Just clip, don't translate. newG.clipRect(insets.left, insets.top, this.width - insets.left - insets.right, this.height - insets.top - insets.bottom); return super .paintSelection(newG, inSelection, startPoint, endPoint); } finally { newG.dispose(); } } /* * (non-Javadoc) * @see org.lobobrowser.html.render.BoundableRenderable#getRenderablePoint(int, * int) */ @Override public RenderableSpot getLowestRenderableSpot(int x, int y) { RBlockViewport bodyLayout = this.bodyLayout; if (bodyLayout != null) { Insets insets = this.getInsets(this.hasHScrollBar, this.hasVScrollBar); if ((x > insets.left) && (x < (this.width - insets.right)) && (y > insets.top) && (y < (this.height - insets.bottom))) { return bodyLayout.getLowestRenderableSpot(x - bodyLayout.x, y - bodyLayout.y); } else { return new RenderableSpot(this, x, y); } } else { return new RenderableSpot(this, x, y); } } /** * RBlocks should only be invalidated if one of their properties change, or * if a descendent changes, or if a style property of an ancestor is such * that it could produce layout changes in this RBlock. */ @Override public void invalidateLayoutLocal() { super.invalidateLayoutLocal(); this.cachedLayout.clear(); this.lastLayoutKey = null; this.lastLayoutValue = null; JScrollBar hScrollBar = this.hScrollBar; if (hScrollBar != null) { // Necessary hScrollBar.invalidate(); } JScrollBar vScrollBar = this.vScrollBar; if (vScrollBar != null) { // Necessary vScrollBar.invalidate(); } } /* * (non-Javadoc) * @see org.lobobrowser.html.renderer.BaseElementRenderable#clearStyle(boolean) */ @Override protected void clearStyle(boolean isRootBlock) { super.clearStyle(isRootBlock); this.overflowX = this.defaultOverflowX; this.overflowY = this.defaultOverflowY; } /* * (non-Javadoc) * @see * org.lobobrowser.html.render.BoundableRenderable#onMouseClick(java.awt.event * .MouseEvent, int, int) */ @Override public boolean onMouseClick(MouseEvent event, int x, int y) { RBlockViewport bodyLayout = this.bodyLayout; if (!HtmlController.getInstance().onMouseClick(this.modelNode, event, bodyLayout, x, y)) { return false; } if (this.backgroundColor != null) { return false; } return true; } /* * (non-Javadoc) * @see * org.lobobrowser.html.renderer.BoundableRenderable#onDoubleClick(java.awt. * event.MouseEvent, int, int) */ @Override public boolean onDoubleClick(MouseEvent event, int x, int y) { RBlockViewport bodyLayout = this.bodyLayout; if (!HtmlController.getInstance().onDoubleClick(this.modelNode, event, bodyLayout, x, y)) { return false; } if (this.backgroundColor != null) { return false; } return true; } /* * (non-Javadoc) * @see org.lobobrowser.html.render.BoundableRenderable#onMouseDisarmed(java.awt * .event.MouseEvent) */ @Override public boolean onMouseDisarmed(MouseEvent event) { BoundableRenderable br = this.armedRenderable; if (br != null) { try { return br.onMouseDisarmed(event); } finally { this.armedRenderable = null; } } else { return true; } } /** The armed renderable. */ private BoundableRenderable armedRenderable; /** Gets the armed renderable. * * @return the armed renderable */ public BoundableRenderable getArmedRenderable() { return armedRenderable; } /** Sets the armed renderable. * * @param armedRenderable * the new armed renderable */ public void setArmedRenderable(BoundableRenderable armedRenderable) { this.armedRenderable = armedRenderable; } /* * (non-Javadoc) * @see org.lobobrowser.html.render.BoundableRenderable#onMousePressed(java.awt. * event.MouseEvent, int, int) */ @Override public boolean onMousePressed(MouseEvent event, int x, int y) { if (!HtmlController.getInstance().onMouseDown(this.modelNode, event, this, x, y)) { return false; } if (this.backgroundColor != null) { return false; } return true; } /* * (non-Javadoc) * @see org.lobobrowser.html.render.BoundableRenderable#onMouseReleased(java.awt * .event.MouseEvent, int, int) */ @Override public boolean onMouseReleased(MouseEvent event, int x, int y) { if (!HtmlController.getInstance().onMouseUp(this.modelNode, event, this, x, y)) { return false; } if (this.backgroundColor != null) { return false; } return true; } @Override public boolean onKeyPressed(KeyEvent event) { if (!HtmlController.getInstance().onKeyPress(this.modelNode, event)) { return false; } return true; } @Override public boolean onKeyUp(KeyEvent event) { if (!HtmlController.getInstance().onKeyUp(this.modelNode, event)) { return false; } return true; } @Override public boolean onKeyDown(KeyEvent event) { if (!HtmlController.getInstance().onKeyDown(this.modelNode, event)) { return false; } return true; } /* * (non-Javadoc) * @see * org.lobobrowser.html.renderer.RenderableContainer#getPaintedBackgroundColor() */ @Override public Color getPaintedBackgroundColor() { return this.backgroundColor; } /* * (non-Javadoc) * @see org.lobobrowser.html.render.RCollection#getRenderables() */ @Override public Iterator<?> getRenderables() { final RBlockViewport bodyLayout = this.bodyLayout; return new Iterator<Object>() { private RBlockViewport bl = bodyLayout; @Override public boolean hasNext() { return bl != null; } @Override public Object next() { if (bl == null) { throw new NoSuchElementException(); } try { return bl; } finally { bl = null; } } @Override public void remove() { throw new UnsupportedOperationException(); } }; } /* * (non-Javadoc) * @see org.lobobrowser.html.dombl.UINode(org.lobobrowser.html.dombl.ModelNode * modelNode); */ @Override public void repaint(ModelNode modelNode) { // this.invalidateRenderStyle(); this.repaint(); } /* * (non-Javadoc) * @see org.lobobrowser.html.renderer.BaseRCollection#updateWidgetBounds(int, * int) */ @Override public void updateWidgetBounds(int guiX, int guiY) { super.updateWidgetBounds(guiX, guiY); boolean hscroll = this.hasHScrollBar; boolean vscroll = this.hasVScrollBar; if (hscroll || vscroll) { Insets insets = this.getInsets(hscroll, vscroll); if (hscroll) { JScrollBar hsb = this.hScrollBar; if (hsb != null) { hsb.setBounds(guiX + insets.left, (guiY + this.height) - insets.bottom, this.width - insets.left - insets.right, SCROLL_BAR_THICKNESS); } } if (vscroll) { JScrollBar vsb = this.vScrollBar; if (vsb != null) { vsb.setBounds((guiX + this.width) - insets.right, guiY + insets.top, SCROLL_BAR_THICKNESS, this.height - insets.top - insets.bottom); } } } } /** * Scroll horizontal to. * * @param newX * the new x */ public void scrollHorizontalTo(int newX) { RBlockViewport bodyLayout = this.bodyLayout; if (bodyLayout != null) { Insets insets = this.getInsets(this.hasHScrollBar, this.hasVScrollBar); int viewPortX = newX; if (viewPortX > insets.left) { bodyLayout.x = insets.left; } else if (viewPortX < (this.width - insets.right - bodyLayout.width)) { bodyLayout.x = Math.min(insets.left, this.width - insets.right - bodyLayout.width); } else { bodyLayout.x = viewPortX; } this.resetScrollBars(null); this.updateWidgetBounds(); this.repaint(); } } /** * Scroll vertical to. * * @param newY * the new y */ public void scrollVerticalTo(int newY) { RBlockViewport bodyLayout = this.bodyLayout; if (bodyLayout != null) { Insets insets = this.getInsets(this.hasHScrollBar, this.hasVScrollBar); int viewPortY = newY; if (viewPortY > insets.top) { bodyLayout.y = insets.top; } else if (viewPortY < (this.height - insets.bottom - bodyLayout.height)) { bodyLayout.y = Math.min(insets.top, this.height - insets.bottom - bodyLayout.height); } else { bodyLayout.y = viewPortY; } this.resetScrollBars(null); this.updateWidgetBounds(); this.repaint(); } } /** * Scroll by units. * * @param orientation * the orientation * @param units * the units */ public void scrollByUnits(int orientation, int units) { int offset = orientation == Adjustable.VERTICAL ? this .getVUnitIncrement(null) * units : units; this.scrollBy(orientation, offset); } /** * Scroll by. * * @param orientation * the orientation * @param offset * the offset */ public void scrollBy(int orientation, int offset) { RBlockViewport bodyLayout = this.bodyLayout; if (bodyLayout != null) { switch (orientation) { case Adjustable.HORIZONTAL: this.scrollHorizontalTo(bodyLayout.x - offset); break; case Adjustable.VERTICAL: this.scrollVerticalTo(bodyLayout.y - offset); break; } } } /** * Scrolls the viewport's origin to the given location, or as close to it as * possible. * <p> * This method should be invoked in the GUI thread. * * @param bounds * The bounds of the scrollable area that should become visible. * @param xIfNeeded * If this parameter is <code>true</code> the x coordinate is * changed only if the horizontal bounds are not currently * visible. * @param yIfNeeded * If this parameter is <code>true</code> the y coordinate is * changed only if the vertical bounds are not currently visible. */ public void scrollTo(Rectangle bounds, boolean xIfNeeded, boolean yIfNeeded) { boolean hscroll = this.hasHScrollBar; boolean vscroll = this.hasVScrollBar; if (hscroll || vscroll) { RBlockViewport bv = this.bodyLayout; Insets insets = this.getInsets(hscroll, vscroll); int vpheight = this.height - insets.top - insets.bottom; int vpwidth = this.width - insets.left - insets.right; int tentativeX = insets.left - bounds.x; int tentativeY = insets.top - bounds.y; boolean needCorrection = false; if (!(xIfNeeded && (tentativeX <= bv.x) && ((-tentativeX + bv.x + bounds.width) <= vpwidth))) { bv.setX(tentativeX); needCorrection = true; } if (!(yIfNeeded && (tentativeY <= bv.y) && ((-tentativeY + bv.y + bounds.height) <= vpheight))) { bv.setY(tentativeY); needCorrection = true; } if (needCorrection) { this.correctViewportOrigin(insets, this.width, this.height); } } } /** * Scroll to sb value. * * @param orientation * the orientation * @param value * the value */ private void scrollToSBValue(int orientation, int value) { Insets insets = this.getInsets(this.hasHScrollBar, this.hasVScrollBar); switch (orientation) { case Adjustable.HORIZONTAL: int xOrigin = insets.left - value; this.scrollHorizontalTo(xOrigin); break; case Adjustable.VERTICAL: int yOrigin = insets.top - value; this.scrollVerticalTo(yOrigin); break; } } /** Gets the r block viewport. * * @return the r block viewport */ public RBlockViewport getRBlockViewport() { return this.bodyLayout; } /* * (non-Javadoc) * @see * org.lobobrowser.html.renderer.BaseRCollection#extractSelectionText(java.lang * .StringBuffer, boolean, org.lobobrowser.html.renderer.RenderableSpot, * org.lobobrowser.html.renderer.RenderableSpot) */ @Override public boolean extractSelectionText(StringBuffer buffer, boolean inSelection, RenderableSpot startPoint, RenderableSpot endPoint) { boolean result = super.extractSelectionText(buffer, inSelection, startPoint, endPoint); String br = System.getProperty("line.separator"); if (inSelection) { buffer.insert(0, br); } if (result) { buffer.append(br); } return result; } /* * (non-Javadoc) * @see java.lang.Object#toString() */ @Override public String toString() { return "RBlock[node=" + this.modelNode + "]"; } /** Gets the exportable floating info. * * @return the exportable floating info */ public FloatingInfo getExportableFloatingInfo() { FloatingInfo info = this.bodyLayout.getExportableFloatingInfo(); if (info == null) { return null; } Insets insets = this.getInsets(this.hasHScrollBar, this.hasVScrollBar); return new FloatingInfo(info.getShiftX() + insets.left, info.getShiftY() + insets.top, info.getFloats()); } /** * The listener interface for receiving localAdjustment events. The class * that is interested in processing a localAdjustment event implements this * interface, and the object created with that class is registered with a * component using the component's <code>addLocalAdjustmentListener</code> * method. When the localAdjustment event occurs, that object's appropriate * method is invoked. * * @see LocalAdjustmentEvent */ private class LocalAdjustmentListener implements AdjustmentListener { /** The orientation. */ private final int orientation; /** * Instantiates a new local adjustment listener. * * @param orientation * the orientation */ public LocalAdjustmentListener(int orientation) { this.orientation = orientation; } /* * (non-Javadoc) * @see java.awt.event.AdjustmentListener#adjustmentValueChanged(java.awt.event. * AdjustmentEvent) */ @Override public void adjustmentValueChanged(AdjustmentEvent e) { if (RBlock.this.resettingScrollBars) { return; } switch (e.getAdjustmentType()) { case AdjustmentEvent.UNIT_INCREMENT: // fall through case AdjustmentEvent.UNIT_DECREMENT: // fall through case AdjustmentEvent.BLOCK_INCREMENT: // fall through case AdjustmentEvent.BLOCK_DECREMENT: // fall through case AdjustmentEvent.TRACK: { int value = e.getValue(); RBlock.this.scrollToSBValue(this.orientation, value); break; } } } } /** Gets the default overflow x. * * @return the default overflow x */ public int getDefaultOverflowX() { return defaultOverflowX; } /** Gets the default overflow y. * * @return the default overflow y */ public int getDefaultOverflowY() { return defaultOverflowY; } /** Sets the default overflow x. * * @param defaultOverflowX * the new default overflow x */ public void setDefaultOverflowX(int defaultOverflowX) { this.defaultOverflowX = defaultOverflowX; } /** Sets the default overflow y. * * @param defaultOverflowY * the new default overflow y */ public void setDefaultOverflowY(int defaultOverflowY) { this.defaultOverflowY = defaultOverflowY; } }