/* 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.Color; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.Rectangle; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import org.lobobrowser.html.dombl.ModelNode; import org.lobobrowser.html.renderstate.RenderState; /** * The Class RLine. * * @author J. H. S. */ public class RLine extends BaseRCollection { /** The renderables. */ private final ArrayList<Renderable> renderables = new ArrayList<Renderable>(8); // private final RenderState startRenderState; /** The base line offset. */ private int baseLineOffset; /** The desired max width. */ private int desiredMaxWidth; /** * Offset where next renderable should be placed. This can be different to * width. */ private int xoffset; /** The allow overflow. */ private boolean allowOverflow = false; /** The first allow overflow word. */ private boolean firstAllowOverflowWord = false; /** * Instantiates a new r line. * * @param modelNode * the model node * @param container * the container * @param x * the x * @param y * the y * @param desiredMaxWidth * the desired max width * @param height * the height * @param initialAllowOverflow * the initial allow overflow */ public RLine(ModelNode modelNode, RenderableContainer container, int x, int y, int desiredMaxWidth, int height, 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; } /** * Sets the allow overflow. * * @param flag * the new allow overflow */ public void setAllowOverflow(boolean flag) { if (flag != this.allowOverflow) { this.allowOverflow = flag; if (flag) { // Set to true only if allowOverflow was // previously false. this.firstAllowOverflowWord = true; } } } /** * Checks if is allow overflow. * * @return the allow overflow */ public boolean isAllowOverflow() { return this.allowOverflow; } /** * This method should only be invoked when the line has no items yet. * * @param x * the x * @param desiredMaxWidth * the desired max width */ public void changeLimits(int x, int desiredMaxWidth) { this.x = x; this.desiredMaxWidth = desiredMaxWidth; } /** * Gets the baseline offset. * * @return the baseline offset */ public int getBaselineOffset() { return this.baseLineOffset; } /* * (non-Javadoc) * * @see org.lobobrowser.html.renderer.BaseBoundableRenderable# * invalidateLayoutLocal() */ @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) */ @Override public void paint(Graphics g) { // Paint according to render state of the start of line first. RenderState rs = this.modelNode.getRenderState(); if (rs != null) { Color textColor = rs.getColor(); g.setColor(textColor); 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. Iterator<Renderable> i = this.renderables.iterator(); if (i != null) { while (i.hasNext()) { Object r = i.next(); if (r instanceof RElement) { // RElement's should be clipped. RElement relement = (RElement) r; Graphics newG = g.create(relement.getX(), relement.getY(), relement.getWidth(), relement.getHeight()); try { relement.paint(newG); } finally { newG.dispose(); } } else if (r instanceof BoundableRenderable) { BoundableRenderable br = (BoundableRenderable) r; br.paintTranslated(g); } else { ((Renderable) r).paint(g); } } } } /* * (non-Javadoc) * * @see * org.lobobrowser.html.renderer.BaseRCollection#extractSelectionText(java. * lang .StringBuffer, boolean, * org.lobobrowser.html.renderer.RenderableSpot, * org.lobobrowser.html.renderer.RenderableSpot) */ @Override public boolean extractSelectionText(StringBuffer buffer, boolean inSelection, RenderableSpot startPoint, RenderableSpot endPoint) { boolean result = super.extractSelectionText(buffer, inSelection, startPoint, endPoint); if (result) { LineBreak br = this.lineBreak; if (br != null) { buffer.append(System.getProperty("line.separator")); } else { ArrayList<Renderable> renderables = this.renderables; int size = renderables.size(); if ((size > 0) && !(renderables.get(size - 1) instanceof RBlank)) { buffer.append(" "); } } } return result; } /** * Adds the style changer. * * @param sc * the sc */ public final void addStyleChanger(RStyleChanger sc) { this.renderables.add(sc); } /** * Simply add. * * @param r * the r */ public final void simplyAdd(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. * * @param renderable * the renderable * @throws OverflowException * Thrown if the renderable overflows the line. All overflowing * renderables are added to the exception. */ public final void add(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); } } /** * Adds the word. * * @param rword * the rword * @throws OverflowException * the overflow exception */ public final void addWord(RWord rword) throws OverflowException { // Check if it fits horzizontally int offset = this.xoffset; int wiwidth = rword.width; boolean allowOverflow = this.allowOverflow; boolean firstAllowOverflowWord = this.firstAllowOverflowWord; if (allowOverflow && firstAllowOverflowWord) { this.firstAllowOverflowWord = false; } if ((!allowOverflow || firstAllowOverflowWord) && (offset != 0) && ((offset + wiwidth) > this.desiredMaxWidth)) { 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;) { Renderable renderable = renderables.get(i); if ((renderable instanceof RWord) || !(renderable instanceof BoundableRenderable)) { if (overflow == null) { overflow = new ArrayList<Renderable>(); } 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) { RBlank rblank = (RBlank) renderable; newWidth = rblank.getX(); newOffset = newWidth + rblank.getWidth(); } else { BoundableRenderable br = (BoundableRenderable) renderable; newWidth = newOffset = br.getX() + br.getWidth(); } break; } } if (cancel) { // Oops. Need to undo overflow. if (overflow != null) { 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(rword)); } else { overflow.add(rword); throw new OverflowException(overflow); } } } // Add it int extraHeight = 0; int maxDescent = this.height - this.baseLineOffset; if (rword.getDescent() > maxDescent) { extraHeight += (rword.getDescent() - maxDescent); } int maxAscentPlusLeading = this.baseLineOffset; if (rword.getAscentPlusLeading() > maxAscentPlusLeading) { extraHeight += (rword.getAscentPlusLeading() - maxAscentPlusLeading); } if (extraHeight > 0) { int newHeight = this.height + extraHeight; this.adjustHeight(newHeight, newHeight, RElement.VALIGN_ABSBOTTOM); } this.renderables.add(rword); rword.setParent(this); int x = offset; offset += wiwidth; this.width = this.xoffset = offset; rword.setOrigin(x, this.baseLineOffset - rword.getAscentPlusLeading()); } /** * Adds the blank. * * @param rblank * the rblank */ public final void addBlank(RBlank rblank) { // NOTE: Blanks may be added without concern for wrapping (?) int x = this.xoffset; int width = rblank.width; rblank.setOrigin(x, this.baseLineOffset - rblank.getAscentPlusLeading()); this.renderables.add(rblank); rblank.setParent(this); // Only move xoffset, but not width this.xoffset = x + width; } /** * Adds the spacing. * * @param rblank * the rblank */ public final void addSpacing(RSpacing rblank) { // NOTE: Spacing may be added without concern for wrapping (?) int x = this.xoffset; 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; } /** * Sets the element y. * * @param relement * the relement * @param elementHeight * The required new line height. * @param valign * the valign */ private final void setElementY(RElement relement, int elementHeight, int valign) { // At this point height should be more than what's needed. int yoffset; switch (valign) { case RElement.VALIGN_ABSBOTTOM: yoffset = this.height - elementHeight; break; case RElement.VALIGN_ABSMIDDLE: yoffset = (this.height - elementHeight) / 2; break; case RElement.VALIGN_BASELINE: case RElement.VALIGN_BOTTOM: yoffset = this.baseLineOffset - elementHeight; break; case RElement.VALIGN_MIDDLE: yoffset = this.baseLineOffset - (elementHeight / 2); break; case RElement.VALIGN_TOP: yoffset = 0; break; default: yoffset = this.baseLineOffset - elementHeight; } // RLine only sets origins, not sizes. // relement.setBounds(x, yoffset, width, height); relement.setY(yoffset); } /** * Adds the element. * * @param relement * the relement * @throws OverflowException * the overflow exception */ private final void addElement(RElement relement) throws OverflowException { // Check if it fits horizontally int origXOffset = this.xoffset; int desiredMaxWidth = this.desiredMaxWidth; int pw = relement.getWidth(); boolean allowOverflow = this.allowOverflow; boolean firstAllowOverflowWord = this.firstAllowOverflowWord; if (allowOverflow && firstAllowOverflowWord) { this.firstAllowOverflowWord = false; } if ((!allowOverflow || firstAllowOverflowWord) && (origXOffset != 0) && ((origXOffset + pw) > desiredMaxWidth)) { throw new OverflowException(Collections.singleton(relement)); } // Note: Renderable for widget doesn't paint the widget, but // it's needed for height readjustment. int boundsh = this.height; int ph = relement.getHeight(); int requiredHeight; int valign = relement.getVAlign(); switch (valign) { case RElement.VALIGN_BASELINE: case RElement.VALIGN_BOTTOM: requiredHeight = ph + (boundsh - this.baseLineOffset); break; case RElement.VALIGN_MIDDLE: requiredHeight = Math.max(ph, (ph / 2) + (boundsh - this.baseLineOffset)); break; default: requiredHeight = ph; break; } 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); int newX = origXOffset + pw; this.width = this.xoffset = newX; } /** * Rearrange line elements based on a new line height and alignment * provided. All line elements are expected to have bounds preset. * * @param newHeight * the new height * @param elementHeight * the element height * @param valign * the valign */ private void adjustHeight(int newHeight, int elementHeight, int valign) { // Set new line height // int oldHeight = this.height; this.height = newHeight; ArrayList<Renderable> renderables = this.renderables; // Find max baseline FontMetrics firstFm = this.modelNode.getRenderState().getFontMetrics(); int maxDescent = firstFm.getDescent(); int maxAscentPlusLeading = firstFm.getAscent() + firstFm.getLeading(); for (Iterator<Renderable> i = renderables.iterator(); i.hasNext();) { Object r = i.next(); if (r instanceof RStyleChanger) { RStyleChanger rstyleChanger = (RStyleChanger) r; FontMetrics fm = rstyleChanger.getModelNode().getRenderState().getFontMetrics(); int descent = fm.getDescent(); if (descent > maxDescent) { maxDescent = descent; } int ascentPlusLeading = fm.getAscent() + fm.getLeading(); if (ascentPlusLeading > maxAscentPlusLeading) { maxAscentPlusLeading = ascentPlusLeading; } } } int textHeight = maxDescent + maxAscentPlusLeading; // TODO: Need to take into account previous RElement's and // their alignments? int baseline; switch (valign) { case RElement.VALIGN_ABSBOTTOM: baseline = newHeight - maxDescent; break; case RElement.VALIGN_ABSMIDDLE: baseline = ((newHeight + textHeight) / 2) - maxDescent; break; case RElement.VALIGN_BASELINE: case RElement.VALIGN_BOTTOM: baseline = elementHeight; break; case RElement.VALIGN_MIDDLE: baseline = newHeight / 2; break; case RElement.VALIGN_TOP: baseline = maxAscentPlusLeading; break; default: baseline = elementHeight; break; } this.baseLineOffset = baseline; // Change bounds of renderables accordingly for (Iterator<Renderable> i = renderables.iterator(); i.hasNext();) { Object r = i.next(); if (r instanceof RWord) { RWord rword = (RWord) r; rword.setY(baseline - rword.getAscentPlusLeading()); } else if (r instanceof RBlank) { RBlank rblank = (RBlank) r; rblank.setY(baseline - rblank.getAscentPlusLeading()); } else if (r instanceof RElement) { 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 } /* * (non-Javadoc) * * @see * org.lobobrowser.html.renderer.BoundableRenderable#onMouseClick(java.awt. * event .MouseEvent, int, int) */ @Override public boolean onMouseClick(MouseEvent event, int x, int y) { Renderable[] rarray = this.renderables.toArray(Renderable.EMPTY_ARRAY); BoundableRenderable r = MarkupUtilities.findRenderable(rarray, x, y, false); if (r != null) { Rectangle rbounds = r.getBounds(); return r.onMouseClick(event, x - rbounds.x, y - rbounds.y); } else { 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) { Renderable[] rarray = this.renderables.toArray(Renderable.EMPTY_ARRAY); BoundableRenderable r = MarkupUtilities.findRenderable(rarray, x, y, false); if (r != null) { Rectangle rbounds = r.getBounds(); return r.onDoubleClick(event, x - rbounds.x, y - rbounds.y); } else { return true; } } /** The mouse press target. */ private BoundableRenderable mousePressTarget; /* * (non-Javadoc) * * @see * org.lobobrowser.html.renderer.BoundableRenderable#onMousePressed(java.awt * .event.MouseEvent, int, int) */ @Override public boolean onMousePressed(MouseEvent event, int x, int y) { Renderable[] rarray = this.renderables.toArray(Renderable.EMPTY_ARRAY); BoundableRenderable r = MarkupUtilities.findRenderable(rarray, x, y, false); if (r != null) { this.mousePressTarget = r; Rectangle rbounds = r.getBounds(); return r.onMousePressed(event, x - rbounds.x, y - rbounds.y); } else { 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.BoundableRenderable#getLowestRenderableSpot * (int, int) */ @Override public RenderableSpot getLowestRenderableSpot(int x, int y) { Renderable[] rarray = this.renderables.toArray(Renderable.EMPTY_ARRAY); BoundableRenderable br = MarkupUtilities.findRenderable(rarray, x, y, false); if (br != null) { Rectangle rbounds = br.getBounds(); return br.getLowestRenderableSpot(x - rbounds.x, y - rbounds.y); } else { return new RenderableSpot(this, x, y); } } /* * (non-Javadoc) * * @see * org.lobobrowser.html.renderer.BoundableRenderable#onMouseReleased(java. * awt .event.MouseEvent, int, int) */ @Override public boolean onMouseReleased(MouseEvent event, int x, int y) { Renderable[] rarray = this.renderables.toArray(Renderable.EMPTY_ARRAY); BoundableRenderable r = MarkupUtilities.findRenderable(rarray, x, y, false); if (r != null) { Rectangle rbounds = r.getBounds(); 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 { BoundableRenderable oldArmedRenderable = this.mousePressTarget; if (oldArmedRenderable != null) { oldArmedRenderable.onMouseDisarmed(event); this.mousePressTarget = null; } return true; } } /* * (non-Javadoc) * * @see * org.lobobrowser.html.renderer.BoundableRenderable#onMouseDisarmed(java. * awt .event.MouseEvent) */ @Override public boolean onMouseDisarmed(MouseEvent event) { BoundableRenderable target = this.mousePressTarget; if (target != null) { this.mousePressTarget = null; return target.onMouseDisarmed(event); } else { return true; } } /* * (non-Javadoc) * * @see org.lobobrowser.html.renderer.BaseBoundableRenderable# * getBlockBackgroundColor () */ @Override public Color getBlockBackgroundColor() { return this.container.getPaintedBackgroundColor(); } /* * (non-Javadoc) * * @see org.lobobrowser.html.render.RCollection#getRenderables() */ @Override public Iterator<Renderable> getRenderables() { return this.renderables.iterator(); } /* * (non-Javadoc) * * @see * org.lobobrowser.html.renderer.BoundableRenderable#isContainedByNode() */ @Override public boolean isContainedByNode() { return false; } /** The line break. */ private LineBreak lineBreak; /** * Gets the line break. * * @return the line break */ public LineBreak getLineBreak() { return lineBreak; } /** * Sets the line break. * * @param lineBreak * the new line break */ public void setLineBreak(LineBreak lineBreak) { this.lineBreak = lineBreak; } /** * Checks if is empty. * * @return true, if is empty */ public boolean isEmpty() { return this.xoffset == 0; } }