| /******************************************************************************* |
| * Copyright (c) 2004, 2008 John Krasnay and others. |
| * All rights reserved. This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License v1.0 |
| * which accompanies this distribution, and is available at |
| * http://www.eclipse.org/legal/epl-v10.html |
| * |
| * Contributors: |
| * John Krasnay - initial API and implementation |
| *******************************************************************************/ |
| package org.eclipse.wst.xml.vex.core.internal.layout; |
| |
| import java.util.ArrayList; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.Set; |
| |
| import org.eclipse.wst.xml.vex.core.internal.core.Caret; |
| import org.eclipse.wst.xml.vex.core.internal.core.Color; |
| import org.eclipse.wst.xml.vex.core.internal.core.ColorResource; |
| import org.eclipse.wst.xml.vex.core.internal.core.FontMetrics; |
| import org.eclipse.wst.xml.vex.core.internal.core.Graphics; |
| import org.eclipse.wst.xml.vex.core.internal.core.Insets; |
| import org.eclipse.wst.xml.vex.core.internal.core.IntRange; |
| import org.eclipse.wst.xml.vex.core.internal.css.CSS; |
| import org.eclipse.wst.xml.vex.core.internal.css.StyleSheet; |
| import org.eclipse.wst.xml.vex.core.internal.css.Styles; |
| import org.eclipse.wst.xml.vex.core.internal.dom.Document; |
| import org.eclipse.wst.xml.vex.core.internal.dom.Element; |
| import org.eclipse.wst.xml.vex.core.internal.dom.Position; |
| |
| /** |
| * Base class of block boxes that can contain other block boxes. This class |
| * implements the layout method and various navigation methods. Subclasses must |
| * implement the createChildren method. |
| * |
| * Subclasses can be anonymous or non-anonymous (i.e. generated by an element). |
| * Since the vast majority of instances will be non-anonymous, this class can |
| * manage the element and top and bottom margins without too much inefficiency. |
| * |
| * <p> |
| * Subclasses that can be anonymous must override the getStartPosition and |
| * getEndPosition classes to return the range covered by the box. |
| * </p> |
| */ |
| public abstract class AbstractBlockBox extends AbstractBox implements BlockBox { |
| |
| /** |
| * Class constructor for non-anonymous boxes. |
| * |
| * @param context |
| * LayoutContext being used. |
| * @param parent |
| * Parent box. |
| * @param element |
| * Element associated with this box. anonymous box. |
| */ |
| public AbstractBlockBox(LayoutContext context, BlockBox parent, |
| Element element) { |
| |
| this.parent = parent; |
| this.element = element; |
| |
| Styles styles = context.getStyleSheet().getStyles(element); |
| int parentWidth = parent.getWidth(); |
| this.marginTop = styles.getMarginTop().get(parentWidth); |
| this.marginBottom = styles.getMarginBottom().get(parentWidth); |
| |
| } |
| |
| /** |
| * Class constructor for anonymous boxes. |
| * |
| * @param context |
| * LayoutContext to use. |
| * @param parent |
| * Parent box. |
| * @param startOffset |
| * Start of the range covered by the box. |
| * @param endOffset |
| * End of the range covered by the box. |
| */ |
| public AbstractBlockBox(LayoutContext context, BlockBox parent, |
| int startOffset, int endOffset) { |
| this.parent = parent; |
| this.marginTop = 0; |
| this.marginBottom = 0; |
| |
| Document doc = context.getDocument(); |
| this.startPosition = doc.createPosition(startOffset); |
| this.endPosition = doc.createPosition(endOffset); |
| } |
| |
| /** |
| * Walks the box tree and returns the nearest enclosing element. |
| */ |
| protected Element findContainingElement() { |
| BlockBox box = this; |
| Element element = box.getElement(); |
| while (element == null) { |
| box = box.getParent(); |
| element = box.getElement(); |
| } |
| return element; |
| } |
| |
| /** |
| * Returns this box's children as an array of BlockBoxes. |
| */ |
| protected BlockBox[] getBlockChildren() { |
| return (BlockBox[]) this.getChildren(); |
| } |
| |
| public Caret getCaret(LayoutContext context, int offset) { |
| |
| // If we haven't yet laid out this block, estimate the caret. |
| if (this.getLayoutState() != LAYOUT_OK) { |
| int relative = offset - this.getStartOffset(); |
| int size = this.getEndOffset() - this.getStartOffset(); |
| int y = 0; |
| if (size > 0) { |
| y = this.getHeight() * relative / size; |
| } |
| return new HCaret(0, y, this.getHCaretWidth()); |
| } |
| |
| int y; |
| |
| Box[] children = this.getContentChildren(); |
| for (int i = 0; i < children.length; i++) { |
| |
| if (offset < children[i].getStartOffset()) { |
| if (i > 0) { |
| y = (children[i - 1].getY() + children[i - 1].getHeight() + children[i] |
| .getY()) / 2; |
| } else { |
| y = 0; |
| } |
| return new HCaret(0, y, this.getHCaretWidth()); |
| } |
| |
| if (offset >= children[i].getStartOffset() |
| && offset <= children[i].getEndOffset()) { |
| |
| Caret caret = children[i].getCaret(context, offset); |
| caret.translate(children[i].getX(), children[i].getY()); |
| return caret; |
| } |
| } |
| |
| if (this.hasChildren()) { |
| y = this.getHeight(); |
| } else { |
| y = this.getHeight() / 2; |
| } |
| |
| return new HCaret(0, y, this.getHCaretWidth()); |
| } |
| |
| public Box[] getChildren() { |
| return this.children; |
| } |
| |
| /** |
| * Return an array of children that contain content. |
| */ |
| protected BlockBox[] getContentChildren() { |
| Box[] children = this.getChildren(); |
| List contentChildren = new ArrayList(children.length); |
| for (int i = 0; i < children.length; i++) { |
| if (children[i].hasContent()) { |
| contentChildren.add(children[i]); |
| } |
| } |
| return (BlockBox[]) contentChildren |
| .toArray(new BlockBox[contentChildren.size()]); |
| } |
| |
| public Element getElement() { |
| return this.element; |
| } |
| |
| public int getEndOffset() { |
| Element element = this.getElement(); |
| if (element != null) { |
| return element.getEndOffset(); |
| } else if (this.getEndPosition() != null) { |
| return this.getEndPosition().getOffset(); |
| } else { |
| throw new IllegalStateException(); |
| } |
| } |
| |
| /** |
| * Returns the estimated size of the box, based on the the current font size |
| * and the number of characters covered by the box. This is a utility method |
| * that can be used in implementation of setInitialSize. It assumes the |
| * width of the box has already been correctly set. |
| * |
| * @param context |
| * LayoutContext to use. |
| */ |
| protected int getEstimatedHeight(LayoutContext context) { |
| |
| Element element = this.findContainingElement(); |
| Styles styles = context.getStyleSheet().getStyles(element); |
| int charCount = this.getEndOffset() - this.getStartOffset(); |
| |
| float fontSize = styles.getFontSize(); |
| float lineHeight = styles.getLineHeight(); |
| float estHeight = lineHeight * fontSize * 0.6f * charCount |
| / this.getWidth(); |
| |
| return Math.round(Math.max(estHeight, lineHeight)); |
| } |
| |
| public LineBox getFirstLine() { |
| if (this.hasChildren()) { |
| BlockBox firstChild = (BlockBox) this.getChildren()[0]; |
| return firstChild.getFirstLine(); |
| } else { |
| return null; |
| } |
| } |
| |
| /** |
| * Returns the width of the horizontal caret. This is overridden by TableBox |
| * to return a caret that is the full width of the table. |
| */ |
| protected int getHCaretWidth() { |
| return H_CARET_LENGTH; |
| } |
| |
| public Insets getInsets(LayoutContext context, int containerWidth) { |
| |
| if (this.getElement() != null) { |
| Styles styles = context.getStyleSheet() |
| .getStyles(this.getElement()); |
| |
| int top = this.marginTop + styles.getBorderTopWidth() |
| + styles.getPaddingTop().get(containerWidth); |
| |
| int left = styles.getMarginLeft().get(containerWidth) |
| + styles.getBorderLeftWidth() |
| + styles.getPaddingLeft().get(containerWidth); |
| |
| int bottom = this.marginBottom + styles.getBorderBottomWidth() |
| + styles.getPaddingBottom().get(containerWidth); |
| |
| int right = styles.getMarginRight().get(containerWidth) |
| + styles.getBorderRightWidth() |
| + styles.getPaddingRight().get(containerWidth); |
| |
| return new Insets(top, left, bottom, right); |
| } else { |
| return new Insets(this.marginTop, 0, this.marginBottom, 0); |
| } |
| } |
| |
| public LineBox getLastLine() { |
| if (this.hasChildren()) { |
| BlockBox lastChild = (BlockBox) this.getChildren()[this |
| .getChildren().length - 1]; |
| return lastChild.getLastLine(); |
| } else { |
| return null; |
| } |
| } |
| |
| /** |
| * Returns the layout state of this box. |
| */ |
| protected byte getLayoutState() { |
| return this.layoutState; |
| } |
| |
| public int getLineEndOffset(int offset) { |
| BlockBox[] children = this.getContentChildren(); |
| for (int i = 0; i < children.length; i++) { |
| if (children[i].containsOffset(offset)) { |
| return children[i].getLineEndOffset(offset); |
| } |
| } |
| return offset; |
| } |
| |
| public int getLineStartOffset(int offset) { |
| BlockBox[] children = this.getContentChildren(); |
| for (int i = 0; i < children.length; i++) { |
| if (children[i].containsOffset(offset)) { |
| return children[i].getLineStartOffset(offset); |
| } |
| } |
| return offset; |
| } |
| |
| public int getMarginBottom() { |
| return this.marginBottom; |
| } |
| |
| public int getMarginTop() { |
| return this.marginTop; |
| } |
| |
| public int getNextLineOffset(LayoutContext context, int offset, int x) { |
| |
| // |
| // This algorithm works when this block owns the offsets between |
| // its children. |
| // |
| |
| if (offset == this.getEndOffset()) { |
| return -1; |
| } |
| |
| BlockBox[] children = this.getContentChildren(); |
| |
| if (offset < this.getStartOffset() && children.length > 0 |
| && children[0].getStartOffset() > this.getStartOffset()) { |
| // |
| // If there's an offset before the first child, put the caret there. |
| // |
| return this.getStartOffset(); |
| } |
| |
| for (int i = 0; i < children.length; i++) { |
| BlockBox child = children[i]; |
| if (offset <= child.getEndOffset()) { |
| int newOffset = child.getNextLineOffset(context, offset, x |
| - child.getX()); |
| if (newOffset < 0 /* && i < children.length-1 */) { |
| return child.getEndOffset() + 1; |
| } else { |
| return newOffset; |
| } |
| } |
| } |
| |
| return this.getEndOffset(); |
| } |
| |
| public BlockBox getParent() { |
| return this.parent; |
| } |
| |
| public int getPreviousLineOffset(LayoutContext context, int offset, int x) { |
| |
| if (offset == this.getStartOffset()) { |
| return -1; |
| } |
| |
| BlockBox[] children = this.getContentChildren(); |
| |
| if (offset > this.getEndOffset() |
| && children.length > 0 |
| && children[children.length - 1].getEndOffset() < this |
| .getEndOffset()) { |
| // |
| // If there's an offset after the last child, put the caret there. |
| // |
| return this.getEndOffset(); |
| } |
| |
| for (int i = children.length; i > 0; i--) { |
| BlockBox child = children[i - 1]; |
| if (offset >= child.getStartOffset()) { |
| int newOffset = child.getPreviousLineOffset(context, offset, x |
| - child.getX()); |
| if (newOffset < 0 && i > 0) { |
| return child.getStartOffset() - 1; |
| } else { |
| return newOffset; |
| } |
| } |
| } |
| |
| return this.getStartOffset(); |
| } |
| |
| public int getStartOffset() { |
| Element element = this.getElement(); |
| if (element != null) { |
| return element.getStartOffset() + 1; |
| } else if (this.getStartPosition() != null) { |
| return this.getStartPosition().getOffset(); |
| } else { |
| throw new IllegalStateException(); |
| } |
| } |
| |
| public boolean hasContent() { |
| return true; |
| } |
| |
| public void invalidate(boolean direct) { |
| |
| if (direct) { |
| this.layoutState = LAYOUT_REDO; |
| } else { |
| this.layoutState = LAYOUT_PROPAGATE; |
| } |
| |
| if (this.getParent() instanceof AbstractBlockBox) { |
| ((AbstractBlockBox) this.getParent()).invalidate(false); |
| } |
| } |
| |
| public boolean isAnonymous() { |
| return this.getElement() == null; |
| } |
| |
| /** |
| * Call the given callback for each child matching one of the given display |
| * styles. Any nodes that do not match one of the given display types cause |
| * the onRange callback to be called, with a range covering all such |
| * contiguous nodes. |
| * |
| * @param styleSheet |
| * StyleSheet from which to determine display styles. |
| * @param displayStyles |
| * Display types to be explicitly recognized. |
| * @param callback |
| * DisplayStyleCallback through which the caller is notified of |
| * matching elements and non-matching ranges. |
| */ |
| protected void iterateChildrenByDisplayStyle(StyleSheet styleSheet, |
| Set displayStyles, ElementOrRangeCallback callback) { |
| LayoutUtils.iterateChildrenByDisplayStyle(styleSheet, displayStyles, |
| this.findContainingElement(), this.getStartOffset(), this |
| .getEndOffset(), callback); |
| } |
| |
| public void paint(LayoutContext context, int x, int y) { |
| |
| if (this.skipPaint(context, x, y)) { |
| return; |
| } |
| |
| boolean drawBorders = !context.isElementSelected(this.getElement()); |
| |
| this.drawBox(context, x, y, this.getParent().getWidth(), drawBorders); |
| |
| this.paintChildren(context, x, y); |
| |
| this.paintSelectionFrame(context, x, y, true); |
| } |
| |
| /** |
| * Default implementation. Width is calculated as the parent's width minus |
| * this box's insets. Height is calculated by getEstimatedHeight. |
| */ |
| public void setInitialSize(LayoutContext context) { |
| int parentWidth = this.getParent().getWidth(); |
| Insets insets = this.getInsets(context, parentWidth); |
| this.setWidth(Math.max(0, parentWidth - insets.getLeft() |
| - insets.getRight())); |
| this.setHeight(this.getEstimatedHeight(context)); |
| } |
| |
| public int viewToModel(LayoutContext context, int x, int y) { |
| |
| Box[] children = this.getChildren(); |
| |
| if (children == null) { |
| int charCount = this.getEndOffset() - this.getStartOffset() - 1; |
| if (charCount == 0 || this.getHeight() == 0) { |
| return this.getEndOffset(); |
| } else { |
| return this.getStartOffset() + charCount * y / this.getHeight(); |
| } |
| } else { |
| for (int i = 0; i < children.length; i++) { |
| Box child = children[i]; |
| if (!child.hasContent()) { |
| continue; |
| } |
| if (y < child.getY()) { |
| return child.getStartOffset() - 1; |
| } else if (y < child.getY() + child.getHeight()) { |
| return child.viewToModel(context, x - child.getX(), y |
| - child.getY()); |
| } |
| } |
| } |
| |
| return this.getEndOffset(); |
| } |
| |
| // ===================================================== PRIVATE |
| |
| private BlockBox parent; |
| private Box[] children; |
| |
| /** |
| * Paint a frame that indicates a block element box has been selected. |
| * |
| * @param context |
| * LayoutContext to use. |
| * @param x |
| * x-coordinate at which to draw |
| * @param y |
| * y-coordinate at which to draw. |
| * @param selected |
| */ |
| protected void paintSelectionFrame(LayoutContext context, int x, int y, |
| boolean selected) { |
| |
| Element element = this.getElement(); |
| Element parent = element == null ? null : element.getParent(); |
| |
| boolean paintFrame = context.isElementSelected(element) |
| && !context.isElementSelected(parent); |
| |
| if (!paintFrame) { |
| return; |
| } |
| |
| Graphics g = context.getGraphics(); |
| ColorResource foreground; |
| ColorResource background; |
| |
| if (selected) { |
| foreground = g.getSystemColor(ColorResource.SELECTION_FOREGROUND); |
| background = g.getSystemColor(ColorResource.SELECTION_BACKGROUND); |
| } else { |
| foreground = g.createColor(new Color(0, 0, 0)); |
| background = g.createColor(new Color(0xcc, 0xcc, 0xcc)); |
| } |
| |
| FontMetrics fm = g.getFontMetrics(); |
| ColorResource oldColor = g.setColor(background); |
| g.setLineStyle(Graphics.LINE_SOLID); |
| g.setLineWidth(1); |
| int tabWidth = g.stringWidth(this.getElement().getName()) |
| + fm.getLeading(); |
| int tabHeight = fm.getHeight(); |
| int tabX = x + this.getWidth() - tabWidth; |
| int tabY = y + this.getHeight() - tabHeight; |
| g.drawRect(x, y, this.getWidth(), this.getHeight()); |
| g.fillRect(tabX, tabY, tabWidth, tabHeight); |
| g.setColor(foreground); |
| g.drawString(this.getElement().getName(), tabX + fm.getLeading() / 2, |
| tabY); |
| |
| g.setColor(oldColor); |
| if (!selected) { |
| foreground.dispose(); |
| background.dispose(); |
| } |
| } |
| |
| /** Layout is OK */ |
| public static final byte LAYOUT_OK = 0; |
| |
| /** My layout is OK, but one of my children needs to be laid out */ |
| public static final byte LAYOUT_PROPAGATE = 1; |
| |
| /** I need to be laid out */ |
| public static final byte LAYOUT_REDO = 2; |
| |
| private byte layoutState = LAYOUT_REDO; |
| |
| public IntRange layout(LayoutContext context, int top, int bottom) { |
| |
| int repaintStart = Integer.MAX_VALUE; |
| int repaintEnd = 0; |
| boolean repaintToBottom = false; |
| int originalHeight = this.getHeight(); |
| |
| if (this.layoutState == LAYOUT_REDO) { |
| |
| // System.out.println("Redo layout of " + |
| // this.getElement().getName()); |
| |
| List childList = this.createChildren(context); |
| this.children = (BlockBox[]) childList |
| .toArray(new BlockBox[childList.size()]); |
| |
| // Even though we position children after layout, we have to |
| // do a preliminary positioning here so we now which ones |
| // overlap our layout band |
| for (int i = 0; i < this.children.length; i++) { |
| BlockBox child = (BlockBox) this.children[i]; |
| child.setInitialSize(context); |
| } |
| this.positionChildren(context); |
| |
| // repaint everything |
| repaintToBottom = true; |
| repaintStart = 0; |
| } |
| |
| Box[] children = this.getChildren(); |
| for (int i = 0; i < children.length; i++) { |
| if (children[i] instanceof BlockBox) { |
| BlockBox child = (BlockBox) children[i]; |
| if (top <= child.getY() + child.getHeight() |
| && bottom >= child.getY()) { |
| |
| IntRange repaintRange = child.layout(context, top |
| - child.getY(), bottom - child.getY()); |
| if (repaintRange != null) { |
| repaintStart = Math.min(repaintStart, repaintRange |
| .getStart() |
| + child.getY()); |
| repaintEnd = Math.max(repaintEnd, repaintRange.getEnd() |
| + child.getY()); |
| } |
| } |
| } |
| } |
| |
| int childRepaintStart = this.positionChildren(context); |
| if (childRepaintStart != -1) { |
| repaintToBottom = true; |
| repaintStart = Math.min(repaintStart, childRepaintStart); |
| } |
| |
| this.layoutState = LAYOUT_OK; |
| |
| if (repaintToBottom) { |
| repaintEnd = Math.max(originalHeight, this.getHeight()); |
| } |
| |
| if (repaintStart < repaintEnd) { |
| return new IntRange(repaintStart, repaintEnd); |
| } else { |
| return null; |
| } |
| } |
| |
| protected abstract List createChildren(LayoutContext context); |
| |
| /** |
| * Creates a list of block boxes for a given document range. beforeInlines |
| * and afterInlines are prepended/appended to the first/last block child, |
| * and each may be null. |
| */ |
| protected List createBlockBoxes(LayoutContext context, int startOffset, |
| int endOffset, int width, List beforeInlines, List afterInlines) { |
| |
| List blockBoxes = new ArrayList(); |
| List pendingInlines = new ArrayList(); |
| |
| if (beforeInlines != null) { |
| pendingInlines.addAll(beforeInlines); |
| } |
| |
| Document document = context.getDocument(); |
| |
| Element element = document.findCommonElement(startOffset, |
| endOffset); |
| |
| if (startOffset == endOffset) { |
| int relOffset = startOffset - element.getStartOffset(); |
| pendingInlines.add(new PlaceholderBox(context, element, relOffset)); |
| } else { |
| |
| BlockInlineIterator iter = new BlockInlineIterator(context, |
| element, startOffset, endOffset); |
| |
| while (true) { |
| |
| Object next = iter.next(); |
| |
| if (next == null) { |
| break; |
| } |
| |
| if (next instanceof IntRange) { |
| |
| IntRange range = (IntRange) next; |
| |
| InlineElementBox.InlineBoxes inlineBoxes = InlineElementBox |
| .createInlineBoxes(context, element, range |
| .getStart(), range.getEnd()); |
| pendingInlines.addAll(inlineBoxes.boxes); |
| pendingInlines.add(new PlaceholderBox(context, element, |
| range.getEnd() - element.getStartOffset())); |
| |
| } else { |
| |
| if (pendingInlines.size() > 0) { |
| blockBoxes.add(ParagraphBox.create(context, element, |
| pendingInlines, width)); |
| pendingInlines.clear(); |
| } |
| |
| if (isTableChild(context, next)) { |
| |
| // Consume continguous table children and create an |
| // anonymous table. |
| |
| int tableStartOffset = ((Element) next) |
| .getStartOffset(); |
| int tableEndOffset = -1; // dummy to hide warning |
| while (isTableChild(context, next)) { |
| tableEndOffset = ((Element) next).getEndOffset() + 1; |
| next = iter.next(); |
| } |
| |
| // add anonymous table |
| blockBoxes.add(new TableBox(context, this, |
| tableStartOffset, tableEndOffset)); |
| |
| if (next == null) { |
| break; |
| } else { |
| iter.push(next); |
| } |
| |
| } else { // next is a block box element |
| Element blockElement = (Element) next; |
| blockBoxes.add(context.getBoxFactory().createBox( |
| context, blockElement, this, width)); |
| } |
| } |
| } |
| } |
| |
| if (afterInlines != null) { |
| pendingInlines.addAll(afterInlines); |
| } |
| |
| if (pendingInlines.size() > 0) { |
| blockBoxes.add(ParagraphBox.create(context, element, |
| pendingInlines, width)); |
| pendingInlines.clear(); |
| } |
| |
| return blockBoxes; |
| } |
| |
| private class BlockInlineIterator { |
| |
| public BlockInlineIterator(LayoutContext context, Element element, |
| int startOffset, int endOffset) { |
| this.context = context; |
| this.element = element; |
| this.startOffset = startOffset; |
| this.endOffset = endOffset; |
| } |
| |
| /** |
| * Returns the next block element or inline range, or null if we're at |
| * the end. |
| */ |
| public Object next() { |
| if (this.pushStack.size() > 0) { |
| return this.pushStack.removeLast(); |
| } else if (startOffset == endOffset) { |
| return null; |
| } else { |
| Element blockElement = findNextBlockElement(this.context, |
| this.element, startOffset, endOffset); |
| if (blockElement == null) { |
| if (startOffset < endOffset) { |
| IntRange result = new IntRange(startOffset, endOffset); |
| startOffset = endOffset; |
| return result; |
| } else { |
| return null; |
| } |
| } else if (blockElement.getStartOffset() > startOffset) { |
| this.pushStack.addLast(blockElement); |
| IntRange result = new IntRange(startOffset, blockElement |
| .getStartOffset()); |
| startOffset = blockElement.getEndOffset() + 1; |
| return result; |
| } else { |
| startOffset = blockElement.getEndOffset() + 1; |
| return blockElement; |
| } |
| } |
| } |
| |
| public Object peek() { |
| if (this.pushStack.size() == 0) { |
| Object next = next(); |
| if (next == null) { |
| return null; |
| } else { |
| push(next); |
| } |
| } |
| return pushStack.getLast(); |
| } |
| |
| public void push(Object pushed) { |
| this.pushStack.addLast(pushed); |
| } |
| |
| private LayoutContext context; |
| private Element element; |
| private int startOffset; |
| private int endOffset; |
| private LinkedList pushStack = new LinkedList(); |
| } |
| |
| protected boolean hasChildren() { |
| return this.getChildren() != null && this.getChildren().length > 0; |
| } |
| |
| /** |
| * Positions the children of this box. Vertical margins are collapsed here. |
| * Returns the vertical offset of the top of the first child to move, or -1 |
| * if not children were actually moved. |
| */ |
| protected int positionChildren(LayoutContext context) { |
| |
| int childY = 0; |
| int prevMargin = 0; |
| BlockBox[] children = this.getBlockChildren(); |
| int repaintStart = -1; |
| |
| Styles styles = null; |
| |
| if (!this.isAnonymous()) { |
| styles = context.getStyleSheet().getStyles(this.getElement()); |
| } |
| |
| if (styles != null && children.length > 0) { |
| if (styles.getBorderTopWidth() |
| + styles.getPaddingTop().get(this.getWidth()) == 0) { |
| // first child's top margin collapses into ours |
| this.marginTop = Math.max(this.marginTop, children[0] |
| .getMarginTop()); |
| childY -= children[0].getMarginTop(); |
| } |
| } |
| |
| for (int i = 0; i < children.length; i++) { |
| |
| Insets insets = children[i].getInsets(context, this.getWidth()); |
| |
| childY += insets.getTop(); |
| |
| if (i > 0) { |
| childY -= Math.min(prevMargin, children[i].getMarginTop()); |
| } |
| |
| if (repaintStart == -1 && children[i].getY() != childY) { |
| repaintStart = Math.min(children[i].getY(), childY); |
| } |
| |
| children[i].setX(insets.getLeft()); |
| children[i].setY(childY); |
| |
| childY += children[i].getHeight() + insets.getBottom(); |
| prevMargin = children[i].getMarginBottom(); |
| } |
| |
| if (styles != null && children.length > 0) { |
| if (styles.getBorderBottomWidth() |
| + styles.getPaddingBottom().get(this.getWidth()) == 0) { |
| // last child's bottom margin collapses into ours |
| this.marginBottom = Math.max(this.marginBottom, prevMargin); |
| childY -= prevMargin; |
| } |
| } |
| |
| this.setHeight(childY); |
| |
| return repaintStart; |
| } |
| |
| /** |
| * Sets the layout state of the box. |
| * |
| * @param layoutState |
| * One of the LAYOUT_* constants |
| */ |
| protected void setLayoutState(byte layoutState) { |
| this.layoutState = layoutState; |
| } |
| |
| // ========================================================= PRIVATE |
| /** The length, in pixels, of the horizontal caret between block boxes */ |
| private static final int H_CARET_LENGTH = 20; |
| |
| /** |
| * Element with which we are associated. For anonymous boxes, this is null. |
| */ |
| private Element element; |
| |
| /* |
| * We cache the top and bottom margins, since they may be affected by our |
| * children. |
| */ |
| private int marginTop; |
| private int marginBottom; |
| |
| /** |
| * Start position of an anonymous box. For non-anonymous boxes, this is |
| * null. |
| */ |
| private Position startPosition; |
| |
| /** |
| * End position of an anonymous box. For non-anonymous boxes, this is null. |
| */ |
| private Position endPosition; |
| |
| /** |
| * Searches for the next block-formatted child. |
| * |
| * @param context |
| * LayoutContext to use. |
| * @param element |
| * Element within which to search. |
| * @param startOffset |
| * The offset at which to start the search. |
| * @param endOffset |
| * The offset at which to end the search. |
| */ |
| private static Element findNextBlockElement(LayoutContext context, |
| Element element, int startOffset, int endOffset) { |
| |
| Element[] children = element.getChildElements(); |
| for (int i = 0; i < children.length; i++) { |
| Element child = children[i]; |
| if (child.getEndOffset() < startOffset) { |
| continue; |
| } else if (child.getStartOffset() >= endOffset) { |
| break; |
| } else { |
| Styles styles = context.getStyleSheet().getStyles(child); |
| if (!styles.getDisplay().equals(CSS.INLINE)) { // TODO do proper |
| // block display |
| // determination |
| return child; |
| } else { |
| Element fromChild = findNextBlockElement(context, child, |
| startOffset, endOffset); |
| if (fromChild != null) { |
| return fromChild; |
| } |
| } |
| } |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Return the end position of an anonymous box. The default implementation |
| * returns null. |
| */ |
| private Position getEndPosition() { |
| return this.endPosition; |
| } |
| |
| /** |
| * Return the start position of an anonymous box. The default implementation |
| * returns null. |
| */ |
| private Position getStartPosition() { |
| return this.startPosition; |
| } |
| |
| private boolean isTableChild(LayoutContext context, Object rangeOrElement) { |
| if (rangeOrElement != null && rangeOrElement instanceof Element) { |
| return LayoutUtils.isTableChild(context.getStyleSheet(), |
| (Element) rangeOrElement); |
| } else { |
| return false; |
| } |
| } |
| |
| } |