// Copyright 2013-03-03 PlanBase Inc. & Glen Peterson
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.planbase.pdf.layoutmanager;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
A styled table cell or layout block with a pre-set horizontal width. Vertical height is calculated
based on how the content is rendered with regard to line-breaks and page-breaks.
*/
public class Cell implements Renderable {
// These are limits of the cell, not the contents.
private final CellStyle cellStyle;
private final float width;
// A list of the contents. It's pretty limiting to have one item per row.
private final List<Renderable> rows;
private final Map<Float,PreCalcRows> preCalcRows = new HashMap<Float,PreCalcRows>(0);
private static class PreCalcRow {
Renderable row;
XyDim blockDim;
public static PreCalcRow of(Renderable r, XyDim d) {
PreCalcRow pcr = new PreCalcRow(); pcr.row = r; pcr.blockDim = d; return pcr;
}
}
private static class PreCalcRows {
List<PreCalcRow> rows = new ArrayList<PreCalcRow>(1);
XyDim blockDim;
}
private Cell(CellStyle cs, float w, List<Renderable> rs) {
if (w < 0) {
throw new IllegalArgumentException("A cell cannot have a negative width");
}
// for (Renderable r : rs) {
// if (r == null) {
// throw new IllegalArgumentException("How am I supposed to render a null?");
// }
// }
cellStyle = cs; width = w; rows = rs;
}
/**
Creates a new cell.
@param w the width (height will be calculated based on how objects can be rendered within this
width).
@param cs the cell style
@return a cell suitable for rendering.
*/
public static Cell of(CellStyle cs, float w) { //, final Object... r) {
return new Cell(cs, w, Collections.<Renderable>emptyList());
// (r == null) ? Collections.emptyList()
// : Arrays.asList(r));
}
// Simple case of a single styled String
public static Cell of(CellStyle cs, float w, TextStyle ts, String s) {
List<Renderable> ls = new ArrayList<Renderable>(1);
ls.add(Text.of(ts, s));
return new Cell(cs, w, ls);
}
// Simple case of a single styled String
public static Cell of(CellStyle cs, float w, Text t) {
List<Renderable> ls = new ArrayList<Renderable>(1);
ls.add(t);
return new Cell(cs, w, ls);
}
public static Cell of(CellStyle cs, float w, ScaledJpeg j) {
List<Renderable> ls = new ArrayList<Renderable>(1);
ls.add(j);
return new Cell(cs, w, ls);
}
public static Cell of(CellStyle cs, float w, Renderable r) {
List<Renderable> ls = new ArrayList<Renderable>(1);
ls.add(r);
return new Cell(cs, w, ls);
}
public static Cell of(CellStyle cs, float w, List<Renderable> ls) {
return new Cell(cs, w, ls);
}
// Simple case of a single styled String
public static Cell of(CellStyle cs, float w, Cell c) {
List<Renderable> ls = new ArrayList<Renderable>(1);
ls.add(c);
return new Cell(cs, w, ls);
}
public CellStyle cellStyle() { return cellStyle; }
// public BorderStyle border() { return borderStyle; }
public float width() { return width; }
private void calcDimensionsForReal(final float maxWidth) {
PreCalcRows pcrs = new PreCalcRows();
XyDim blockDim = XyDim.ZERO;
Padding padding = cellStyle.padding();
float innerWidth = maxWidth;
if (padding != null) {
innerWidth -= (padding.left() + padding.right());
}
for (Renderable row : rows) {
XyDim rowDim = (row == null) ? XyDim.ZERO : row.calcDimensions(innerWidth);
blockDim = XyDim.of(Math.max(blockDim.x(), rowDim.x()),
blockDim.y() + rowDim.y());
// System.out.println("\trow = " + row);
// System.out.println("\trowDim = " + rowDim);
// System.out.println("\tactualDim = " + actualDim);
pcrs.rows.add(PreCalcRow.of(row, rowDim));
}
pcrs.blockDim = blockDim;
preCalcRows.put(maxWidth, pcrs);
}
private PreCalcRows ensurePreCalcRows(final float maxWidth) {
PreCalcRows pcr = preCalcRows.get(maxWidth);
if (pcr == null) {
calcDimensionsForReal(maxWidth);
pcr = preCalcRows.get(maxWidth);
}
return pcr;
}
public XyDim calcDimensions(final float maxWidth) {
// I think zero or negative width cells might be OK to ignore. I'd like to try to make
// Text.calcDimensionsForReal() handle this situation before throwing an error here.
// if (maxWidth < 0) {
// throw new IllegalArgumentException("maxWidth must be positive, not " + maxWidth);
// }
XyDim blockDim = ensurePreCalcRows(maxWidth).blockDim;
XyDim ret = ((cellStyle.padding() == null) ? blockDim : cellStyle.padding().addTo(blockDim));
// System.out.println("Cell.calcDimensions(" + maxWidth + ") blockDim=" + blockDim +
// " returns " + ret);
return ret;
}
/*
Renders item and all child-items with given width and returns the x-y pair of the
lower-right-hand corner of the last line (e.g. of text).
*/
public XyOffset render(LogicalPage lp, XyOffset outerTopLeft, final XyDim outerDimensions,
boolean allPages) {
// System.out.println("Cell.render(" + this.toString());
// new Exception().printStackTrace();
float maxWidth = outerDimensions.x();
PreCalcRows pcrs = ensurePreCalcRows(maxWidth);
final Padding padding = cellStyle.padding();
// XyDim outerDimensions = padding.addTo(pcrs.blockDim);
// Draw background first (if necessary) so that everything else ends up on top of it.
if (cellStyle.bgColor() != null) {
// System.out.println("\tCell.render calling putRect...");
lp.putRect(outerTopLeft, outerDimensions, cellStyle.bgColor());
// System.out.println("\tCell.render back from putRect");
}
// Draw contents over background, but under border
XyOffset innerTopLeft;
final XyDim innerDimensions;
if (padding == null) {
innerTopLeft = outerTopLeft;
innerDimensions = outerDimensions;
} else {
// System.out.println("\tCell.render outerTopLeft before padding=" + outerTopLeft);
innerTopLeft = padding.applyTopLeft(outerTopLeft);
// System.out.println("\tCell.render innerTopLeft after padding=" + innerTopLeft);
innerDimensions = padding.subtractFrom(outerDimensions);
}
XyDim wrappedBlockDim = pcrs.blockDim;
// System.out.println("\tCell.render cellStyle.align()=" + cellStyle.align());
// System.out.println("\tCell.render outerDimensions=" + outerDimensions);
// System.out.println("\tCell.render padding=" + padding);
// System.out.println("\tCell.render innerDimensions=" + innerDimensions);
// System.out.println("\tCell.render wrappedBlockDim=" + wrappedBlockDim);
Padding alignPad = cellStyle.align().calcPadding(innerDimensions, wrappedBlockDim);
// System.out.println("\tCell.render alignPad=" + alignPad);
if (alignPad != null) {
innerTopLeft = XyOffset.of(innerTopLeft.x() + alignPad.left(),
innerTopLeft.y() - alignPad.top());
}
XyOffset outerLowerRight = innerTopLeft;
for (int i = 0; i < rows.size(); i++) {
Renderable row = rows.get(i);
if (row == null) {
continue;
}
PreCalcRow pcr = pcrs.rows.get(i);
float rowXOffset = cellStyle.align().leftOffset(wrappedBlockDim.x(), pcr.blockDim.x());
outerLowerRight = row.render(lp,
innerTopLeft.x(innerTopLeft.x() + rowXOffset),
pcr.blockDim, allPages);
innerTopLeft = outerLowerRight.x(innerTopLeft.x());
}
// Draw border last to cover anything that touches it?
BorderStyle border = cellStyle.borderStyle();
if (border != null) {
float origX = outerTopLeft.x();
float origY = outerTopLeft.y();
float rightX = outerTopLeft.x() + outerDimensions.x();
float bottomY = outerTopLeft.y() - outerDimensions.y();
// Like CSS it's listed Top, Right, Bottom, left
if (border.top() != null) {
lp.putLine(origX, origY, rightX, origY, border.top());
}
if (border.right() != null) {
lp.putLine(rightX, origY, rightX, bottomY, border.right());
}
if (border.bottom() != null) {
lp.putLine(origX, bottomY, rightX, bottomY, border.bottom());
}
if (border.left() != null) {
lp.putLine(origX, origY, origX, bottomY, border.left());
}
}
return outerLowerRight;
}
public static Builder builder(CellStyle cellStyle, float width) {
return new Builder(cellStyle, width);
}
// Replaced with TableRow.CellBuilder.of()
// /**
// Be careful when adding multiple cell builders at once because the cell size is based upon
// a pointer into the list of cell sizes. That pointer gets incremented each time a cell is
// added, not each time nextCellSize() is called. Is this a bug? Or would fixing it create
// too many other bugs?
// @param trb
// @return
// */
// public static Builder builder(TableRowBuilder trb) {
// Builder b = new Builder(trb.cellStyle(), trb.nextCellSize()).textStyle(trb.textStyle());
// b.trb = trb;
// return b;
// }
/**
* A mutable Builder for somewhat less mutable cells.
*/
public static class Builder implements CellBuilder {
private final float width;
private CellStyle cellStyle;
private final List<Renderable> rows = new ArrayList<Renderable>();
private TextStyle textStyle;
private Builder(CellStyle cs, float w) { width = w; cellStyle = cs; }
// Is this necessary?
// public Builder width(float w) { width = w; return this; }
public Builder cellStyle(CellStyle cs) { cellStyle = cs; return this;}
public Builder align(CellStyle.Align align) { cellStyle = cellStyle.align(align); return this;}
public Builder textStyle(TextStyle x) { textStyle = x; return this; }
// This is a builder which is not Renderable. No way to add something to itself *here*.
public Builder add(Renderable... rs) { Collections.addAll(rows, rs); return this; }
public Builder add(List<Renderable> js) {
if (js != null) { rows.addAll(js); } return this;
}
public Builder add(TextStyle ts, List<String> ls) {
if (ls != null) {
for (String s : ls) {
rows.add(Text.of(ts, s));
}
}
return this;
}
public Builder add(String... ss) {
if (textStyle == null) {
throw new IllegalStateException("Must set a default text style before adding raw strings");
}
for (String s : ss) {
rows.add(Text.of(textStyle, s));
}
return this;
}
// public Builder add(Cell c) { rows.add(c); return this; }
public Cell build() { return new Cell(cellStyle, width, rows); }
// Replaced with TableRow.CellBuilder.buildCell()
// public TableRowBuilder buildCell() {
// Cell c = new Cell(cellStyle, width, rows);
// return trb.addCell(c);
// }
@Override public String toString() {
StringBuilder sB = new StringBuilder("Cell.Builder(").append(cellStyle).append(" width=")
.append(width).append(" rows=[");
for (int i = 0; (i < rows.size()) && (i < 3); i++) {
if (i > 0) { sB.append(" "); }
sB.append(rows.get(i));
}
return sB.append("])").toString();
}
}
@Override public String toString() {
StringBuilder sB = new StringBuilder("Cell(").append(cellStyle).append(" width=")
.append(width).append(" rows=[");
for (int i = 0; (i < rows.size()) && (i < 3); i++) {
if (i > 0) { sB.append(" "); }
sB.append(rows.get(i));
}
return sB.append("])").toString();
}
}