/*
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: lobochief@users.sourceforge.net
*/
/*
* Created on Apr 16, 2005
*/
package org.lobobrowser.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.lobobrowser.html.domimpl.ModelNode;
import org.lobobrowser.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<Renderable> 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<Renderable> 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<Renderable> renderables = this.renderables;
ArrayList<Renderable> 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<Renderable> 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<Renderable> 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<Renderable> 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<Renderable> 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<Renderable> 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();
}
}