blob: 985fb99deb19a59c6370f6dfd1187ea2f7e72e6a [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2004, 2014 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
* Igor Jacy Lino Campista - Java 5 warnings fixed (bug 311325)
* Carsten Hiesserich - added support for processing instructions / includes
*******************************************************************************/
package org.eclipse.vex.core.internal.layout;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.vex.core.internal.core.Drawable;
import org.eclipse.vex.core.internal.core.FontMetrics;
import org.eclipse.vex.core.internal.core.FontResource;
import org.eclipse.vex.core.internal.core.Graphics;
import org.eclipse.vex.core.internal.core.Rectangle;
import org.eclipse.vex.core.internal.css.CSS;
import org.eclipse.vex.core.internal.css.Styles;
import org.eclipse.vex.core.provisional.dom.BaseNodeVisitor;
import org.eclipse.vex.core.provisional.dom.ContentRange;
import org.eclipse.vex.core.provisional.dom.IComment;
import org.eclipse.vex.core.provisional.dom.IElement;
import org.eclipse.vex.core.provisional.dom.IIncludeNode;
import org.eclipse.vex.core.provisional.dom.INode;
import org.eclipse.vex.core.provisional.dom.IProcessingInstruction;
import org.eclipse.vex.core.provisional.dom.IText;
/**
* An inline box that represents an inline element. This box is responsible for creating and laying out its child boxes.
*/
public class InlineElementBox extends CompositeInlineBox {
private final INode node;
private InlineBox[] children;
private InlineBox firstContentChild = null;
private InlineBox lastContentChild = null;
private int baseline;
private int halfLeading;
/**
* Class constructor, called by the createInlineBoxes static factory method. The Box created here is only temporary,
* it will be replaced by the split method.
*
* @param context
* LayoutContext to use.
* @param node
* Element that generated this box
* @param startOffset
* Start offset of the range being rendered, which may be arbitrarily before or inside the element.
* @param endOffset
* End offset of the range being rendered, which may be arbitrarily after or inside the element.
*/
private InlineElementBox(final LayoutContext context, final INode node, final int startOffset, final int endOffset) {
this.node = node;
final List<InlineBox> childList = new ArrayList<InlineBox>();
final Styles styles = context.getStyleSheet().getStyles(node);
if (startOffset <= node.getStartOffset()) {
// space for the left margin/border/padding
final int space = styles.getMarginLeft().get(0) + styles.getBorderLeftWidth() + styles.getPaddingLeft().get(0);
if (space > 0) {
childList.add(new SpaceBox(space, 1));
}
final boolean showLeftMarker = styles.getInlineMarker().equals(CSS.NORMAL);
// :before content
final IElement beforeElement = context.getStyleSheet().getPseudoElementBefore(node);
if (beforeElement != null) {
childList.addAll(LayoutUtils.createGeneratedInlines(context, beforeElement, showLeftMarker ? StaticTextBox.NO_MARKER : StaticTextBox.START_MARKER));
}
// left marker
if (showLeftMarker) {
childList.add(createLeftMarker(node, styles));
}
}
// background image
if (styles.hasBackgroundImage() && !styles.getDisplay().equalsIgnoreCase(CSS.NONE)) {
final ImageBox imageBox = ImageBox.createWithHeight(getNode(), context, styles.getLineHeight());
if (imageBox != null) {
childList.add(imageBox);
}
}
if (styles.isContentDefined()) {
// A CSS 'content' definition overrides the actual content
final String content = LayoutUtils.getGeneratedContent(context, styles, node);
final InlineBox child = new StaticTextBox(context, node, content);
childList.add(child);
firstContentChild = lastContentChild = child;
} else {
final InlineBoxes inlines = createInlineBoxes(context, node, new ContentRange(startOffset, endOffset));
childList.addAll(inlines.boxes);
firstContentChild = inlines.firstContentBox;
lastContentChild = inlines.lastContentBox;
}
if (endOffset > node.getEndOffset()) {
childList.add(new PlaceholderBox(context, node, node.getEndOffset() - node.getStartOffset()));
final boolean showRightMarker = styles.getInlineMarker().equals(CSS.NORMAL);
// trailing marker
if (showRightMarker) {
childList.add(createRightMarker(node, styles));
}
// :after content
final IElement afterElement = context.getStyleSheet().getPseudoElementAfter(node);
if (afterElement != null) {
childList.addAll(LayoutUtils.createGeneratedInlines(context, afterElement, showRightMarker ? StaticTextBox.NO_MARKER : StaticTextBox.END_MARKER));
}
// space for the right margin/border/padding
final int space = styles.getMarginRight().get(0) + styles.getBorderRightWidth() + styles.getPaddingRight().get(0);
if (space > 0) {
childList.add(new SpaceBox(space, 1));
}
}
children = childList.toArray(new InlineBox[childList.size()]);
}
/**
* Class constructor. This constructor is called by the split method.
*
* @param context
* LayoutContext used for the layout.
* @param node
* Node to which this box applies.
* @param children
* Child boxes.
*/
private InlineElementBox(final LayoutContext context, final INode node, final InlineBox[] children) {
this.node = node;
this.children = children;
layout(context);
for (final InlineBox child : children) {
if (child.hasContent()) {
if (firstContentChild == null) {
firstContentChild = child;
}
lastContentChild = child;
}
}
}
/**
* @see org.eclipse.vex.core.internal.layout.InlineBox#getBaseline()
*/
@Override
public int getBaseline() {
return baseline;
}
/**
* @see org.eclipse.vex.core.internal.layout.Box#getChildren()
*/
@Override
public Box[] getChildren() {
return children;
}
/**
* Returns the element associated with this box.
*/
@Override
public INode getNode() {
return node;
}
/**
* @see org.eclipse.vex.core.internal.layout.Box#getEndOffset()
*/
@Override
public int getEndOffset() {
if (lastContentChild == null) {
return getNode().getEndOffset();
} else {
return lastContentChild.getEndOffset();
}
}
/**
* @see org.eclipse.vex.core.internal.layout.Box#getStartOffset()
*/
@Override
public int getStartOffset() {
if (firstContentChild == null) {
return getNode().getStartOffset();
} else {
return firstContentChild.getStartOffset();
}
}
/**
* Override to paint background and borders.
*
* @see org.eclipse.vex.core.internal.layout.AbstractBox#paint(org.eclipse.vex.core.internal.layout.LayoutContext,
* int, int)
*/
@Override
public void paint(final LayoutContext context, final int x, final int y) {
this.drawBox(context, x, y, 0, true); // TODO CSS violation
super.paint(context, x, y);
}
@Override
public Pair split(final LayoutContext context, final InlineBox[] lefts, final InlineBox[] rights, final int remaining) {
InlineElementBox left = null;
InlineElementBox right = null;
if (lefts.length > 0 || rights.length == 0) {
left = new InlineElementBox(context, getNode(), lefts);
}
if (rights.length > 0) {
// We reuse this element instead of creating a new one
children = rights;
for (final InlineBox child : children) {
if (child.hasContent()) {
// The lastContentChild is already set in this instance an did not change
firstContentChild = child;
break;
}
}
if (left == null && remaining >= 0) {
// There is no left box, and the right box fits without further splitting, so we have to calculate the size here
layout(context);
}
right = this;
}
return new Pair(left, right, remaining);
}
@Override
public String toString() {
final StringBuffer sb = new StringBuffer();
if (getStartOffset() == getNode().getStartOffset() + 1) {
sb.append("<");
sb.append(getNode());
sb.append(">");
}
final Box[] children = getChildren();
for (final Box element2 : children) {
sb.append(element2);
}
if (getEndOffset() == getNode().getEndOffset()) {
sb.append("</");
sb.append(getNode());
sb.append(">");
}
return sb.toString();
}
/**
* Holds the results of the createInlineBoxes method.
*/
static class InlineBoxes {
/** List of generated boxes */
public List<InlineBox> boxes = new ArrayList<InlineBox>();
/** First generated box that has content */
public InlineBox firstContentBox;
/** Last generated box that has content */
public InlineBox lastContentBox;
}
/**
* Creates a list of inline boxes given a range of offsets. This method is used when creating both ParagraphBoxes
* and InlineElementBoxes.
*
* @param context
* LayoutContext to be used.
* @param node
* Parent containing both offsets
* @param startOffset
* The start of the range to convert to inline boxes.
* @param endOffset
* The end of the range to convert to inline boxes.
* @return
*/
static InlineBoxes createInlineBoxes(final LayoutContext context, final INode node, final ContentRange range) {
final InlineBoxes result = new InlineBoxes();
node.accept(new BaseNodeVisitor() {
@Override
public void visit(final IElement element) {
for (final INode childNode : element.children().in(range)) {
childNode.accept(new BaseNodeVisitor() {
@Override
public void visit(final IElement element) {
addPlaceholderBox(result, new PlaceholderBox(context, node, element.getStartOffset() - node.getStartOffset()));
final InlineBox child = new InlineElementBox(context, element, range.getStartOffset(), range.getEndOffset());
addChildInlineBox(result, child);
}
@Override
public void visit(final IComment comment) {
addPlaceholderBox(result, new PlaceholderBox(context, node, comment.getStartOffset() - node.getStartOffset()));
final InlineBox child = new InlineElementBox(context, comment, range.getStartOffset(), range.getEndOffset());
addChildInlineBox(result, child);
};
@Override
public void visit(final IIncludeNode include) {
addPlaceholderBox(result, new PlaceholderBox(context, node, include.getStartOffset() - node.getStartOffset()));
final InlineBox child = new IncludeInlineBox(context, include, range.getStartOffset(), range.getEndOffset());
addChildInlineBox(result, child);
}
@Override
public void visit(final IProcessingInstruction pi) {
addPlaceholderBox(result, new PlaceholderBox(context, node, pi.getStartOffset() - node.getStartOffset()));
final InlineBox child = new InlineElementBox(context, pi, range.getStartOffset(), range.getEndOffset());
addChildInlineBox(result, child);
};
@Override
public void visit(final IText text) {
final ContentRange boxRange = range.intersection(text.getRange());
final InlineBox child = new DocumentTextBox(context, node, boxRange.getStartOffset(), boxRange.getEndOffset());
addChildInlineBox(result, child);
}
});
}
}
@Override
public void visit(final IComment comment) {
final InlineBox child = new DocumentTextBox(context, comment, comment.getStartOffset() + 1, comment.getEndOffset() - 1);
addChildInlineBox(result, child);
}
@Override
public void visit(final IProcessingInstruction pi) {
final InlineBox child = new DocumentTextBox(context, pi, pi.getStartOffset() + 1, pi.getEndOffset() - 1);
addChildInlineBox(result, child);
}
});
return result;
}
private static void addPlaceholderBox(final InlineBoxes result, final PlaceholderBox placeholder) {
if (result.firstContentBox == null) {
result.firstContentBox = placeholder;
}
result.lastContentBox = placeholder;
result.boxes.add(placeholder);
}
private static void addChildInlineBox(final InlineBoxes result, final InlineBox child) {
if (result.firstContentBox == null) {
result.firstContentBox = child;
}
result.lastContentBox = child;
result.boxes.add(child);
}
// ========================================================== PRIVATE
private static InlineBox createLeftMarker(final INode node, final Styles styles) {
final int size = Math.round(0.5f * styles.getFontSize());
final int lift = Math.round(0.1f * styles.getFontSize());
final Drawable drawable = new Drawable() {
@Override
public void draw(final Graphics g, final int x, int y) {
g.setLineStyle(Graphics.LINE_SOLID);
g.setLineWidth(1);
y -= lift;
g.drawLine(x, y - size, x, y);
g.drawLine(x, y, x + size - 1, y - size / 2);
g.drawLine(x + size - 1, y - size / 2, x, y - size);
}
@Override
public Rectangle getBounds() {
return new Rectangle(0, -size, size, size);
}
};
return new DrawableBox(drawable, node, DrawableBox.START_MARKER);
}
private static InlineBox createRightMarker(final INode node, final Styles styles) {
final int size = Math.round(0.5f * styles.getFontSize());
final int lift = Math.round(0.1f * styles.getFontSize());
final Drawable drawable = new Drawable() {
@Override
public void draw(final Graphics g, final int x, int y) {
g.setLineStyle(Graphics.LINE_SOLID);
g.setLineWidth(1);
y -= lift;
g.drawLine(x + size - 1, y - size, x + size - 1, y);
g.drawLine(x + size - 1, y, x, y - size / 2);
g.drawLine(x, y - size / 2, x + size - 1, y - size);
}
@Override
public Rectangle getBounds() {
return new Rectangle(0, -size, size, size);
}
};
return new DrawableBox(drawable, node, DrawableBox.END_MARKER);
}
private void layout(final LayoutContext context) {
final Graphics g = context.getGraphics();
final Styles styles = context.getStyleSheet().getStyles(node);
final FontResource font = g.createFont(styles.getFont());
final FontResource oldFont = g.setFont(font);
final FontMetrics fm = g.getFontMetrics();
setHeight(styles.getLineHeight());
halfLeading = (styles.getLineHeight() - fm.getAscent() - fm.getDescent()) / 2;
baseline = halfLeading + fm.getAscent();
g.setFont(oldFont);
font.dispose();
int x = 0;
for (final InlineBox child : children) {
// TODO: honour the child's vertical-align property
child.setX(x);
child.alignOnBaseline(baseline);
x += child.getWidth();
}
setWidth(x);
}
}