org.cobraparser.html.renderer.RLine Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of Cobra Show documentation
Show all versions of Cobra Show documentation
Cobra is the rendering engine designed for LoboBrowser
/*
GNU LESSER GENERAL PUBLIC LICENSE
Copyright (C) 2006 The Lobo Project
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
Contact info: [email protected]
*/
/*
* Created on Apr 16, 2005
*/
package org.cobraparser.html.renderer;
import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Rectangle;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import org.cobraparser.html.domimpl.ModelNode;
import org.cobraparser.html.style.RenderState;
import cz.vutbr.web.css.CSSProperty.VerticalAlign;
/**
* @author J. H. S.
*/
class RLine extends BaseRCollection {
private final ArrayList<@NonNull Renderable> renderables = new ArrayList<>(8);
// private final RenderState startRenderState;
private int baseLineOffset;
private int desiredMaxWidth;
/**
* Offset where next renderable should be placed. This can be different to
* width.
*/
private int xoffset;
private boolean allowOverflow = false;
private boolean firstAllowOverflowWord = false;
public RLine(final ModelNode modelNode, final RenderableContainer container, final int x, final int y, final int desiredMaxWidth,
final int height,
final boolean initialAllowOverflow) {
// Note that in the case of RLine, modelNode is the context node
// at the beginning of the line, not a node that encloses the whole line.
super(container, modelNode);
this.x = x;
this.y = y;
this.height = height;
this.desiredMaxWidth = desiredMaxWidth;
// Layout here can always be "invalidated"
this.layoutUpTreeCanBeInvalidated = true;
this.allowOverflow = initialAllowOverflow;
}
public void setAllowOverflow(final boolean flag) {
if (flag != this.allowOverflow) {
this.allowOverflow = flag;
if (flag) {
// Set to true only if allowOverflow was
// previously false.
this.firstAllowOverflowWord = true;
}
}
}
public boolean isAllowOverflow() {
return this.allowOverflow;
}
/**
* This method should only be invoked when the line has no items yet.
*/
public void changeLimits(final int x, final int desiredMaxWidth) {
this.x = x;
this.desiredMaxWidth = desiredMaxWidth;
}
public int getBaselineOffset() {
return this.baseLineOffset;
}
@Override
protected void invalidateLayoutLocal() {
// Workaround for fact that RBlockViewport does not
// get validated or invalidated.
this.layoutUpTreeCanBeInvalidated = true;
}
/*
* (non-Javadoc)
*
* @see
* net.sourceforge.xamj.domimpl.markup.Renderable#paint(java.awt.Graphics)
*/
public void paint(final Graphics g) {
// Paint according to render state of the start of line first.
final RenderState rs = this.modelNode.getRenderState();
if ((rs != null) && (rs.getVisibility() != RenderState.VISIBILITY_VISIBLE)) {
// Just don't paint it.
return;
}
if (rs != null) {
final Color textColor = rs.getColor();
g.setColor(textColor);
final Font font = rs.getFont();
g.setFont(font);
}
// Note that partial paints of the line can only be done
// if all RStyleChanger's are applied first.
final Iterator i = this.renderables.iterator();
while (i.hasNext()) {
final Renderable r = i.next();
if (r instanceof RElement) {
// RElements should be translated.
final RElement relement = (RElement) r;
if (!relement.isDelegated()) {
final Graphics newG = g.create();
newG.translate(relement.getVisualX(), relement.getVisualY());
try {
relement.paint(newG);
} finally {
newG.dispose();
}
}
} else if (r instanceof BoundableRenderable) {
final BoundableRenderable br = (BoundableRenderable) r;
if (!br.isDelegated()) {
br.paintTranslated(g);
}
} else {
r.paint(g);
}
}
}
@Override
public boolean extractSelectionText(final StringBuffer buffer, final boolean inSelection, final RenderableSpot startPoint,
final RenderableSpot endPoint) {
final boolean result = super.extractSelectionText(buffer, inSelection, startPoint, endPoint);
if (result) {
final LineBreak br = this.lineBreak;
if (br != null) {
buffer.append(System.getProperty("line.separator"));
} else {
final ArrayList renderables = this.renderables;
final int size = renderables.size();
if ((size > 0) && !(renderables.get(size - 1) instanceof RBlank)) {
buffer.append(" ");
}
}
}
return result;
}
public final void addStyleChanger(final @NonNull RStyleChanger sc) {
this.renderables.add(sc);
}
public final void simplyAdd(final @NonNull Renderable r) {
this.renderables.add(r);
}
/**
* This method adds and positions a renderable in the line, if possible. Note
* that RLine does not set sizes, but only origins.
*
* @throws OverflowException
* Thrown if the renderable overflows the line. All overflowing
* renderables are added to the exception.
*/
public final void add(final Renderable renderable) throws OverflowException {
if (renderable instanceof RWord) {
this.addWord((RWord) renderable);
} else if (renderable instanceof RBlank) {
this.addBlank((RBlank) renderable);
} else if (renderable instanceof RElement) {
this.addElement((RElement) renderable);
} else if (renderable instanceof RSpacing) {
this.addSpacing((RSpacing) renderable);
} else if (renderable instanceof RStyleChanger) {
this.addStyleChanger((RStyleChanger) renderable);
} else if (renderable instanceof RFloatInfo) {
this.simplyAdd(renderable);
} else {
throw new IllegalArgumentException("Can't add " + renderable);
}
}
public final void addWord(final RWord rword) throws OverflowException {
// Check if it fits horzizontally
int offset = this.xoffset;
final int wiwidth = rword.width;
final boolean allowOverflow = this.allowOverflow;
final boolean firstAllowOverflowWord = this.firstAllowOverflowWord;
if (allowOverflow && firstAllowOverflowWord) {
this.firstAllowOverflowWord = false;
}
if ((!allowOverflow || firstAllowOverflowWord) && (offset != 0) && ((offset + wiwidth) > this.desiredMaxWidth)) {
final ArrayList renderables = this.renderables;
ArrayList overflow = null;
boolean cancel = false;
// Check if other words need to be overflown (for example,
// a word just before a markup tag adjacent to the word
// we're trying to add). An RBlank between words prevents
// a word from being overflown to the next line (and this
// is the usefulness of RBlank.)
int newOffset = offset;
int newWidth = offset;
for (int i = renderables.size(); --i >= 0;) {
final Renderable renderable = renderables.get(i);
if ((renderable instanceof RWord) || !(renderable instanceof BoundableRenderable)) {
if (overflow == null) {
overflow = new ArrayList<>();
}
if ((renderable != rword) && (renderable instanceof RWord) && (((RWord) renderable).getX() == 0)) {
// Can't overflow words starting at offset zero.
// Note that all or none should be overflown.
cancel = true;
// No need to set offset - set later.
break;
}
overflow.add(0, renderable);
renderables.remove(i);
} else {
if (renderable instanceof RBlank) {
final RBlank rblank = (RBlank) renderable;
newWidth = rblank.getX();
newOffset = newWidth + rblank.getWidth();
} else {
final BoundableRenderable br = (BoundableRenderable) renderable;
newWidth = newOffset = br.getX() + br.getWidth();
}
break;
}
}
if (cancel) {
// Oops. Need to undo overflow.
if (overflow != null) {
final Iterator i = overflow.iterator();
while (i.hasNext()) {
renderables.add(i.next());
}
}
} else {
this.xoffset = newOffset;
this.width = newWidth;
if (overflow == null) {
throw new OverflowException(Collections.singleton((Renderable) rword));
} else {
overflow.add(rword);
throw new OverflowException(overflow);
}
}
}
// Add it
int extraHeight = 0;
final int maxDescent = this.height - this.baseLineOffset;
if (rword.descent > maxDescent) {
extraHeight += (rword.descent - maxDescent);
}
final int maxAscentPlusLeading = this.baseLineOffset;
if (rword.ascentPlusLeading > maxAscentPlusLeading) {
extraHeight += (rword.ascentPlusLeading - maxAscentPlusLeading);
}
if (extraHeight > 0) {
final int newHeight = this.height + extraHeight;
this.adjustHeight(newHeight, newHeight, VerticalAlign.BOTTOM);
}
this.renderables.add(rword);
rword.setParent(this);
final int x = offset;
offset += wiwidth;
this.width = this.xoffset = offset;
rword.setOrigin(x, this.baseLineOffset - rword.ascentPlusLeading);
}
public final void addBlank(final RBlank rblank) {
// NOTE: Blanks may be added without concern for wrapping (?)
final int x = this.xoffset;
final int width = rblank.width;
rblank.setOrigin(x, this.baseLineOffset - rblank.ascentPlusLeading);
this.renderables.add(rblank);
rblank.setParent(this);
// Only move xoffset, but not width
this.xoffset = x + width;
}
public final void addSpacing(final RSpacing rblank) {
// NOTE: Spacing may be added without concern for wrapping (?)
final int x = this.xoffset;
final int width = rblank.width;
rblank.setOrigin(x, (this.height - rblank.height) / 2);
this.renderables.add(rblank);
rblank.setParent(this);
this.width = this.xoffset = x + width;
}
/**
*
* @param relement
* @param x
* @param elementHeight
* The required new line height.
* @param valign
*/
private final void setElementY(final RElement relement, final int elementHeight, final @Nullable VerticalAlign valign) {
// At this point height should be more than what's needed.
int yoffset;
if (valign != null) {
switch (valign) {
case BOTTOM:
yoffset = this.height - elementHeight;
break;
case MIDDLE:
yoffset = (this.height - elementHeight) / 2;
break;
case BASELINE:
yoffset = this.baseLineOffset - elementHeight;
break;
case TOP:
yoffset = 0;
break;
default:
yoffset = this.baseLineOffset - elementHeight;
}
} else {
yoffset = this.baseLineOffset - elementHeight;
}
// RLine only sets origins, not sizes.
// relement.setBounds(x, yoffset, width, height);
relement.setY(yoffset);
}
// Check if it fits horizontally
final boolean checkFit(final RElement relement) {
final int origXOffset = this.xoffset;
final int desiredMaxWidth = this.desiredMaxWidth;
final int pw = relement.getWidth();
final boolean allowOverflow = this.allowOverflow;
final boolean firstAllowOverflowWord = this.firstAllowOverflowWord;
if (allowOverflow && firstAllowOverflowWord) {
this.firstAllowOverflowWord = false;
}
final boolean overflows = (!allowOverflow || firstAllowOverflowWord) && (origXOffset != 0) && ((origXOffset + pw) > desiredMaxWidth);
return !overflows;
}
private final void addElement(final RElement relement) throws OverflowException {
if (!checkFit(relement)) {
throw new OverflowException(Collections.singleton((Renderable) relement));
}
// Note: Renderable for widget doesn't paint the widget, but
// it's needed for height readjustment.
final int boundsh = this.height;
final int origXOffset = this.xoffset;
final int pw = relement.getWidth();
final int ph = relement.getHeight();
int requiredHeight;
final @Nullable VerticalAlign valign = relement.getVAlign();
if (valign != null) {
switch (valign) {
case BASELINE:
requiredHeight = ph + (boundsh - this.baseLineOffset);
break;
case MIDDLE:
// TODO: This code probably only works with the older ABS-MIDDLE type of alignment.
requiredHeight = Math.max(ph, (ph / 2) + (boundsh - this.baseLineOffset));
break;
default:
requiredHeight = ph;
break;
}
} else {
requiredHeight = ph;
}
if (requiredHeight > boundsh) {
// Height adjustment depends on bounds being already set.
this.adjustHeight(requiredHeight, ph, valign);
}
this.renderables.add(relement);
relement.setParent(this);
relement.setX(origXOffset);
this.setElementY(relement, ph, valign);
final int newX = origXOffset + pw;
this.width = this.xoffset = newX;
}
/**
* Positions line elements vertically.
*/
/*
final void positionVertically() {
final ArrayList renderables = this.renderables;
// System.out.println("pos vertically: " + this + " : " + renderables.size());
// Find word maximum metrics.
int maxDescent = 0;
int maxAscentPlusLeading = 0;
int maxWordHeight = 0;
for (final Iterator i = renderables.iterator(); i.hasNext();) {
final Renderable r = i.next();
if (r instanceof RWord) {
final RWord rword = (RWord) r;
final int descent = rword.descent;
if (descent > maxDescent) {
maxDescent = descent;
}
final int ascentPlusLeading = rword.ascentPlusLeading;
if (ascentPlusLeading > maxAscentPlusLeading) {
maxAscentPlusLeading = ascentPlusLeading;
}
if (rword.height > maxWordHeight) {
maxWordHeight = rword.height;
}
}
}
// Determine proper baseline
final int lineHeight = this.height;
int baseLine = lineHeight - maxDescent;
for (final Iterator i = renderables.iterator(); i.hasNext();) {
final Renderable r = i.next();
if (r instanceof RElement) {
final RElement relement = (RElement) r;
// System.out.println("Placing: " + r + "\n with: " + relement.getVAlign());
@Nullable VerticalAlign vAlign = relement.getVAlign();
if (vAlign != null) {
switch (vAlign) {
case BOTTOM:
// This case was implemented by HRJ, but not tested
relement.setY(lineHeight - relement.getHeight());
break;
case MIDDLE:
int midWord = baseLine + maxDescent - maxWordHeight / 2;
final int halfElementHeight = relement.getHeight() / 2;
if (midWord + halfElementHeight > lineHeight) {
// Change baseLine
midWord = lineHeight - halfElementHeight;
baseLine = midWord + maxWordHeight / 2 - maxDescent;
} else if (midWord - halfElementHeight < 0) {
midWord = halfElementHeight;
baseLine = midWord + maxWordHeight / 2 - maxDescent;
} else {
relement.setY(midWord - halfElementHeight);
}
break;
default:
// TODO
System.out.println("Not implemented yet");
}
} else {
// NOP
}
}
}
}
*/
/**
* Rearrange line elements based on a new line height and alignment provided.
* All line elements are expected to have bounds preset.
*
* @param newHeight
* @param alignmentY
*/
private void adjustHeight(final int newHeight, final int elementHeight, final @Nullable VerticalAlign valign) {
// Set new line height
// int oldHeight = this.height;
this.height = newHeight;
final ArrayList renderables = this.renderables;
// Find max baseline
final FontMetrics firstFm = this.modelNode.getRenderState().getFontMetrics();
int maxDescent = firstFm.getDescent();
int maxAscentPlusLeading = firstFm.getAscent() + firstFm.getLeading();
for (final Renderable renderable : renderables) {
final Object r = renderable;
if (r instanceof RStyleChanger) {
final RStyleChanger rstyleChanger = (RStyleChanger) r;
final FontMetrics fm = rstyleChanger.getModelNode().getRenderState().getFontMetrics();
final int descent = fm.getDescent();
if (descent > maxDescent) {
maxDescent = descent;
}
final int ascentPlusLeading = fm.getAscent() + fm.getLeading();
if (ascentPlusLeading > maxAscentPlusLeading) {
maxAscentPlusLeading = ascentPlusLeading;
}
}
}
final int textHeight = maxDescent + maxAscentPlusLeading;
// TODO: Need to take into account previous RElement's and
// their alignments?
int baseline;
if (valign != null) {
switch (valign) {
case BOTTOM:
baseline = newHeight - maxDescent;
break;
case MIDDLE:
baseline = ((newHeight + textHeight) / 2) - maxDescent;
break;
case BASELINE:
baseline = elementHeight;
break;
case TOP:
baseline = maxAscentPlusLeading;
break;
default:
baseline = elementHeight;
break;
}
} else {
baseline = elementHeight;
}
this.baseLineOffset = baseline;
// Change bounds of renderables accordingly
for (final Renderable renderable : renderables) {
final Object r = renderable;
if (r instanceof RWord) {
final RWord rword = (RWord) r;
rword.setY(baseline - rword.ascentPlusLeading);
} else if (r instanceof RBlank) {
final RBlank rblank = (RBlank) r;
rblank.setY(baseline - rblank.ascentPlusLeading);
} else if (r instanceof RElement) {
final RElement relement = (RElement) r;
// int w = relement.getWidth();
this.setElementY(relement, relement.getHeight(), relement.getVAlign());
} else {
// RSpacing and RStyleChanger don't matter?
}
}
// TODO: Could throw OverflowException when we add floating widgets
}
public boolean onMouseClick(final java.awt.event.MouseEvent event, final int x, final int y) {
final Renderable[] rarray = this.renderables.toArray(Renderable.EMPTY_ARRAY);
final BoundableRenderable r = MarkupUtilities.findRenderable(rarray, x, y, false);
if (r != null) {
final Rectangle rbounds = r.getVisualBounds();
return r.onMouseClick(event, x - rbounds.x, y - rbounds.y);
} else {
return true;
}
}
public boolean onDoubleClick(final java.awt.event.MouseEvent event, final int x, final int y) {
final Renderable[] rarray = this.renderables.toArray(Renderable.EMPTY_ARRAY);
final BoundableRenderable r = MarkupUtilities.findRenderable(rarray, x, y, false);
if (r != null) {
final Rectangle rbounds = r.getVisualBounds();
return r.onDoubleClick(event, x - rbounds.x, y - rbounds.y);
} else {
return true;
}
}
private BoundableRenderable mousePressTarget;
/*
public boolean onMousePressed(final java.awt.event.MouseEvent event, final int x, final int y) {
final Renderable[] rarray = this.renderables.toArray(Renderable.EMPTY_ARRAY);
final BoundableRenderable r = MarkupUtilities.findRenderable(rarray, x, y, false);
if (r != null) {
this.mousePressTarget = r;
final Rectangle rbounds = r.getBounds();
return r.onMousePressed(event, x - rbounds.x, y - rbounds.y);
} else {
return true;
}
}*/
public RenderableSpot getLowestRenderableSpot(final int x, final int y) {
final Renderable[] rarray = this.renderables.toArray(Renderable.EMPTY_ARRAY);
final BoundableRenderable br = MarkupUtilities.findRenderable(rarray, x, y, false);
if (br != null) {
final Rectangle rbounds = br.getVisualBounds();
return br.getLowestRenderableSpot(x - rbounds.x, y - rbounds.y);
} else {
return new RenderableSpot(this, x, y);
}
}
public boolean onMouseReleased(final java.awt.event.MouseEvent event, final int x, final int y) {
final Renderable[] rarray = this.renderables.toArray(Renderable.EMPTY_ARRAY);
final BoundableRenderable r = MarkupUtilities.findRenderable(rarray, x, y, false);
if (r != null) {
final Rectangle rbounds = r.getVisualBounds();
final BoundableRenderable oldArmedRenderable = this.mousePressTarget;
if ((oldArmedRenderable != null) && (r != oldArmedRenderable)) {
oldArmedRenderable.onMouseDisarmed(event);
this.mousePressTarget = null;
}
return r.onMouseReleased(event, x - rbounds.x, y - rbounds.y);
} else {
final BoundableRenderable oldArmedRenderable = this.mousePressTarget;
if (oldArmedRenderable != null) {
oldArmedRenderable.onMouseDisarmed(event);
this.mousePressTarget = null;
}
return true;
}
}
public boolean onMouseDisarmed(final java.awt.event.MouseEvent event) {
final BoundableRenderable target = this.mousePressTarget;
if (target != null) {
this.mousePressTarget = null;
return target.onMouseDisarmed(event);
} else {
return true;
}
}
@Override
public Color getBlockBackgroundColor() {
return this.container.getPaintedBackgroundColor();
}
// public final void adjustHorizontalBounds(int newX, int newMaxWidth) throws
// OverflowException {
// this.x = newX;
// this.desiredMaxWidth = newMaxWidth;
// int topX = newX + newMaxWidth;
// ArrayList renderables = this.renderables;
// int size = renderables.size();
// ArrayList overflown = null;
// Rectangle lastInLine = null;
// for(int i = 0; i < size; i++) {
// Object r = renderables.get(i);
// if(overflown == null) {
// if(r instanceof BoundableRenderable) {
// BoundableRenderable br = (BoundableRenderable) r;
// Rectangle brb = br.getBounds();
// int x2 = brb.x + brb.width;
// if(x2 > topX) {
// overflown = new ArrayList(1);
// }
// else {
// lastInLine = brb;
// }
// }
// }
// /* must not be else here */
// if(overflown != null) {
// //TODO: This could break a word across markup boundary.
// overflown.add(r);
// renderables.remove(i--);
// size--;
// }
// }
// if(overflown != null) {
// if(lastInLine != null) {
// this.width = this.xoffset = lastInLine.x + lastInLine.width;
// }
// throw new OverflowException(overflown);
// }
// }
/*
* (non-Javadoc)
*
* @see org.xamjwg.html.renderer.RCollection#getRenderables()
*/
public Iterator<@NonNull Renderable> getRenderables(final boolean topFirst) {
// TODO: Returning Renderables in order always, assuming that they don't overlap.
// Need to check the assumption
return this.renderables.iterator();
/*
if (topFirst) {
return CollectionUtilities.reverseIterator(this.renderables);
} else {
return this.renderables.iterator();
}*/
}
public boolean isContainedByNode() {
return false;
}
private LineBreak lineBreak;
public LineBreak getLineBreak() {
return lineBreak;
}
public void setLineBreak(final LineBreak lineBreak) {
this.lineBreak = lineBreak;
}
public boolean isEmpty() {
return this.xoffset == 0;
}
@Override
public Rectangle getClipBounds() {
// throw new NotImplementedYetException("This method is not expected to be called for RLine");
return null;
}
@Override
public String toString() {
return "RLine belonging to: " + getParent();
}
}