/* 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.FontMetrics; import java.awt.Graphics; import java.awt.Insets; import java.awt.Point; import java.awt.Rectangle; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.LinkedList; import java.util.Map; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import javax.swing.SwingUtilities; import org.lobobrowser.html.HtmlAttributeProperties; import org.lobobrowser.html.HtmlLayoutMapping; import org.lobobrowser.html.HtmlRendererContext; import org.lobobrowser.html.control.HrControl; import org.lobobrowser.html.control.RUIControl; import org.lobobrowser.html.dombl.ModelNode; import org.lobobrowser.html.dombl.UINode; import org.lobobrowser.html.domimpl.DOMNodeImpl; import org.lobobrowser.html.domimpl.HTMLElementImpl; import org.lobobrowser.html.domimpl.HTMLTableElementImpl; import org.lobobrowser.html.info.FloatingInfo; import org.lobobrowser.html.layout.MarkupLayout; import org.lobobrowser.html.layout.MiscLayout; import org.lobobrowser.html.renderstate.RenderState; import org.lobobrowser.html.style.AbstractCSS2Properties; import org.lobobrowser.html.style.HtmlInsets; import org.lobobrowser.html.style.HtmlValues; import org.lobobrowser.http.UserAgentContext; import org.w3c.dom.Node; /** * A substantial portion of the HTML rendering logic of the package can be found * in this class. This class is in charge of laying out the DOM subtree of a * node, usually on behalf of an RBlock. It creates a renderer subtree * consisting of RLine's or RBlock's. RLine's in turn contain RWord's and so on. * This class also happens to be used as an RBlock scrollable viewport. * An * RBlockViewport basically consists of two collections: seqRenderables and * positionedRenderables. The seqRenderables collection is a sequential list of * RLine's and RBlock's that is amenable to a binary search by Y position. The * positionedRenderables collection is a z-index ordered collection meant for * blocks with position=absolute and such. * * HOW FLOATS WORK Float boxes are scheduled to be added on the next available * line. Line layout is bounded by the current floatBounds. When a float is * placed with placeFloat(), an absolutely positioned box is added. Whether the * float height expands the RBlockViewport height is determined by * isFloatLimit(). * * FloatingBounds are inherited by sub-boxes, but the bounds are shifted. * * The RBlockViewport also publishes a collection of "exporatable floating * bounds." These are float boxes that go beyond the bounds of the * RBlockViewport, so ancestor blocks can obtain them to adjust their own * bounds. * * @author J. H. S. */ public class RBlockViewport extends BaseRCollection { /** The Constant ZERO_INSETS. */ public static final Insets ZERO_INSETS = new Insets(0, 0, 0, 0); /** The Constant logger. */ private static final Logger logger = LogManager.getLogger(RBlockViewport.class.getName()); /** The container. */ private RenderableContainer container; /** The list nesting. */ private int listNesting; /** The user agent context. */ private UserAgentContext userAgentContext; /** The renderer context. */ private HtmlRendererContext rendererContext; /** The frame context. */ private FrameContext frameContext; /** The positioned renderables. */ private SortedSet<PositionedRenderable> positionedRenderables; /** The seq renderables. */ private ArrayList<RLine> seqRenderables = null; /** The exportable floats. */ private ArrayList<ExportableFloat> exportableFloats = null; /** The current line. */ private RLine currentLine; /** The max x. */ private int maxX; /** The max y. */ private int maxY; /** The desired width. */ private int desiredWidth; // includes insets /** The desired height. */ private int desiredHeight; // includes insets /** The avail content height. */ private int availContentHeight; // does not include insets /** The avail content width. */ private int availContentWidth; // does not include insets /** The y limit. */ private int yLimit; /** The positioned ordinal. */ private int positionedOrdinal; /** The current collapsible margin. */ private int currentCollapsibleMargin; /** The padding insets. */ private Insets paddingInsets; /** The override no wrap. */ private boolean overrideNoWrap; /** The float bounds. */ private FloatingBounds floatBounds = null; /** The size only. */ private boolean sizeOnly; /** The last seq block. */ private BoundableRenderable lastSeqBlock; /** The Constant elementLayout. */ private static final Map<String, Object> elementLayout = HtmlLayoutMapping.layout(); /** The Constant miscLayout. */ private static final MarkupLayout miscLayout = new MiscLayout(); /** * Constructs an HtmlBlockLayout. * * @param modelNode * the model node * @param container * This is usually going to be an RBlock. * @param listNesting * The nesting level for lists. This is zero except inside a * list. * @param pcontext * The UserAgentContext instance. * @param rcontext * the rcontext * @param frameContext * This is usually going to be HtmlBlock, an object where text * selections are contained. * @param parent * This is usually going to be the parent of * <code>container</code>. */ public RBlockViewport(ModelNode modelNode, RenderableContainer container, int listNesting, UserAgentContext pcontext, HtmlRendererContext rcontext, FrameContext frameContext, RCollection parent) { super(container, modelNode); this.parent = parent; this.userAgentContext = pcontext; this.rendererContext = rcontext; this.frameContext = frameContext; this.container = container; this.listNesting = listNesting; // Layout here can always be "invalidated" this.layoutUpTreeCanBeInvalidated = true; } /* * (non-Javadoc) * * @see org.lobobrowser.html.renderer.BaseBoundableRenderable# * invalidateLayoutLocal() */ @Override public void invalidateLayoutLocal() { // Workaround for fact that RBlockViewport does not // get validated or invalidated. this.layoutUpTreeCanBeInvalidated = true; } /** * Gets the avail content width. * * @return the avail content width */ public int getAvailContentWidth() { return this.availContentWidth; } /** * Inits the collapsible margin. * * @return the int */ private int initCollapsibleMargin() { Object parent = this.parent; if (!(parent instanceof RBlock)) { return 0; } RBlock parentBlock = (RBlock) parent; return parentBlock.getCollapsibleMarginTop(); } /** * Builds the layout/renderer tree from scratch. Note: Returned dimension * needs to be actual size needed for rendered content, not the available * container size. This is relied upon by table layout. * * @param desiredWidth * the desired width * @param desiredHeight * the desired height * @param paddingInsets * the padding insets * @param yLimit * If other than -1, <code>layout</code> will throw * <code>SizeExceededException</code> in the event that the * layout goes beyond this y-coordinate point. * @param floatBounds * the float bounds * @param sizeOnly * the size only */ public void layout(int desiredWidth, int desiredHeight, Insets paddingInsets, int yLimit, FloatingBounds floatBounds, boolean sizeOnly) { // Expected in GUI thread. It's possible it may be invoked during pack() // outside of the GUI thread. if (!SwingUtilities.isEventDispatchThread() && logger.isEnabled(Level.INFO)) { logger.warn("layout(): Invoked outside GUI dispatch thread."); } RenderableContainer container = this.container; this.paddingInsets = paddingInsets; this.yLimit = yLimit; this.desiredHeight = desiredHeight; this.desiredWidth = desiredWidth; this.floatBounds = floatBounds; this.isFloatLimit = null; this.pendingFloats = null; this.sizeOnly = sizeOnly; this.lastSeqBlock = null; this.currentCollapsibleMargin = this.initCollapsibleMargin(); // maxX and maxY should not be reset by layoutPass. this.maxX = paddingInsets.left; this.maxY = paddingInsets.top; int availw = desiredWidth - paddingInsets.left - paddingInsets.right; if (availw < 0) { availw = 0; } int availh = desiredHeight - paddingInsets.top - paddingInsets.bottom; if (availh == 0) { availh = 0; } this.availContentHeight = availh; this.availContentWidth = availw; // New floating algorithm. this.layoutPass((DOMNodeImpl) this.modelNode); Collection delayedPairs = container.getDelayedPairs(); if ((delayedPairs != null) && (delayedPairs.size() > 0)) { // Add positioned renderables that belong here Iterator i = delayedPairs.iterator(); while (i.hasNext()) { DelayedPair pair = (DelayedPair) i.next(); if (pair.containingBlock == container) { this.importDelayedPair(pair); } } } // Compute maxY according to last block. int maxY = this.maxY; int maxYWholeBlock = maxY; BoundableRenderable lastSeqBlock = this.lastSeqBlock; if (lastSeqBlock != null) { int effBlockHeight = this.getEffectiveBlockHeight(lastSeqBlock); if ((lastSeqBlock.getY() + effBlockHeight) > maxY) { this.maxY = maxY = lastSeqBlock.getY() + effBlockHeight; maxYWholeBlock = lastSeqBlock.getY() + lastSeqBlock.getHeight(); } } // See if line should increase maxY. Empty // lines shouldn't, except in cases where // there was a BR. RLine lastLine = this.currentLine; Rectangle lastBounds = lastLine.getBounds(); if ((lastBounds.height > 0) || (lastBounds.y > maxYWholeBlock)) { int lastTopX = lastBounds.x + lastBounds.width; if (lastTopX > this.maxX) { this.maxX = lastTopX; } int lastTopY = lastBounds.y + lastBounds.height; if (lastTopY > maxY) { this.maxY = maxY = lastTopY; } } // Check positioned renderables for maxX and maxY SortedSet<PositionedRenderable> posRenderables = this.positionedRenderables; if (posRenderables != null) { boolean isFloatLimit = this.isFloatLimit(); Iterator<PositionedRenderable> i = posRenderables.iterator(); while (i.hasNext()) { PositionedRenderable pr = i.next(); BoundableRenderable br = pr.getRenderable(); if ((br.getX() + br.getWidth()) > this.maxX) { this.maxX = br.getX() + br.getWidth(); } if (isFloatLimit || !pr.isFloat()) { if ((br.getY() + br.getHeight()) > maxY) { this.maxY = maxY = br.getY() + br.getHeight(); } } } } this.width = paddingInsets.right + this.maxX; this.height = paddingInsets.bottom + maxY; } /** * Layout pass. * * @param rootNode * the root node */ private void layoutPass(DOMNodeImpl rootNode) { RenderableContainer container = this.container; container.clearDelayedPairs(); this.positionedOrdinal = 0; // Remove sequential renderables... this.seqRenderables = null; // Remove other renderables... this.positionedRenderables = null; // Remove exporatable floats... this.exportableFloats = null; // Call addLine after setting margins this.currentLine = this.addLine(rootNode, null, this.paddingInsets.top); // Start laying out... // The parent is expected to have set the RenderState already. this.layoutChildren(rootNode); // This adds last-line floats. this.lineDone(this.currentLine); } /** * Applies any horizonal aLignment. It may adjust height if necessary. * * @param alignXPercent * the align x percent * @param canvasWidth * The new width of the viewport. It could be different to the * previously calculated width. * @param paddingInsets * the padding insets */ public void alignX(int alignXPercent, int canvasWidth, Insets paddingInsets) { int prevMaxY = this.maxY; // Horizontal alignment if (alignXPercent > 0) { ArrayList<RLine> renderables = this.seqRenderables; if (renderables != null) { Insets insets = this.paddingInsets; int numRenderables = renderables.size(); int yoffset = 0; // This may get adjusted due to blocks and // floats. for (int i = 0; i < numRenderables; i++) { Object r = renderables.get(i); if (r instanceof BoundableRenderable) { BoundableRenderable seqRenderable = (BoundableRenderable) r; int y = seqRenderable.getY(); int newY; if (yoffset > 0) { newY = y + yoffset; seqRenderable.setY(newY); if ((newY + seqRenderable.getHeight()) > this.maxY) { this.maxY = newY + seqRenderable.getHeight(); } } else { newY = y; } boolean isVisibleBlock = (seqRenderable instanceof RBlock) && ((RBlock) seqRenderable).isOverflowVisibleX(); int leftOffset = isVisibleBlock ? insets.left : this.fetchLeftOffset(y); int rightOffset = isVisibleBlock ? insets.right : this.fetchRightOffset(y); int actualAvailWidth = canvasWidth - leftOffset - rightOffset; int difference = actualAvailWidth - seqRenderable.getWidth(); if (difference > 0) { // The difference check means that only // blocks with a declared width would get adjusted? int shift = (difference * alignXPercent) / 100; // if(floatBounds != null && isVisibleBlock) { // RBlock block = (RBlock) seqRenderable; // // Block needs to layed out again. Contents need // // to shift because of float. // final int expectedWidth = availContentWidth; // final int blockShiftRight = insets.right; // final int newX = leftOffset; // FloatingBoundsSource floatBoundsSource = new // ParentFloatingBoundsSource(blockShiftRight, // expectedWidth, newX, newY, floatBounds); // block.layout(actualAvailWidth, // this.availContentHeight, true, false, // floatBoundsSource); // } if (!isVisibleBlock) { int newX = leftOffset + shift; seqRenderable.setX(newX); } } } } } } if (prevMaxY != this.maxY) { this.height += (this.maxY - prevMaxY); } } /** * Applies vertical alignment. * * @param alignYPercent * the align y percent * @param canvasHeight * the canvas height * @param paddingInsets * the padding insets */ public void alignY(int alignYPercent, int canvasHeight, Insets paddingInsets) { int prevMaxY = this.maxY; if (alignYPercent > 0) { int availContentHeight = canvasHeight - paddingInsets.top - paddingInsets.bottom; int usedHeight = this.maxY - paddingInsets.top; int difference = availContentHeight - usedHeight; if (difference > 0) { int shift = (difference * alignYPercent) / 100; ArrayList<RLine> rlist = this.seqRenderables; if (rlist != null) { // Try sequential renderables first. Iterator renderables = rlist.iterator(); while (renderables.hasNext()) { Object r = renderables.next(); if (r instanceof BoundableRenderable) { BoundableRenderable line = (BoundableRenderable) r; int newY = line.getY() + shift; line.setY(newY); if ((newY + line.getHeight()) > this.maxY) { this.maxY = newY + line.getHeight(); } } } } // Now other renderables, but only those that can be // vertically aligned Set<PositionedRenderable> others = this.positionedRenderables; if (others != null) { Iterator<PositionedRenderable> i2 = others.iterator(); while (i2.hasNext()) { PositionedRenderable pr = i2.next(); if (pr.isVerticalAlignable()) { BoundableRenderable br = pr.getRenderable(); int newY = br.getY() + shift; br.setY(newY); if ((newY + br.getHeight()) > this.maxY) { this.maxY = newY + br.getHeight(); } } } } } } if (prevMaxY != this.maxY) { this.height += (this.maxY - prevMaxY); } } // /** // * // * @param block A block needing readjustment due to horizontal alignment. // * @return // */ // private int readjustBlock(RBlock block, final int newX, final int newY, // final FloatingBounds floatBounds) { // final int rightInsets = this.paddingInsets.right; // final int expectedWidth = this.desiredWidth - rightInsets - newX; // final int blockShiftRight = rightInsets; // final int prevHeight = block.height; // FloatingBoundsSource floatBoundsSource = new FloatingBoundsSource() { // public FloatingBounds getChildBlockFloatingBounds(int apparentBlockWidth) // { // int actualRightShift = blockShiftRight + (expectedWidth - // apparentBlockWidth); // return new ShiftedFloatingBounds(floatBounds, -newX, -actualRightShift, // -newY); // } // }; // block.adjust(expectedWidth, this.availContentHeight, true, false, // floatBoundsSource, true); // return block.height - prevHeight; // } // /** * Adds the line. * * @param startNode * the start node * @param prevLine * the prev line * @param newLineY * the new line y * @return the r line */ private RLine addLine(ModelNode startNode, RLine prevLine, int newLineY) { // lineDone must be called before we try to // get float bounds. this.lineDone(prevLine); this.checkY(newLineY); int leftOffset = this.fetchLeftOffset(newLineY); int newX = leftOffset; int newMaxWidth = this.desiredWidth - this.fetchRightOffset(newLineY) - leftOffset; RLine rline; boolean initialAllowOverflow; if (prevLine == null) { // Note: Assumes that prevLine == null means it's the first line. RenderState rs = this.modelNode.getRenderState(); initialAllowOverflow = rs == null ? false : rs.getWhiteSpace() == RenderState.WS_NOWRAP; // Text indentation only applies to the first line in the block. int textIndent = rs == null ? 0 : rs.getTextIndent(this.availContentWidth); if (textIndent != 0) { newX += textIndent; // Line width also changes! newMaxWidth += (leftOffset - newX); } } else { int prevLineHeight = prevLine.getHeight(); if (prevLineHeight > 0) { this.currentCollapsibleMargin = 0; } initialAllowOverflow = prevLine.isAllowOverflow(); if ((prevLine.x + prevLine.width) > this.maxX) { this.maxX = prevLine.x + prevLine.width; } } rline = new RLine(startNode, this.container, newX, newLineY, newMaxWidth, 0, initialAllowOverflow); rline.setParent(this); ArrayList<RLine> sr = this.seqRenderables; if (sr == null) { sr = new ArrayList(1); this.seqRenderables = sr; } sr.add(rline); this.currentLine = rline; return rline; } /** * Layout markup. * * @param node * the node */ public void layoutMarkup(DOMNodeImpl node) { // This is the "inline" layout of an element. // The difference with layoutChildren is that this // method checks for padding and margin insets. RenderState rs = node.getRenderState(); Insets marginInsets = null; Insets paddingInsets = null; if (rs != null) { HtmlInsets mi = rs.getMarginInsets(); marginInsets = mi == null ? null : mi.getSimpleAWTInsets(this.availContentWidth, this.availContentHeight); HtmlInsets pi = rs.getPaddingInsets(); paddingInsets = pi == null ? null : pi.getSimpleAWTInsets(this.availContentWidth, this.availContentHeight); } int leftSpacing = 0; int rightSpacing = 0; if (marginInsets != null) { leftSpacing += marginInsets.left; rightSpacing += marginInsets.right; } if (paddingInsets != null) { leftSpacing += paddingInsets.left; rightSpacing += paddingInsets.right; } if (leftSpacing > 0) { RLine line = this.currentLine; line.addSpacing(new RSpacing(node, this.container, leftSpacing, line.height)); } this.layoutChildren(node); if (rightSpacing > 0) { RLine line = this.currentLine; line.addSpacing(new RSpacing(node, this.container, rightSpacing, line.height)); } } /** * Layout children. * * @param node * the node */ public void layoutChildren(DOMNodeImpl node) { DOMNodeImpl[] childrenArray = node.getChildrenArray(); if (childrenArray != null) { int length = childrenArray.length; for (int i = 0; i < length; i++) { DOMNodeImpl child = childrenArray[i]; short nodeType = child.getNodeType(); if (nodeType == Node.TEXT_NODE) { this.layoutText(child); } else if (nodeType == Node.ELEMENT_NODE) { // Note that scanning for node bounds (anchor location) // depends on there being a style changer for inline // elements. this.currentLine.addStyleChanger(new RStyleChanger(child)); String nodeName = child.getNodeName().toUpperCase(); MarkupLayout ml = (MarkupLayout) elementLayout.get(nodeName); if (ml == null) { ml = miscLayout; } ml.layoutMarkup(this, (HTMLElementImpl) child); this.currentLine.addStyleChanger(new RStyleChanger(node)); } } } } /** * Position r block. * * @param markupElement * the markup element * @param renderable * the renderable */ public final void positionRBlock(HTMLElementImpl markupElement, RBlock renderable) { if (!this.addElsewhereIfPositioned(renderable, markupElement, false, true, false)) { int availContentHeight = this.availContentHeight; RLine line = this.currentLine; // Inform line done before layout so floats are considered. this.lineDone(line); Insets paddingInsets = this.paddingInsets; int newLineY = line == null ? paddingInsets.top : line.y + line.height; // int leftOffset = this.fetchLeftOffset(newLineY); // int rightOffset = this.fetchRightOffset(newLineY); // Float offsets are ignored with block. int availContentWidth = this.availContentWidth; final int expectedWidth = availContentWidth; final int blockShiftRight = paddingInsets.right; final int newX = paddingInsets.left; final int newY = newLineY; FloatingBounds floatBounds = this.floatBounds; FloatingBoundsSource floatBoundsSource = floatBounds == null ? null : new ParentFloatingBoundsSource(blockShiftRight, expectedWidth, newX, newY, floatBounds); renderable.layout(availContentWidth, availContentHeight, true, false, floatBoundsSource, this.sizeOnly); this.addAsSeqBlock(renderable, false, false, false, false); // Calculate new floating bounds after block has been put in place. FloatingInfo floatingInfo = renderable.getExportableFloatingInfo(); if (floatingInfo != null) { this.importFloatingInfo(floatingInfo, renderable); } // Now add line, after float is set. this.addLineAfterBlock(renderable, false); layoutRelative(markupElement, renderable); } } /** * Position r element. * * @param markupElement * the markup element * @param renderable * the renderable * @param usesAlignAttribute * the uses align attribute * @param obeysFloats * the obeys floats * @param alignCenterAttribute * the align center attribute */ public final void positionRElement(HTMLElementImpl markupElement, RElement renderable, boolean usesAlignAttribute, boolean obeysFloats, boolean alignCenterAttribute) { if (!this.addElsewhereIfPositioned(renderable, markupElement, usesAlignAttribute, true, true)) { int availContentWidth = this.availContentWidth; int availContentHeight = this.availContentHeight; RLine line = this.currentLine; // Inform line done before layout so floats are considered. this.lineDone(line); if (obeysFloats) { int newLineY = line == null ? this.paddingInsets.top : line.y + line.height; int leftOffset = this.fetchLeftOffset(newLineY); int rightOffset = this.fetchRightOffset(newLineY); availContentWidth = this.desiredWidth - leftOffset - rightOffset; } renderable.layout(availContentWidth, availContentHeight, this.sizeOnly); boolean centerBlock = false; if (alignCenterAttribute) { String align = markupElement.getAttribute(HtmlAttributeProperties.ALIGN); centerBlock = (align != null) && align.equalsIgnoreCase("center"); } this.addAsSeqBlock(renderable, obeysFloats, false, true, centerBlock); layoutRelative(markupElement, renderable); } } /** * Layout r block. * * @param markupElement * the markup element */ public final void layoutRBlock(HTMLElementImpl markupElement) { RBlock renderable = (RBlock) markupElement.getUINode(); if (renderable == null) { renderable = new RBlock(markupElement, this.listNesting, this.userAgentContext, this.rendererContext, this.frameContext, this.container); markupElement.setUINode(renderable); } renderable.setOriginalParent(this); this.positionRBlock(markupElement, renderable); } /** * Layout r table. * * @param markupElement * the markup element */ public final void layoutRTable(HTMLElementImpl markupElement) { RElement renderable = (RElement) markupElement.getUINode(); if (renderable == null) { renderable = new RTable(markupElement, this.userAgentContext, this.rendererContext, this.frameContext, container); markupElement.setUINode(renderable); } renderable.setOriginalParent(this); this.positionRElement(markupElement, renderable, markupElement instanceof HTMLTableElementImpl, true, true); } /** * Layout list item. * * @param markupElement * the markup element */ public final void layoutListItem(HTMLElementImpl markupElement) { RListItem renderable = (RListItem) markupElement.getUINode(); if (renderable == null) { renderable = new RListItem(markupElement, this.listNesting, this.userAgentContext, this.rendererContext, this.frameContext, this.container, null); markupElement.setUINode(renderable); } renderable.setOriginalParent(this); this.positionRBlock(markupElement, renderable); } /** * Layout list. * * @param markupElement * the markup element */ public final void layoutList(HTMLElementImpl markupElement) { RList renderable = (RList) markupElement.getUINode(); if (renderable == null) { renderable = new RList(markupElement, this.listNesting, this.userAgentContext, this.rendererContext, this.frameContext, this.container, null); markupElement.setUINode(renderable); } renderable.setOriginalParent(this); this.positionRBlock(markupElement, renderable); } /** * Adds the line break. * * @param startNode * the start node * @param breakType * the break type */ public void addLineBreak(ModelNode startNode, int breakType) { RLine line = this.currentLine; if (line == null) { Insets insets = this.paddingInsets; this.addLine(startNode, null, insets.top); line = this.currentLine; } if (line.getHeight() == 0) { RenderState rs = startNode.getRenderState(); int fontHeight = rs.getFontMetrics().getHeight(); line.setHeight(fontHeight); } line.setLineBreak(new LineBreak(breakType, startNode)); int newLineY; FloatingBounds fb = this.floatBounds; if ((breakType == LineBreak.NONE) || (fb == null)) { newLineY = line == null ? this.paddingInsets.top : line.y + line.height; } else { int prevY = line == null ? this.paddingInsets.top : line.y + line.height; switch (breakType) { case LineBreak.LEFT: newLineY = fb.getLeftClearY(prevY); break; case LineBreak.RIGHT: newLineY = fb.getRightClearY(prevY); break; default: newLineY = fb.getClearY(prevY); break; } } this.currentLine = this.addLine(startNode, line, newLineY); } /** * Adds the elsewhere if float. * * @param renderable * the renderable * @param element * the element * @param usesAlignAttribute * the uses align attribute * @param style * the style * @param layout * the layout * @return true, if successful */ private boolean addElsewhereIfFloat(BoundableRenderable renderable, HTMLElementImpl element, boolean usesAlignAttribute, AbstractCSS2Properties style, boolean layout) { // "static" handled here String align = null; if (style != null) { align = style.getFloat(); if ((align != null) && (align.length() == 0)) { align = null; } } if ((align == null) && usesAlignAttribute) { align = element.getAttribute(HtmlAttributeProperties.ALIGN); } if (align != null) { if ("left".equalsIgnoreCase(align)) { this.layoutFloat(renderable, layout, true); return true; } else if ("right".equalsIgnoreCase(align)) { this.layoutFloat(renderable, layout, false); return true; } else { // fall through } } return false; } /** * Gets the parent viewport. * * @return the parent viewport */ final RBlockViewport getParentViewport() { // Use originalParent, which for one, is not going to be null during // layout. RCollection parent = this.getOriginalOrCurrentParent(); while ((parent != null) && !(parent instanceof RBlockViewport)) { parent = parent.getOriginalOrCurrentParent(); } return (RBlockViewport) parent; } /** * Gets the position. * * @param element * the element * @return the position */ private static int getPosition(HTMLElementImpl element) { RenderState rs = element.getRenderState(); return rs == null ? RenderState.POSITION_STATIC : rs.getPosition(); } /** * Checks for position and float attributes. * * @param renderable * the renderable * @param element * the element * @param usesAlignAttribute * the uses align attribute * @param layoutIfPositioned * the layout if positioned * @param obeysFloats * the obeys floats * @return True if it was added elsewhere. */ private boolean addElsewhereIfPositioned(RElement renderable, HTMLElementImpl element, boolean usesAlignAttribute, boolean layoutIfPositioned, boolean obeysFloats) { // At this point block already has bounds. AbstractCSS2Properties style = element.getCurrentStyle(); int position = getPosition(element); boolean absolute = position == RenderState.POSITION_ABSOLUTE; boolean relative = position == RenderState.POSITION_RELATIVE; if (absolute || relative) { if (layoutIfPositioned) { // Presumes the method will return true. if (renderable instanceof RBlock) { RBlock block = (RBlock) renderable; FloatingBoundsSource inheritedFloatBoundsSource = null; if (relative) { Insets paddingInsets = this.paddingInsets; RLine line = this.currentLine; // Inform line done before layout so floats are // considered. this.lineDone(line); int newY = line == null ? paddingInsets.top : line.y + line.height; final int expectedWidth = this.availContentWidth; final int blockShiftRight = paddingInsets.right; final int newX = paddingInsets.left; FloatingBounds floatBounds = this.floatBounds; inheritedFloatBoundsSource = floatBounds == null ? null : new ParentFloatingBoundsSource(blockShiftRight, expectedWidth, newX, newY, floatBounds); } boolean floating = element.getRenderState().getFloat() != RenderState.FLOAT_NONE; boolean growHorizontally = relative && !floating; block.layout(this.availContentWidth, this.availContentHeight, growHorizontally, false, inheritedFloatBoundsSource, this.sizeOnly); } else { renderable.layout(this.availContentWidth, this.availContentHeight, this.sizeOnly); } } RenderState rs = element.getRenderState(); String leftText = style.getLeft(); String rightText = style.getRight(); String bottomText = style.getBottom(); String topText = style.getTop(); RLine line = this.currentLine; int lineBottomY = line == null ? 0 : line.getY() + line.getHeight(); int newLeft; if (leftText != null) { newLeft = HtmlValues.getPixelSize(leftText, rs, 0, this.availContentWidth); } else { if (rightText != null) { int right = HtmlValues.getPixelSize(rightText, rs, 0, this.availContentWidth); newLeft = this.desiredWidth - right - renderable.getWidth(); // If right==0 and renderable.width is larger than the // parent's width, // the expected behavior is for newLeft to be negative. } else { newLeft = 0; } } int newTop = relative ? 0 : lineBottomY; if (topText != null) { newTop = HtmlValues.getPixelSize(topText, rs, newTop, this.availContentHeight); } else { if (bottomText != null) { int bottom = HtmlValues.getPixelSize(bottomText, rs, 0, this.availContentHeight); if (relative) { newTop = -bottom; } else { newTop = Math.max(0, this.desiredHeight - bottom - renderable.getHeight()); } } } if (relative) { // First, try to add normally. RRelative rrel = new RRelative(this.container, element, renderable, newLeft, newTop); rrel.assignDimension(); if (!this.addElsewhereIfFloat(rrel, element, usesAlignAttribute, style, true)) { boolean centerBlock = false; if (renderable instanceof RTable) { String align = element.getAttribute(HtmlAttributeProperties.ALIGN); centerBlock = (align != null) && align.equalsIgnoreCase("center"); } this.addAsSeqBlock(rrel, obeysFloats, true, true, centerBlock); // Need to import float boxes from relative, after // the box's origin has been set. FloatingInfo floatingInfo = rrel.getExportableFloatingInfo(); if (floatingInfo != null) { this.importFloatingInfo(floatingInfo, rrel); } } else { // Adjust size of RRelative again - float might have been // adjusted. rrel.assignDimension(); } } else { // Schedule as delayed pair. Will be positioned after everything // else. this.scheduleAbsDelayedPair(renderable, leftText, rightText, topText, bottomText, rs, currentLine.getY() + currentLine.getHeight()); return true; } int newBottomY = renderable.getY() + renderable.getHeight(); this.checkY(newBottomY); if (newBottomY > this.maxY) { this.maxY = newBottomY; } return true; } else { if (this.addElsewhereIfFloat(renderable, element, usesAlignAttribute, style, layoutIfPositioned)) { return true; } } return false; } /** * Checks property 'float' and in some cases attribute 'align'. * * @param renderable * the renderable * @param element * the element * @param usesAlignAttribute * the uses align attribute */ public void addRenderableToLineCheckStyle(RElement renderable, HTMLElementImpl element, boolean usesAlignAttribute) { if (this.addElsewhereIfPositioned(renderable, element, usesAlignAttribute, true, true)) { return; } renderable.layout(this.availContentWidth, this.availContentHeight, this.sizeOnly); this.addRenderableToLine(renderable); layoutRelative(element, renderable); } /** * Adds the renderable to line. * * @param renderable * the renderable */ private void addRenderableToLine(Renderable renderable) { renderable.getModelNode().getRenderState(); RLine line = this.currentLine; int liney = line.y; boolean emptyLine = line.isEmpty(); FloatingBounds floatBounds = this.floatBounds; int cleary; if (floatBounds != null) { cleary = floatBounds.getFirstClearY(liney); } else { cleary = liney + line.height; } try { line.add(renderable); // Check if the line goes into the float. if ((floatBounds != null) && (cleary > liney)) { int rightOffset = this.fetchRightOffset(liney); int topLineX = this.desiredWidth - rightOffset; if ((line.getX() + line.getWidth()) > topLineX) { // Shift line down to clear area line.setY(cleary); } } } catch (OverflowException oe) { int nextY = emptyLine ? cleary : liney + line.height; this.addLine(renderable.getModelNode(), line, nextY); Collection renderables = oe.getRenderables(); Iterator i = renderables.iterator(); while (i.hasNext()) { Renderable r = (Renderable) i.next(); this.addRenderableToLine(r); } } if (renderable instanceof RUIControl) { this.container.addComponent(((RUIControl) renderable).getWidget().getComponent()); } } /** * Adds the word to line. * * @param renderable * the renderable */ private void addWordToLine(RWord renderable) { // this.skipLineBreakBefore = false; RLine line = this.currentLine; int liney = line.y; boolean emptyLine = line.isEmpty(); FloatingBounds floatBounds = this.floatBounds; int cleary; if (floatBounds != null) { cleary = floatBounds.getFirstClearY(liney); } else { cleary = liney + line.height; } try { line.addWord(renderable); // Check if the line goes into the float. if (!line.isAllowOverflow() && floatBounds != null && cleary > liney) { int rightOffset = this.fetchRightOffset(liney); int topLineX = this.desiredWidth - rightOffset; if ((line.getX() + line.getWidth()) > topLineX) { // Shift line down to clear area line.setY(cleary); } } } catch (OverflowException oe) { int nextY = emptyLine ? cleary : liney + line.height; this.addLine(renderable.getModelNode(), line, nextY); Collection renderables = oe.getRenderables(); Iterator i = renderables.iterator(); while (i.hasNext()) { Renderable r = (Renderable) i.next(); this.addRenderableToLine(r); } } } /** * Adds the as seq block. * * @param block * the block */ private void addAsSeqBlock(RElement block) { this.addAsSeqBlock(block, true, true, true, false); } /** * Gets the new block y. * * @param newBlock * the new block * @param expectedY * the expected y * @return the new block y */ private int getNewBlockY(BoundableRenderable newBlock, int expectedY) { // Assumes the previous block is not a line with height > 0. if (!(newBlock instanceof RElement)) { return expectedY; } RElement block = (RElement) newBlock; int ccm = this.currentCollapsibleMargin; int topMargin = block.getMarginTop(); if ((topMargin == 0) && (ccm == 0)) { return expectedY; } return expectedY - Math.min(topMargin, ccm); } /** * Gets the effective block height. * * @param block * the block * @return the effective block height */ private int getEffectiveBlockHeight(BoundableRenderable block) { // Assumes block is the last one in the sequence. if (!(block instanceof RElement)) { return block.getHeight(); } RCollection parent = this.getParent(); if (!(parent instanceof RElement)) { return block.getHeight(); } int blockMarginBottom = ((RElement) block).getMarginBottom(); int parentMarginBottom = ((RElement) parent).getCollapsibleMarginBottom(); return block.getHeight() - Math.min(blockMarginBottom, parentMarginBottom); } /** * Adds the as seq block. * * @param block * the block * @param obeysFloats * the obeys floats * @param informLineDone * the inform line done * @param addLine * the add line * @param centerBlock * the center block */ private void addAsSeqBlock(BoundableRenderable block, boolean obeysFloats, boolean informLineDone, boolean addLine, boolean centerBlock) { Insets insets = this.paddingInsets; int insetsl = insets.left; ArrayList sr = this.seqRenderables; if (sr == null) { sr = new ArrayList(1); this.seqRenderables = sr; } RLine prevLine = this.currentLine; boolean initialAllowOverflow; if (prevLine != null) { initialAllowOverflow = prevLine.isAllowOverflow(); if (informLineDone) { this.lineDone(prevLine); } if ((prevLine.x + prevLine.width) > this.maxX) { this.maxX = prevLine.x + prevLine.width; } // Check height only with floats. } else { initialAllowOverflow = false; } int prevLineHeight = prevLine == null ? 0 : prevLine.height; int newLineY = prevLine == null ? insets.top : prevLine.y + prevLineHeight; int blockX; int blockY = prevLineHeight == 0 ? this.getNewBlockY(block, newLineY) : newLineY; int blockWidth = block.getWidth(); if (obeysFloats) { // TODO: execution of fetchLeftOffset done twice with // positionRElement. FloatingBounds floatBounds = this.floatBounds; int actualAvailWidth; if (floatBounds != null) { int blockOffset = this.fetchLeftOffset(newLineY); blockX = blockOffset; int rightOffset = this.fetchRightOffset(newLineY); actualAvailWidth = this.desiredWidth - rightOffset - blockOffset; if (blockWidth > actualAvailWidth) { blockY = floatBounds.getClearY(newLineY); } } else { actualAvailWidth = this.availContentWidth; blockX = insetsl; } if (centerBlock) { int roomX = actualAvailWidth - blockWidth; if (roomX > 0) { blockX += (roomX / 2); } } } else { // Block does not obey alignment margins blockX = insetsl; } block.setOrigin(blockX, blockY); sr.add(block); block.setParent(this); if ((blockX + blockWidth) > this.maxX) { this.maxX = blockX + blockWidth; } this.lastSeqBlock = block; this.currentCollapsibleMargin = block instanceof RElement ? ((RElement) block).getMarginBottom() : 0; if (addLine) { newLineY = blockY + block.getHeight(); this.checkY(newLineY); int leftOffset = this.fetchLeftOffset(newLineY); int newX = leftOffset; int newMaxWidth = this.desiredWidth - this.fetchRightOffset(newLineY) - leftOffset; ModelNode lineNode = block.getModelNode().getParentModelNode(); RLine newLine = new RLine(lineNode, this.container, newX, newLineY, newMaxWidth, 0, initialAllowOverflow); newLine.setParent(this); sr.add(newLine); this.currentLine = newLine; } } /** * Adds the line after block. * * @param block * the block * @param informLineDone * the inform line done */ private void addLineAfterBlock(RBlock block, boolean informLineDone) { ArrayList sr = this.seqRenderables; if (sr == null) { sr = new ArrayList(1); this.seqRenderables = sr; } RLine prevLine = this.currentLine; boolean initialAllowOverflow; if (prevLine != null) { initialAllowOverflow = prevLine.isAllowOverflow(); if (informLineDone) { this.lineDone(prevLine); } if ((prevLine.x + prevLine.width) > this.maxX) { this.maxX = prevLine.x + prevLine.width; } // Check height only with floats. } else { initialAllowOverflow = false; } ModelNode lineNode = block.getModelNode().getParentModelNode(); int newLineY = block.getY() + block.getHeight(); this.checkY(newLineY); int leftOffset = this.fetchLeftOffset(newLineY); int newX = leftOffset; int newMaxWidth = this.desiredWidth - this.fetchRightOffset(newLineY) - leftOffset; RLine newLine = new RLine(lineNode, this.container, newX, newLineY, newMaxWidth, 0, initialAllowOverflow); newLine.setParent(this); sr.add(newLine); this.currentLine = newLine; } /** * Layout text. * * @param textNode * the text node */ private void layoutText(DOMNodeImpl textNode) { RenderState renderState = textNode.getRenderState(); if (renderState == null) { throw new IllegalStateException( "RenderState is null for node " + textNode + " with parent " + textNode.getParentNode()); } FontMetrics fm = renderState.getFontMetrics(); int descent = fm.getDescent(); int ascentPlusLeading = fm.getAscent() + fm.getLeading(); int wordHeight = fm.getHeight(); int blankWidth = fm.charWidth(' '); int whiteSpace = this.overrideNoWrap ? RenderState.WS_NOWRAP : renderState.getWhiteSpace(); int textTransform = renderState.getTextTransform(); String text = textNode.getNodeValue(); if (whiteSpace != RenderState.WS_PRE) { boolean prevAllowOverflow = this.currentLine.isAllowOverflow(); boolean allowOverflow = whiteSpace == RenderState.WS_NOWRAP; this.currentLine.setAllowOverflow(allowOverflow); try { int length = text.length(); StringBuffer word = new StringBuffer(12); for (int i = 0; i < length; i++) { char ch = text.charAt(i); if (Character.isWhitespace(ch)) { int wlen = word.length(); if (wlen > 0) { RWord rword = new RWord(textNode, word.toString(), container, fm, descent, ascentPlusLeading, wordHeight, textTransform); this.addWordToLine(rword); word.delete(0, wlen); } RLine line = this.currentLine; if (line.width > 0) { RBlank rblank = new RBlank(textNode, fm, container, ascentPlusLeading, blankWidth, wordHeight); line.addBlank(rblank); } for (i++; i < length; i++) { ch = text.charAt(i); if (!Character.isWhitespace(ch)) { word.append(ch); break; } } } else { word.append(ch); } } if (word.length() > 0) { RWord rword = new RWord(textNode, word.toString(), container, fm, descent, ascentPlusLeading, wordHeight, textTransform); this.addWordToLine(rword); } } finally { this.currentLine.setAllowOverflow(prevAllowOverflow); } } else { int length = text.length(); boolean lastCharSlashR = false; StringBuffer line = new StringBuffer(); for (int i = 0; i < length; i++) { char ch = text.charAt(i); switch (ch) { case '\r': lastCharSlashR = true; break; case '\n': int llen = line.length(); if (llen > 0) { RWord rword = new RWord(textNode, line.toString(), container, fm, descent, ascentPlusLeading, wordHeight, textTransform); this.addWordToLine(rword); line.delete(0, line.length()); } RLine prevLine = this.currentLine; prevLine.setLineBreak(new LineBreak(LineBreak.NONE, textNode)); this.addLine(textNode, prevLine, prevLine.y + prevLine.height); break; default: if (lastCharSlashR) { line.append('\r'); lastCharSlashR = false; } line.append(ch); break; } } if (line.length() > 0) { RWord rword = new RWord(textNode, line.toString(), container, fm, descent, ascentPlusLeading, wordHeight, textTransform); this.addWordToLine(rword); } } } /** * Populate z index groups. * * @param others * An ordered collection. * @param seqRenderables * the seq renderables * @param destination * the destination */ private void populateZIndexGroups(Collection others, Collection seqRenderables, ArrayList destination) { this.populateZIndexGroups(others, seqRenderables == null ? null : seqRenderables.iterator(), destination); } /** * Populate z index groups. * * @param others * An ordered collection. * @param seqRenderablesIterator * the seq renderables iterator * @param destination * the destination */ private void populateZIndexGroups(Collection others, Iterator seqRenderablesIterator, ArrayList destination) { // First, others with z-index < 0 Iterator i1 = others.iterator(); Renderable pending = null; while (i1.hasNext()) { PositionedRenderable pr = (PositionedRenderable) i1.next(); BoundableRenderable r = pr.getRenderable(); if (r.getZIndex() >= 0) { pending = r; break; } destination.add(r); } // Second, sequential renderables Iterator i2 = seqRenderablesIterator; if (i2 != null) { while (i2.hasNext()) { destination.add(i2.next()); } } // Third, other renderables with z-index >= 0. if (pending != null) { destination.add(pending); while (i1.hasNext()) { PositionedRenderable pr = (PositionedRenderable) i1.next(); Renderable r = pr.getRenderable(); destination.add(r); } } } /** * Gets the renderables array. * * @return the renderables array */ public Renderable[] getRenderablesArray() { SortedSet<PositionedRenderable> others = this.positionedRenderables; int othersSize = others == null ? 0 : others.size(); if (othersSize == 0) { ArrayList sr = this.seqRenderables; return sr == null ? Renderable.EMPTY_ARRAY : (Renderable[]) sr.toArray(Renderable.EMPTY_ARRAY); } else { ArrayList allRenderables = new ArrayList(); this.populateZIndexGroups(others, this.seqRenderables, allRenderables); return (Renderable[]) allRenderables.toArray(Renderable.EMPTY_ARRAY); } } /* * (non-Javadoc) * * @see org.lobobrowser.html.renderer.RCollection#getRenderables() */ @Override public Iterator getRenderables() { SortedSet others = this.positionedRenderables; if ((others == null) || (others.size() == 0)) { ArrayList sr = this.seqRenderables; return sr == null ? null : sr.iterator(); } else { ArrayList allRenderables = new ArrayList(); this.populateZIndexGroups(others, this.seqRenderables, allRenderables); return allRenderables.iterator(); } } /** * Gets the renderables. * * @param clipBounds * the clip bounds * @return the renderables */ public Iterator getRenderables(Rectangle clipBounds) { if (!SwingUtilities.isEventDispatchThread() && logger.isEnabled(Level.INFO)) { logger.warn("getRenderables(): Invoked outside GUI dispatch thread."); } ArrayList sr = this.seqRenderables; Iterator baseIterator = null; if (sr != null) { Renderable[] array = (Renderable[]) sr.toArray(Renderable.EMPTY_ARRAY); Range range = MarkupUtilities.findRenderables(array, clipBounds, true); baseIterator = org.lobobrowser.util.ArrayUtilities.iterator(array, range.getOffset(), range.getLength()); } SortedSet others = this.positionedRenderables; if ((others == null) || (others.size() == 0)) { return baseIterator; } else { ArrayList<PositionedRenderable> matches = new ArrayList<PositionedRenderable>(); // ArrayList "matches" keeps the order from "others". Iterator i = others.iterator(); while (i.hasNext()) { PositionedRenderable pr = (PositionedRenderable) i.next(); Object r = pr.getRenderable(); if (r instanceof BoundableRenderable) { BoundableRenderable br = (BoundableRenderable) r; Rectangle rbounds = br.getBounds(); if (clipBounds.intersects(rbounds)) { matches.add(pr); } } } if (matches.size() == 0) { return baseIterator; } else { ArrayList destination = new ArrayList(); this.populateZIndexGroups(matches, baseIterator, destination); return destination.iterator(); } } } /* * (non-Javadoc) * * @see org.lobobrowser.html.renderer.BaseRCollection#getRenderable(int, * int) */ @Override public BoundableRenderable getRenderable(int x, int y) { Iterator i = this.getRenderables(x, y); return i == null ? null : (i.hasNext() ? (BoundableRenderable) i.next() : null); } /** * Gets the renderable. * * @param point * the point * @return the renderable */ public BoundableRenderable getRenderable(java.awt.Point point) { return this.getRenderable(point.x, point.y); } /** * Gets the renderables. * * @param point * the point * @return the renderables */ public Iterator getRenderables(java.awt.Point point) { return this.getRenderables(point.x, point.y); } /** * Gets the renderables. * * @param pointx * the pointx * @param pointy * the pointy * @return the renderables */ public Iterator getRenderables(int pointx, int pointy) { if (!SwingUtilities.isEventDispatchThread() && logger.isEnabled(Level.INFO)) { logger.warn("getRenderable(): Invoked outside GUI dispatch thread."); } Collection result = null; SortedSet others = this.positionedRenderables; int size = others == null ? 0 : others.size(); PositionedRenderable[] otherArray = size == 0 ? null : (PositionedRenderable[]) others.toArray(PositionedRenderable.EMPTY_ARRAY); // Try to find in other renderables with z-index >= 0 first. int index = 0; if (size != 0) { int px = pointx; int py = pointy; // Must go in reverse order for (index = size; --index >= 0;) { PositionedRenderable pr = otherArray[index]; BoundableRenderable r = pr.getRenderable(); if (r.getZIndex() < 0) { break; } if (r instanceof BoundableRenderable) { BoundableRenderable br = r; Rectangle rbounds = br.getBounds(); if (rbounds.contains(px, py)) { if (result == null) { result = new LinkedList(); } result.add(br); } } } } // Now do a "binary" search on sequential renderables. ArrayList sr = this.seqRenderables; if (sr != null) { Renderable[] array = (Renderable[]) sr.toArray(Renderable.EMPTY_ARRAY); BoundableRenderable found = MarkupUtilities.findRenderable(array, pointx, pointy, true); if (found != null) { if (result == null) { result = new LinkedList(); } result.add(found); } } // Finally, try to find it in renderables with z-index < 0. if (size != 0) { int px = pointx; int py = pointy; // Must go in reverse order for (; index >= 0; index--) { PositionedRenderable pr = otherArray[index]; Renderable r = pr.getRenderable(); if (r instanceof BoundableRenderable) { BoundableRenderable br = (BoundableRenderable) r; Rectangle rbounds = br.getBounds(); if (rbounds.contains(px, py)) { if (result == null) { result = new LinkedList(); } result.add(br); } } } } return result == null ? null : result.iterator(); } /** * Setup new ui control. * * @param container * the container * @param element * the element * @param control * the control * @return the r element */ private RElement setupNewUIControl(RenderableContainer container, HTMLElementImpl element, UIControl control) { RElement renderable = new RUIControl(element, control, container, this.frameContext, this.userAgentContext); element.setUINode(renderable); return renderable; } /** * Adds the alignable as block. * * @param markupElement * the markup element * @param renderable * the renderable */ public final void addAlignableAsBlock(HTMLElementImpl markupElement, RElement renderable) { // TODO: Get rid of this method? // At this point block already has bounds. boolean regularAdd = false; String align = markupElement.getAttribute(HtmlAttributeProperties.ALIGN); if (align != null) { if ("left".equalsIgnoreCase(align)) { this.layoutFloat(renderable, false, true); } else if ("right".equalsIgnoreCase(align)) { this.layoutFloat(renderable, false, false); } else { regularAdd = true; } } else { regularAdd = true; } if (regularAdd) { this.addAsSeqBlock(renderable); } } /** * Layout hr. * * @param markupElement * the markup element */ public final void layoutHr(HTMLElementImpl markupElement) { RElement renderable = (RElement) markupElement.getUINode(); if (renderable == null) { renderable = this.setupNewUIControl(container, markupElement, new HrControl(markupElement)); } renderable.layout(this.availContentWidth, this.availContentHeight, this.sizeOnly); this.addAlignableAsBlock(markupElement, renderable); } /** * Gets offset from the left due to floats. It includes padding. * * @param newLineY * the new line y * @return the int */ public final int fetchLeftOffset(int newLineY) { Insets paddingInsets = this.paddingInsets; FloatingBounds floatBounds = this.floatBounds; if (floatBounds == null) { return paddingInsets.left; } int left = floatBounds.getLeft(newLineY); if (left < paddingInsets.left) { return paddingInsets.left; } return left; } /** * Gets offset from the right due to floats. It includes padding. * * @param newLineY * the new line y * @return the int */ public final int fetchRightOffset(int newLineY) { Insets paddingInsets = this.paddingInsets; FloatingBounds floatBounds = this.floatBounds; if (floatBounds == null) { return paddingInsets.right; } int right = floatBounds.getRight(newLineY); if (right < paddingInsets.right) { return paddingInsets.right; } return right; } /** The Constant SEE. */ private static final SizeExceededException SEE = new SizeExceededException(); /** * Check y. * * @param y * the y */ public final void checkY(int y) { if ((this.yLimit != -1) && (y > this.yLimit)) { throw SEE; } } /** * Layout float. * * @param renderable * the renderable * @param layout * the layout * @param leftFloat * the left float */ public final void layoutFloat(BoundableRenderable renderable, boolean layout, boolean leftFloat) { renderable.setOriginalParent(this); if (layout) { int availWidth = this.availContentWidth; int availHeight = this.availContentHeight; if (renderable instanceof RBlock) { RBlock block = (RBlock) renderable; // Float boxes don't inherit float bounds? block.layout(availWidth, availHeight, false, false, null, this.sizeOnly); } else if (renderable instanceof RElement) { RElement e = (RElement) renderable; e.layout(availWidth, availHeight, this.sizeOnly); } } RFloatInfo floatInfo = new RFloatInfo(renderable.getModelNode(), renderable, leftFloat); this.currentLine.simplyAdd(floatInfo); this.scheduleFloat(floatInfo); } /** * Schedule abs delayed pair. * * @param renderable * the renderable */ private void scheduleAbsDelayedPair(final BoundableRenderable renderable, String leftText, String rightText, String topText, String bottomText, RenderState rs, int currY) { // It gets reimported in the local // viewport if it turns out it can't be exported up. RenderableContainer container = this.container; for (;;) { if (container instanceof Renderable) { Object node = ((Renderable) container).getModelNode(); if (node instanceof HTMLElementImpl) { HTMLElementImpl element = (HTMLElementImpl) node; int position = getPosition(element); if (position != RenderState.POSITION_STATIC) { break; } RenderableContainer newContainer = container.getParentContainer(); if (newContainer == null) { break; } container = newContainer; } else { break; } } else { break; } } DelayedPair pair = new DelayedPair(this.container, container, renderable, leftText, rightText, topText, bottomText, rs, currY); this.container.addDelayedPair(pair); } /** * Import delayed pair. * * @param pair * the pair */ void importDelayedPair(DelayedPair pair) { pair.positionPairChild(); BoundableRenderable r = pair.child; this.addPositionedRenderable(r, false, false); } /** * Adds the positioned renderable. * * @param renderable * the renderable * @param verticalAlignable * the vertical alignable * @param isFloat * the is float */ public final void addPositionedRenderable(BoundableRenderable renderable, boolean verticalAlignable, boolean isFloat) { // Expected to be called only in GUI thread. SortedSet others = this.positionedRenderables; if (others == null) { others = new TreeSet(new ZIndexComparator()); this.positionedRenderables = others; } others.add(new PositionedRenderable(renderable, verticalAlignable, this.positionedOrdinal++, isFloat)); renderable.setParent(this); if (renderable instanceof RUIControl) { this.container.addComponent(((RUIControl) renderable).getWidget().getComponent()); } } /** * Gets the first line height. * * @return the first line height */ public int getFirstLineHeight() { ArrayList renderables = this.seqRenderables; if (renderables != null) { int size = renderables.size(); if (size == 0) { return 0; } for (int i = 0; i < size; i++) { BoundableRenderable br = (BoundableRenderable) renderables.get(0); int height = br.getHeight(); if (height != 0) { return height; } } } // Not found!! return 1; } /** * Gets the first baseline offset. * * @return the first baseline offset */ public int getFirstBaselineOffset() { ArrayList renderables = this.seqRenderables; if (renderables != null) { Iterator i = renderables.iterator(); while (i.hasNext()) { Object r = i.next(); if (r instanceof RLine) { int blo = ((RLine) r).getBaselineOffset(); if (blo != 0) { return blo; } } else if (r instanceof RBlock) { RBlock block = (RBlock) r; if (block.getHeight() > 0) { Insets insets = block.getInsets(false, false); Insets paddingInsets = this.paddingInsets; return block.getFirstBaselineOffset() + insets.top + (paddingInsets == null ? 0 : paddingInsets.top); } } } } return 0; } // ---------------------------------------------------------------- /* * (non-Javadoc) * * @see * org.lobobrowser.html.renderer.BoundableRenderable#getLowestRenderableSpot * (int, int) */ @Override public RenderableSpot getLowestRenderableSpot(int x, int y) { BoundableRenderable br = this.getRenderable(new Point(x, y)); if (br != null) { return br.getLowestRenderableSpot(x - br.getX(), y - br.getY()); } else { return new RenderableSpot(this, x, y); } } /* * (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) { Iterator i = this.getRenderables(new Point(x, y)); if (i != null) { while (i.hasNext()) { BoundableRenderable br = (BoundableRenderable) i.next(); if (br != null) { Rectangle bounds = br.getBounds(); if (!br.onMouseClick(event, x - bounds.x, y - bounds.y)) { 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) { Iterator i = this.getRenderables(new Point(x, y)); if (i != null) { while (i.hasNext()) { BoundableRenderable br = (BoundableRenderable) i.next(); if (br != null) { Rectangle bounds = br.getBounds(); if (!br.onDoubleClick(event, x - bounds.x, y - bounds.y)) { 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; /* * (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) { Iterator i = this.getRenderables(new Point(x, y)); if (i != null) { while (i.hasNext()) { BoundableRenderable br = (BoundableRenderable) i.next(); if (br != null) { Rectangle bounds = br.getBounds(); if (!br.onMousePressed(event, x - bounds.x, y - bounds.y)) { this.armedRenderable = br; 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) { Iterator i = this.getRenderables(new Point(x, y)); if (i != null) { while (i.hasNext()) { BoundableRenderable br = (BoundableRenderable) i.next(); if (br != null) { Rectangle bounds = br.getBounds(); if (!br.onMouseReleased(event, x - bounds.x, y - bounds.y)) { BoundableRenderable oldArmedRenderable = this.armedRenderable; if ((oldArmedRenderable != null) && (br != oldArmedRenderable)) { oldArmedRenderable.onMouseDisarmed(event); this.armedRenderable = null; } return false; } } } } BoundableRenderable oldArmedRenderable = this.armedRenderable; if (oldArmedRenderable != null) { oldArmedRenderable.onMouseDisarmed(event); this.armedRenderable = null; } return true; } @Override public boolean onKeyPressed(KeyEvent event) { // TODO Auto-generated method stub return false; } @Override public boolean onKeyUp(KeyEvent event) { // TODO Auto-generated method stub return false; } @Override public boolean onKeyDown(KeyEvent event) { // TODO Auto-generated method stub return false; } /* * (non-Javadoc) * * @see org.lobobrowser.html.renderer.Renderable#paint(java.awt.Graphics) */ @Override public void paint(Graphics g) { Rectangle clipBounds = g.getClipBounds(); Iterator i = this.getRenderables(clipBounds); if (i != null) { while (i.hasNext()) { Object robj = i.next(); // The expected behavior in HTML is for boxes // not to be clipped unless overflow=hidden. if (robj instanceof BoundableRenderable) { BoundableRenderable renderable = (BoundableRenderable) robj; renderable.paintTranslated(g); // numRenderables++; } else { ((Renderable) robj).paint(g); } } } } /* * (non-Javadoc) * * @see * org.lobobrowser.html.renderer.BoundableRenderable#isContainedByNode() */ @Override public boolean isContainedByNode() { return false; } /* * (non-Javadoc) * * @see java.lang.Object#toString() */ @Override public String toString() { return "RBlockViewport[node=" + this.modelNode + "]"; } /** * Schedule float. * * @param floatInfo * the float info */ private void scheduleFloat(RFloatInfo floatInfo) { RLine line = this.currentLine; if (line == null) { int y = line == null ? this.paddingInsets.top : line.getY(); this.placeFloat(floatInfo.getRenderable(), y, floatInfo.isLeftFloat()); } else if (line.getWidth() == 0) { int y = line.getY(); this.placeFloat(floatInfo.getRenderable(), y, floatInfo.isLeftFloat()); int leftOffset = this.fetchLeftOffset(y); int rightOffset = this.fetchRightOffset(y); line.changeLimits(leftOffset, this.desiredWidth - leftOffset - rightOffset); } else { // These pending floats are positioned when // lineDone() is called. Collection<RFloatInfo> c = this.pendingFloats; if (c == null) { c = new LinkedList<RFloatInfo>(); this.pendingFloats = c; } c.add(floatInfo); } } /** The pending floats. */ private Collection<RFloatInfo> pendingFloats = null; /** * Line done. * * @param line * the line */ private void lineDone(RLine line) { int yAfterLine = line == null ? this.paddingInsets.top : line.y + line.height; Collection<RFloatInfo> pfs = this.pendingFloats; if (pfs != null) { this.pendingFloats = null; Iterator i = pfs.iterator(); while (i.hasNext()) { RFloatInfo pf = (RFloatInfo) i.next(); this.placeFloat(pf.getRenderable(), yAfterLine, pf.isLeftFloat()); } } } /** * Adds the exportable float. * * @param element * the element * @param leftFloat * the left float * @param origX * the orig x * @param origY * the orig y */ private void addExportableFloat(BoundableRenderable element, boolean leftFloat, int origX, int origY) { ArrayList<ExportableFloat> ep = this.exportableFloats; if (ep == null) { ep = new ArrayList<ExportableFloat>(1); this.exportableFloats = ep; } ep.add(new ExportableFloat(element, leftFloat, origX, origY)); } /** * Place float. * * @param element * the element * @param y * The desired top position of the float element. * @param leftFloat * the left float */ private void placeFloat(BoundableRenderable element, int y, boolean leftFloat) { Insets insets = this.paddingInsets; int boxY = y; int boxWidth = element.getWidth(); int boxHeight = element.getHeight(); int desiredWidth = this.desiredWidth; int boxX; for (;;) { int leftOffset = this.fetchLeftOffset(boxY); int rightOffset = this.fetchRightOffset(boxY); boxX = leftFloat ? leftOffset : desiredWidth - rightOffset - boxWidth; if ((leftOffset == insets.left) && (rightOffset == insets.right)) { // Probably typical scenario. If it's overflowing to the left, // we need to correct. if (!leftFloat && (boxX < leftOffset)) { boxX = leftOffset; } break; } if (boxWidth <= (desiredWidth - rightOffset - leftOffset)) { // Size is fine. break; } // At this point the float doesn't fit at the current Y position. if (element instanceof RBlock) { // Try shrinking it. RBlock relement = (RBlock) element; if (!relement.hasDeclaredWidth()) { int availableBoxWidth = desiredWidth - rightOffset - leftOffset; relement.layout(availableBoxWidth, this.availContentHeight, this.sizeOnly); if (relement.getWidth() < boxWidth) { if (relement.getWidth() > (desiredWidth - rightOffset - leftOffset)) { // Didn't work out. Put it back the way it was. relement.layout(this.availContentWidth, this.availContentHeight, this.sizeOnly); } else { // Retry boxWidth = relement.getWidth(); boxHeight = relement.getHeight(); continue; } } } } FloatingBounds fb = this.floatBounds; int newY = fb == null ? boxY + boxHeight : fb.getFirstClearY(boxY); if (newY == boxY) { // Possible if prior box has height zero? break; } boxY = newY; } // Position element element.setOrigin(boxX, boxY); // Update float bounds accordingly int offsetFromBorder = leftFloat ? boxX + boxWidth : desiredWidth - boxX; this.floatBounds = new FloatingViewportBounds(this.floatBounds, leftFloat, boxY, offsetFromBorder, boxHeight); // Add element to collection boolean isFloatLimit = this.isFloatLimit(); if (isFloatLimit) { this.addPositionedRenderable(element, true, true); } else { this.addExportableFloat(element, leftFloat, boxX, boxY); } // Adjust maxX based on float. if ((boxX + boxWidth) > this.maxX) { this.maxX = boxX + boxWidth; } // Adjust maxY based on float, but only if this viewport is the float // limit. if (this.isFloatLimit()) { if ((boxY + boxHeight) > this.maxY) { this.maxY = boxY + boxHeight; } } } /** The is float limit. */ private Boolean isFloatLimit = null; /** * Checks if is float limit. * * @return true, if is float limit */ private boolean isFloatLimit() { Boolean fl = this.isFloatLimit; if (fl == null) { fl = this.isFloatLimitImpl(); this.isFloatLimit = fl; } return fl.booleanValue(); } /** * Checks if is float limit impl. * * @return the boolean */ private Boolean isFloatLimitImpl() { Object parent = this.getOriginalOrCurrentParent(); if (!(parent instanceof RBlock)) { return Boolean.TRUE; } RBlock blockParent = (RBlock) parent; Object grandParent = blockParent.getOriginalOrCurrentParent(); if (!(grandParent instanceof RBlockViewport)) { // Could be contained in a table, or it could // be a list item, for example. return Boolean.TRUE; } ModelNode node = this.modelNode; if (!(node instanceof HTMLElementImpl)) { // Can only be a document here. return Boolean.TRUE; } HTMLElementImpl element = (HTMLElementImpl) node; int position = getPosition(element); if ((position == RenderState.POSITION_ABSOLUTE) || (position == RenderState.POSITION_FIXED)) { return Boolean.TRUE; } element.getCurrentStyle(); RenderState rs = element.getRenderState(); int floatValue = rs == null ? RenderState.FLOAT_NONE : rs.getFloat(); if (floatValue != RenderState.FLOAT_NONE) { return Boolean.TRUE; } int overflowX = rs == null ? RenderState.OVERFLOW_NONE : rs.getOverflowX(); int overflowY = rs == null ? RenderState.OVERFLOW_NONE : rs.getOverflowY(); if ((overflowX == RenderState.OVERFLOW_AUTO) || (overflowX == RenderState.OVERFLOW_SCROLL) || (overflowY == RenderState.OVERFLOW_AUTO) || (overflowY == RenderState.OVERFLOW_SCROLL)) { return Boolean.TRUE; } return Boolean.FALSE; } /** * Gets the exportable floating info. * * @return the exportable floating info */ public FloatingInfo getExportableFloatingInfo() { ArrayList<ExportableFloat> ef = this.exportableFloats; if (ef == null) { return null; } ExportableFloat[] floats = ef.toArray(ExportableFloat.EMPTY_ARRAY); return new FloatingInfo(0, 0, floats); } /** * Import floating info. * * @param floatingInfo * the floating info * @param block * the block */ private void importFloatingInfo(FloatingInfo floatingInfo, BoundableRenderable block) { int shiftX = floatingInfo.getShiftX() + block.getX(); int shiftY = floatingInfo.getShiftY() + block.getY(); ExportableFloat[] floats = floatingInfo.getFloats(); int length = floats.length; for (int i = 0; i < length; i++) { ExportableFloat ef = floats[i]; this.importFloat(ef, shiftX, shiftY); } } /** * Import float. * * @param ef * the ef * @param shiftX * the shift x * @param shiftY * the shift y */ private void importFloat(ExportableFloat ef, int shiftX, int shiftY) { BoundableRenderable renderable = ef.getElement(); int newX = ef.getOrigX() + shiftX; int newY = ef.getOrigY() + shiftY; renderable.setOrigin(newX, newY); FloatingBounds prevBounds = this.floatBounds; int offsetFromBorder; boolean leftFloat = ef.isLeftFloat(); if (leftFloat) { offsetFromBorder = newX + renderable.getWidth(); } else { offsetFromBorder = this.desiredWidth - newX; } this.floatBounds = new FloatingViewportBounds(prevBounds, leftFloat, newY, offsetFromBorder, renderable.getHeight()); if (this.isFloatLimit()) { this.addPositionedRenderable(renderable, true, true); } else { this.addExportableFloat(renderable, leftFloat, newX, newY); } } public void positionDelayed() { final Collection<DelayedPair> delayedPairs = container.getDelayedPairs(); if (delayedPairs != null && delayedPairs.size() > 0) { // Add positioned renderables that belong here final Iterator<DelayedPair> i = delayedPairs.iterator(); while (i.hasNext()) { final DelayedPair pair = i.next(); if (pair.containingBlock == container) { this.importDelayedPair(pair); } } } } /** * Layout r inline block. * * @param markupElement * the markup element */ public void layoutRInlineBlock(final HTMLElementImpl markupElement) { UINode uINode = markupElement.getUINode(); RInlineBlock inlineBlock = null; if (uINode instanceof RInlineBlock) { inlineBlock = (RInlineBlock) uINode; } else { final RInlineBlock newInlineBlock = new RInlineBlock(container, markupElement, userAgentContext, rendererContext, frameContext); markupElement.setUINode(newInlineBlock); inlineBlock = newInlineBlock; } inlineBlock.doLayout(availContentWidth, availContentHeight, sizeOnly); addRenderableToLine(inlineBlock); inlineBlock.setOriginalParent(inlineBlock.getParent()); layoutRelative(markupElement, inlineBlock); } /* This is used to bubble up relative elements (on the z-axis) */ private boolean layoutRelative(final HTMLElementImpl markupElement, final RElement renderable) { int position = getPosition(markupElement); boolean isRelative = position == RenderState.POSITION_RELATIVE; if (isRelative) { RenderableContainer con = getPositionedAncestor(container); DelayedPair dp = new DelayedPair(container, con, renderable, null, null, null, null, null, position); container.addDelayedPair(dp); if (renderable instanceof RUIControl) { this.container.addComponent(((RUIControl) renderable).getWidget().getComponent()); } return true; } return false; } /** Gets an ancestor which is "positioned" (that is whose position is not static). * Stops searching when HTML element is encountered. */ private static RenderableContainer getPositionedAncestor(RenderableContainer containingBlock) { for (;;) { if (containingBlock instanceof Renderable) { final ModelNode node = ((Renderable) containingBlock).getModelNode(); if (node instanceof HTMLElementImpl) { HTMLElementImpl element = (HTMLElementImpl) node; int position = getPosition(element); // if (position != RenderState.POSITION_STATIC || (element instanceof HTMLHtmlElement)) { if (position != RenderState.POSITION_STATIC) { break; } RenderableContainer newContainer = containingBlock.getParentContainer(); if (newContainer == null) { break; } containingBlock = newContainer; } else { break; } } else { break; } } return containingBlock; } /** * Gets the frame context. * * @return the frame context */ public FrameContext getFrameContext() { return frameContext; } /** * Gets the container. * * @return the container */ public RenderableContainer getContainer() { return container; } /** * Gets the user agent context. * * @return the user agent context */ public UserAgentContext getUserAgentContext() { return userAgentContext; } /** * Gets the renderer context. * * @return the renderer context */ public HtmlRendererContext getRendererContext() { return rendererContext; } }