// Copyright 2014-08-18 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.List; /** Unsynchronized mutable class which is not thread-safe. The internal tracking of cells and widths allows you to make a cell builder for a cell at a given column, add cells in subsequent columns, then complete (buildCell()) the cell and have it find its proper (now previous) column. */ public class TableRowBuilder { private final TablePart tablePart; private TextStyle textStyle; private CellStyle cellStyle; private final List<Cell> cells; private float minRowHeight = 0; private int nextCellIdx = 0; // private TableRow(TablePart tp, float[] a, Cell[] b, CellStyle c, TextStyle d) { // tablePart = tp; cellWidths = a; cells = b; cellStyle = c; textStyle = d; // } private TableRowBuilder(TablePart tp) { tablePart = tp; cells = new ArrayList<Cell>(tp.cellWidths().size()); textStyle = tp.textStyle(); cellStyle = tp.cellStyle(); minRowHeight = tp.minRowHeight(); } public float nextCellSize() { if (tablePart.numCellWidths() <= nextCellIdx) { throw new IllegalStateException("Tried to add more cells than you set sizes for"); } return tablePart.cellWidths().get(nextCellIdx).floatValue(); } public static TableRowBuilder of(TablePart tp) { return new TableRowBuilder(tp); } public TextStyle textStyle() { return textStyle; } public TableRowBuilder textStyle(TextStyle x) { textStyle = x; return this; } public CellStyle cellStyle() { return cellStyle; } public TableRowBuilder addCells(Cell... cs) { Collections.addAll(cells, cs); nextCellIdx += cs.length; return this; } public int nextCellIdx() { return nextCellIdx; } public TableRowBuilder addTextCells(String... ss) { if (textStyle == null) { throw new IllegalStateException("Tried to add a text cell without setting a default text style"); } for (String s : ss) { addCellAt(Cell.of(cellStyle, nextCellSize(), Text.of(textStyle, s)), nextCellIdx); nextCellIdx++; } return this; } // TODO: This should be add Renderable Cells. public TableRowBuilder addJpegCells(ScaledJpeg... js) { for (ScaledJpeg j : js) { addCellAt(Cell.of(cellStyle, nextCellSize(), j), nextCellIdx); nextCellIdx++; } return this; } // Because cells are renderable, this would accept one which could result in duplicate cells // when Cell.buildCell() creates a cell and passes it in here. // public TableRowBuilder addCell(CellStyle.Align align, Renderable... things) { // cells.add(Cell.builder(this).add(things).build()); // return this; // } public TableRowBuilder addCell(Cell c) { cells.add(c); nextCellIdx++; return this; } public TableRowBuilder addCellAt(Cell c, int idx) { // Ensure capacity in the list. while (cells.size() < (idx + 1)) { cells.add(null); } if (cells.get(idx) != null) { // System.out.println("Existing cell was: " + cells.get(idx) + "\n Added cell was: " + c); throw new IllegalStateException("Tried to add a cell built from a table row back to the row after adding a free cell in its spot."); } cells.set(idx, c); return this; } public TableRowBuilder minRowHeight(float f) { minRowHeight = f; return this; } public RowCellBuilder cellBuilder() { RowCellBuilder cb = new RowCellBuilder(this); nextCellIdx++; return cb; } public TablePart buildRow() { // Do we want to fill out the row with blank cells? if (cells.contains(null)) { throw new IllegalStateException("Cannot build row when some TableRowCellBuilders have been created but the cells not built and added back to the row."); } return tablePart.addRow(this); } public XyDim calcDimensions() { XyDim maxDim = XyDim.ZERO; // Similar to PdfLayoutMgr.putRow(). Should be combined? for (Cell cell : cells) { XyDim wh = cell.calcDimensions(cell.width()); maxDim = XyDim.of(wh.x() + maxDim.x(), Math.max(maxDim.y(), wh.y())); } return maxDim; } public XyOffset render(LogicalPage lp, XyOffset outerTopLeft, boolean allPages) { XyDim maxDim = XyDim.ZERO.y(minRowHeight); for (Cell cell : cells) { XyDim wh = cell.calcDimensions(cell.width()); maxDim = XyDim.of(maxDim.x() + cell.width(), Math.max(maxDim.y(), wh.y())); } float maxHeight = maxDim.y(); float x = outerTopLeft.x(); for (Cell cell : cells) { // System.out.println("\t\tAbout to render cell: " + cell); // TODO: Cache the duplicate cell.calcDimensions call!!! cell.render(lp, XyOffset.of(x, outerTopLeft.y()), XyDim.of(cell.width(), maxHeight), allPages); x += cell.width(); } return XyOffset.of(x, outerTopLeft.y() - maxHeight); } @Override public String toString() { return new StringBuilder("TableRowBuilder(").append(tablePart).append(" ") .append(System.identityHashCode(this)).append(")").toString(); } public class RowCellBuilder implements CellBuilder { private final TableRowBuilder tableRowBuilder; private float width; // Both require this. private CellStyle cellStyle; // Both require this. private final List<Renderable> rows = new ArrayList<Renderable>(); private TextStyle textStyle; private final int colIdx; private RowCellBuilder(TableRowBuilder trb) { tableRowBuilder = trb; width = trb.nextCellSize(); cellStyle = trb.cellStyle(); textStyle = trb.textStyle(); colIdx = trb.nextCellIdx(); } // I think setting the width after creation is a pretty bad idea for this class since so much // is put into getting the width and column correct. // public TableRowCellBuilder width(float w) { width = w; return this; } public RowCellBuilder cellStyle(CellStyle cs) { cellStyle = cs; return this;} public RowCellBuilder borderStyle(BorderStyle bs) { cellStyle = cellStyle.borderStyle(bs); return this;} public RowCellBuilder align(CellStyle.Align align) { cellStyle = cellStyle.align(align); return this; } public RowCellBuilder textStyle(TextStyle x) { textStyle = x; return this; } // Do we want to (and how could we?) prevent adding a cell to itself? public RowCellBuilder add(Renderable... rs) { Collections.addAll(rows, rs); return this; } public RowCellBuilder add(List<Renderable> js) { if (js != null) { rows.addAll(js); } return this; } public RowCellBuilder 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 RowCellBuilder add(TextStyle ts, List<String> ls) { if (ls != null) { for (String s : ls) { rows.add(Text.of(ts, s)); } } return this; } public TableRowBuilder buildCell() { Cell c = Cell.of(cellStyle, width, rows); return tableRowBuilder.addCellAt(c, colIdx); } @Override public String toString() { return new StringBuilder("RowCellBuilder(").append(tableRowBuilder).append(" colIdx=") .append(colIdx).append(")").toString(); } @Override public int hashCode() { return tableRowBuilder.hashCode() + colIdx; } @Override public boolean equals(Object other) { // Cheapest operation first... if (this == other) { return true; } if ((other == null) || !(other instanceof RowCellBuilder) || (this.hashCode() != other.hashCode())) { return false; } // Details... final RowCellBuilder that = (RowCellBuilder) other; return (this.colIdx == that.colIdx) && tableRowBuilder.equals(that.tableRowBuilder); } } }