/*
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
*/
package org.lobobrowser.html.renderer;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import org.lobobrowser.html.HtmlRendererContext;
import org.lobobrowser.html.domimpl.AnonymousNodeImpl;
import org.lobobrowser.html.domimpl.HTMLElementImpl;
import org.lobobrowser.html.domimpl.ModelNode;
import org.lobobrowser.html.domimpl.NodeImpl;
import org.lobobrowser.html.domimpl.TextImpl;
import org.lobobrowser.html.style.BorderInfo;
import org.lobobrowser.html.style.HtmlInsets;
import org.lobobrowser.html.style.HtmlLength;
import org.lobobrowser.html.style.HtmlValues;
import org.lobobrowser.html.style.JStyleProperties;
import org.lobobrowser.html.style.RenderState;
import org.lobobrowser.html.style.RenderThreadState;
import org.lobobrowser.ua.UserAgentContext;
final class TableMatrix {
private final ArrayList<Row> ROWS = new ArrayList<>();
private final ArrayList<RowGroup> ROW_GROUPS = new ArrayList<>();
private final ArrayList<@NonNull RAbstractCell> ALL_CELLS = new ArrayList<>();
private final HTMLElementImpl tableElement;
private final UserAgentContext uaContext;
private final HtmlRendererContext rendererContext;
private final FrameContext frameContext;
private final RElement relement;
private final RenderableContainer container;
private ColSizeInfo[] columnSizes;
private RowSizeInfo[] rowSizes;
private int tableWidth;
private int tableHeight;
/*
* This is so that we can draw the lines inside the table that appear when a
* border attribute is used.
*/
private int hasOldStyleBorder;
/**
* @param element
*/
public TableMatrix(final HTMLElementImpl element, final UserAgentContext uaContext, final HtmlRendererContext rcontext,
final FrameContext frameContext,
final RenderableContainer tableAsContainer, final RElement relement) {
this.tableElement = element;
this.uaContext = uaContext;
this.rendererContext = rcontext;
this.frameContext = frameContext;
this.relement = relement;
this.container = tableAsContainer;
}
@Override
public void finalize() throws Throwable {
super.finalize();
}
public int getNumRows() {
return this.ROWS.size();
}
public int getNumColumns() {
return this.columnSizes.length;
}
/**
* @return Returns the tableHeight.
*/
public int getTableHeight() {
return this.tableHeight;
}
/**
* @return Returns the tableWidth.
*/
public int getTableWidth() {
return this.tableWidth;
}
// private int border;
private int cellSpacingY;
private int cellSpacingX;
private int widthsOfExtras;
private int heightsOfExtras;
private HtmlLength tableWidthLength;
private ArrayList<RowGroupSizeInfo> rowGroupSizes;
/**
* Called on every relayout. Element children might have changed.
*/
public void reset(final Insets insets, final int availWidth, final int availHeight) {
// TODO: Incorporate into build() and calculate
// sizes properly based on parameters.
ROW_GROUPS.clear();
ROWS.clear();
ALL_CELLS.clear();
rowGroupSizes = null;
// TODO: Does it need this old-style border?
final int border = getBorderAttribute();
final int cellSpacing = getCellSpacingAttribute();
this.cellSpacingX = cellSpacing;
this.cellSpacingY = cellSpacing;
this.tableWidthLength = TableMatrix.getWidthLength(this.tableElement, availWidth);
this.populateRows();
this.adjustForCellSpans();
this.createSizeArrays();
// Calculate widths of extras
final ColSizeInfo[] columnSizes = this.columnSizes;
final int numCols = columnSizes.length;
int widthsOfExtras = insets.left + insets.right + ((numCols + 1) * cellSpacing);
if (border > 0) {
widthsOfExtras += (numCols * 2);
}
this.widthsOfExtras = widthsOfExtras;
// Calculate heights of extras
final RowSizeInfo[] rowSizes = this.rowSizes;
final int numRows = rowSizes.length;
int heightsOfExtras = insets.top + insets.bottom + ((numRows + 1) * cellSpacing);
if (border > 0) {
heightsOfExtras += (numRows * 2);
}
this.heightsOfExtras = heightsOfExtras;
this.hasOldStyleBorder = border > 0 ? 1 : 0;
}
private int getCellSpacingAttribute() {
int cellSpacing = 0;
final String cellSpacingText = this.tableElement.getAttribute("cellspacing");
if (cellSpacingText != null) {
try {
// TODO: cellSpacing can be a percentage as well
cellSpacing = Integer.parseInt(cellSpacingText);
if (cellSpacing < 0) {
cellSpacing = 0;
}
} catch (final NumberFormatException nfe) {
System.out.println("Exception while parsing cellSpacing: " + nfe);
// ignore
}
}
return cellSpacing;
}
private int getBorderAttribute() {
int border = 0;
final String borderText = this.tableElement.getAttribute("border");
if (borderText != null) {
if (borderText.length() == 0) {
border = 1;
} else {
try {
border = Integer.parseInt(borderText);
if (border < 0) {
border = 0;
}
} catch (final NumberFormatException nfe) {
System.out.println("Exception while parsing border: " + nfe);
// ignore
}
}
}
return border;
}
public void build(final int availWidth, final int availHeight, final boolean sizeOnly) {
final int hasBorder = this.hasOldStyleBorder;
this.determineColumnSizes(hasBorder, this.cellSpacingX, this.cellSpacingY, availWidth);
this.determineRowSizes(hasBorder, this.cellSpacingY, availHeight, sizeOnly);
}
private static HtmlLength getWidthLength(final HTMLElementImpl element, final int availWidth) {
try {
final JStyleProperties props = element.getCurrentStyle();
final String widthText = props.getWidth();
if (widthText == null) {
// TODO: convert attributes to CSS properties
final String widthAttr = element.getAttribute("width");
if (widthAttr == null) {
return null;
}
return new HtmlLength(HtmlValues.getPixelSize(widthAttr, element.getRenderState(), 0, availWidth));
} else {
return new HtmlLength(HtmlValues.getPixelSize(widthText, element.getRenderState(), 0, availWidth));
}
} catch (final NumberFormatException err) {
System.out.println("Exception while parsing width: " + err);
return null;
}
}
private static HtmlLength getHeightLength(final HTMLElementImpl element, final int availHeight) {
try {
final JStyleProperties props = element.getCurrentStyle();
final String heightText = props.getHeight();
if (heightText == null) {
final String ha = element.getAttribute("height");
if (ha == null) {
return null;
} else {
return new HtmlLength(HtmlValues.getPixelSize(ha, element.getRenderState(), 0, availHeight));
}
} else {
return new HtmlLength(HtmlValues.getPixelSize(heightText, element.getRenderState(), 0, availHeight));
}
} catch (final NumberFormatException err) {
System.out.println("Exception while parsing height: " + err);
return null;
}
}
static Insets getCSSInsets(final RenderState rs) {
final BorderInfo borderInfo = rs.getBorderInfo();
final HtmlInsets elemBorderHtmlInsets = borderInfo == null ? null : borderInfo.insets;
return elemBorderHtmlInsets == null ? RBlockViewport.ZERO_INSETS : elemBorderHtmlInsets.getAWTInsets(0, 0, 0, 0, 0, 0, 0, 0);
}
private static final class RowGroup {
final ArrayList<Row> rows = new ArrayList<>();
private final HTMLElementImpl rowGroupElem;
final BorderOverrider borderOverrider = new BorderOverrider();
public RowGroup(final HTMLElementImpl rowGroupElem) {
this.rowGroupElem = rowGroupElem;
}
void add(final Row row) {
rows.add(row);
row.rowGroup = this;
}
public void finish() {
final int numRows = rows.size();
int minCellBorderLeft = -1;
int minCellBorderRight = -1;
for (int i = 0; i < numRows; i++) {
final Row r = rows.get(i);
final int cellBorderLeftMost = r.getCellBorderLeftMost();
if ((minCellBorderLeft == -1) || (cellBorderLeftMost < minCellBorderLeft)) {
minCellBorderLeft = cellBorderLeftMost;
}
final int cellBorderRightMost = r.getCellBorderRightMost();
if ((minCellBorderRight == -1) || (cellBorderRightMost < minCellBorderRight)) {
minCellBorderRight = cellBorderRightMost;
}
}
final int minCellBorderTop = rows.get(0).minCellBorderTop;
final int minCellBorderBottom = rows.get(0).minCellBorderBottom;
final Insets groupBorderInsets = rowGroupElem == null ? null : getCSSInsets(rowGroupElem.getRenderState());
if (groupBorderInsets != null) {
if (groupBorderInsets.top <= minCellBorderTop) {
borderOverrider.topOverridden = true;
} else {
final Row firstRow = rows.get(0);
for (final VirtualCell cell : firstRow.cells) {
// TODO: Only override if cells border is less than minCellBorderTop (?)
cell.getActualCell().borderOverrider.topOverridden = true;
}
}
if (groupBorderInsets.bottom <= minCellBorderBottom) {
borderOverrider.bottomOverridden = true;
} else {
final Row lastRow = rows.get(rows.size() - 1);
for (final VirtualCell cell : lastRow.cells) {
// TODO: Only override if cells border is less than minCellBorderBottom (?)
cell.getActualCell().borderOverrider.bottomOverridden = true;
}
}
if (groupBorderInsets.left <= minCellBorderLeft) {
borderOverrider.leftOverridden = true;
} else {
for (final Row row : rows) {
row.getLeftMostCell().getActualCell().borderOverrider.leftOverridden = true;
}
}
if (groupBorderInsets.right <= minCellBorderRight) {
borderOverrider.rightOverridden = true;
} else {
for (final Row row : rows) {
row.getRightMostCell().getActualCell().borderOverrider.rightOverridden = true;
}
}
}
}
@Nullable HtmlInsets getGroupBorderInsets() {
final BorderInfo borderInfo = rowGroupElem == null ? null : rowGroupElem.getRenderState().getBorderInfo();
return borderInfo == null ? null : borderOverrider.get(borderInfo.insets);
}
}
private static final class Row {
final ArrayList<VirtualCell> cells = new ArrayList<>();
final HTMLElementImpl rowGroupElem;
RowGroup rowGroup;
// TODO: Add getters and make private for the following four
public boolean firstInGroup;
public boolean lastInGroup;
public int maxCellBorderTop = 0;
public int maxCellBorderBottom = 0;
int minCellBorderBottom = -1;
int minCellBorderTop = -1;
int rowIndex;
Row(final HTMLElementImpl rowGroup) {
this.rowGroupElem = rowGroup;
}
VirtualCell getLeftMostCell() {
return cells.get(0);
}
VirtualCell getRightMostCell() {
return cells.get(cells.size() - 1);
}
int getCellBorderRightMost() {
return getCSSInsets(getLeftMostCell().getActualCell().getRenderState()).right;
}
int getCellBorderLeftMost() {
return getCSSInsets(getLeftMostCell().getActualCell().getRenderState()).left;
}
void add(final @Nullable VirtualCell cell) {
if (cell != null) {
final RAbstractCell ac = cell.getActualCell();
final @NonNull RenderState rs = ac.getRenderState();
BorderInfo binfo = rs.getBorderInfo();
if (binfo != null) {
final HtmlInsets bi = binfo.insets;
if (bi != null) {
if (bi.top > maxCellBorderTop) {
maxCellBorderTop = bi.top;
}
if ((bi.top < minCellBorderTop) || (minCellBorderTop == -1)) {
minCellBorderTop = bi.top;
}
if (bi.bottom > maxCellBorderBottom) {
maxCellBorderBottom = bi.bottom;
}
if ((bi.bottom < minCellBorderBottom) || (minCellBorderBottom == -1)) {
minCellBorderBottom = bi.bottom;
}
}
}
}
cells.add(cell);
}
public void add(int nc, VirtualCell virtualCell) {
cells.add(nc, virtualCell);
}
public int size() {
return cells.size();
}
public VirtualCell get(int c) {
return cells.get(c);
}
}
/** A class that helps map elements to children (or their delegates). It automatically takes care of
* non-existing parents by creating a place holder.
* For example, helps map table rows to virtual cells (which are delegates for table columns).
*/
private static final class TableRelation {
private final Map<HTMLElementImpl, Row> elementToRow = new HashMap<>(2);
private Row currentFallbackRow = null;
private final ArrayList<Row> listOfRows;
private final ArrayList<RowGroup> listOfRowGroups;
public TableRelation(final ArrayList<Row> listOfRows, final ArrayList<RowGroup> listOfRowGroups) {
this.listOfRows = listOfRows;
this.listOfRowGroups = listOfRowGroups;
}
void associate(final HTMLElementImpl rowGroupElem, final HTMLElementImpl rowElem, final VirtualCell cell) {
Row row;
if (rowElem != null) {
currentFallbackRow = null;
row = elementToRow.get(rowElem);
if (row == null) {
row = createRow(rowGroupElem);
elementToRow.put(rowElem, row);
}
} else {
// Doesn't have a parent. Let's add a list just for itself.
if (currentFallbackRow != null) {
row = currentFallbackRow;
} else {
row = createRow(rowGroupElem);
currentFallbackRow = row;
}
}
row.add(cell);
}
private Row createRow(final HTMLElementImpl rowGroupElem) {
final Row row = new Row(rowGroupElem);
row.rowIndex = this.listOfRows.size();
this.listOfRows.add(row);
return row;
}
void finish() {
HTMLElementImpl prevRowGroupElem = null;
RowGroup currentRowGroup = null;
int numRows = listOfRows.size();
for (int i = 0; i < numRows; i++) {
final Row row = listOfRows.get(i);
row.firstInGroup = (i == 0) || (row.rowGroupElem != prevRowGroupElem);
row.lastInGroup = (i == numRows - 1) || (listOfRows.get(i+1).rowGroupElem != row.rowGroupElem);
if (row.firstInGroup) {
currentRowGroup = new RowGroup(row.rowGroupElem);
this.listOfRowGroups.add(currentRowGroup);
}
assert(currentRowGroup != null);
currentRowGroup.add(row);
if (row.lastInGroup) {
currentRowGroup.finish();
}
prevRowGroupElem = row.rowGroupElem;
}
}
}
/**
* Populates the ROWS and ALL_CELLS collections.
*/
private ArrayList<HTMLElementImpl> populateRows() {
final HTMLElementImpl te = this.tableElement;
final ArrayList<HTMLElementImpl> rowElements = new ArrayList<>();
final NodeImpl[] tChildren = te.getChildrenArray();
final TableRelation rowRelation = new TableRelation(this.ROWS, this.ROW_GROUPS);
if (tChildren != null) {
for (final NodeImpl cn : tChildren) {
if (cn instanceof HTMLElementImpl) {
final HTMLElementImpl ce = (HTMLElementImpl) cn;
final int display = ce.getRenderState().getDisplay();
if (display == RenderState.DISPLAY_TABLE_ROW_GROUP || display == RenderState.DISPLAY_TABLE_HEADER_GROUP
|| display == RenderState.DISPLAY_TABLE_FOOTER_GROUP) {
processRowGroup(ce, rowRelation);
} else if (display == RenderState.DISPLAY_TABLE_ROW) {
processRow(ce, null, rowRelation);
} else if (display == RenderState.DISPLAY_TABLE_CELL) {
processCell(ce, null, null, rowRelation);
} else if (display != RenderState.DISPLAY_TABLE_COLUMN && display != RenderState.DISPLAY_TABLE_COLUMN_GROUP) {
addAnonCell(rowRelation, null, null, cn);
}
} else if (cn instanceof TextImpl) {
addAnonTextCell(rowRelation, null, null, (TextImpl) cn);
}
}
}
rowRelation.finish();
{
// Find the max insets among row group elements
maxRowGroupLeft = 0;
maxRowGroupRight = 0;
for (final RowGroup rowGroup : this.ROW_GROUPS) {
final HtmlInsets groupInsets = rowGroup.getGroupBorderInsets();
if (groupInsets != null) {
if (groupInsets.left > maxRowGroupLeft) {
maxRowGroupLeft = groupInsets.left;
}
if (groupInsets.right > maxRowGroupRight) {
maxRowGroupRight = groupInsets.right;
}
}
}
}
return rowElements;
}
private void processCell(HTMLElementImpl ce, HTMLElementImpl rowGroupElem, HTMLElementImpl rowElem, TableRelation rowRelation) {
RTableCell ac = new RTableCell(ce, this.uaContext, this.rendererContext, this.frameContext, this.container);
ac.setParent(this.relement);
ce.setUINode(ac);
final VirtualCell vc = new VirtualCell(ac, true);
ac.setTopLeftVirtualCell(vc);
rowRelation.associate(rowGroupElem, rowElem, vc);
this.ALL_CELLS.add(ac);
}
private void processRow(HTMLElementImpl rowE, HTMLElementImpl rowGroupElem, TableRelation rowRelation) {
final NodeImpl[] rChildren = rowE.getChildrenArray();
if (rChildren != null) {
for (final NodeImpl cn : rChildren) {
if (cn instanceof HTMLElementImpl) {
final HTMLElementImpl ce = (HTMLElementImpl) cn;
final int display = ce.getRenderState().getDisplay();
if (display == RenderState.DISPLAY_TABLE_CELL) {
processCell(ce, rowGroupElem, rowE, rowRelation);
} else {
addAnonCell(rowRelation, rowGroupElem, rowE, cn);
}
} else if (cn instanceof TextImpl) {
addAnonTextCell(rowRelation, rowGroupElem, rowE, (TextImpl) cn);
}
}
}
}
private void processRowGroup(HTMLElementImpl rowGroupElem, TableRelation rowRelation) {
final NodeImpl[] rChildren = rowGroupElem.getChildrenArray();
if (rChildren != null) {
for (final NodeImpl cn : rChildren) {
if (cn instanceof HTMLElementImpl) {
final HTMLElementImpl ce = (HTMLElementImpl) cn;
final int display = ce.getRenderState().getDisplay();
if (display == RenderState.DISPLAY_TABLE_ROW) {
processRow(ce, rowGroupElem, rowRelation);
} else {
addAnonCell(rowRelation, rowGroupElem, null, cn);
}
} else if (cn instanceof TextImpl) {
addAnonTextCell(rowRelation, rowGroupElem, null, (TextImpl) cn);
}
}
}
}
private void addAnonTextCell(final TableRelation rowRelation, HTMLElementImpl rowGroupElem, HTMLElementImpl rowElem, final TextImpl tn) {
if (!tn.isElementContentWhitespace()) {
addAnonCell(rowRelation, rowGroupElem, rowElem, tn);
}
}
private void addAnonCell(final TableRelation rowRelation, HTMLElementImpl rowGroupElem, HTMLElementImpl rowElem, final NodeImpl node) {
final AnonymousNodeImpl acn = new AnonymousNodeImpl(node.getParentNode());
acn.appendChildSilently(node);
final RAnonTableCell ac = new RAnonTableCell(acn, this.uaContext, this.rendererContext, this.frameContext, this.container);
ac.setParent(this.relement);
acn.setUINode(ac);
final VirtualCell vc = new VirtualCell(ac, true);
ac.setTopLeftVirtualCell(vc);
rowRelation.associate(rowGroupElem, rowElem, vc);
this.ALL_CELLS.add(ac);
}
/**
* Based on colspans and rowspans, creates additional virtual cells from
* actual table cells.
*/
private void adjustForCellSpans() {
final ArrayList<Row> rows = this.ROWS;
int numRows = rows.size();
for (int r = 0; r < numRows; r++) {
final Row row = rows.get(r);
int numCols = row.size();
for (int c = 0; c < numCols; c++) {
final VirtualCell vc = row.get(c);
if ((vc != null) && vc.isTopLeft()) {
final RAbstractCell ac = vc.getActualCell();
int colspan = ac.getColSpan();
if (colspan < 1) {
colspan = 1;
}
int rowspan = ac.getRowSpan();
if (rowspan < 1) {
rowspan = 1;
}
// Can't go beyond last row (Fix bug #2022584)
final int targetRows = r + rowspan;
if (numRows < targetRows) {
rowspan = numRows - r;
ac.setRowSpan(rowspan);
}
numRows = rows.size();
for (int y = 0; y < rowspan; y++) {
if ((colspan > 1) || (y > 0)) {
// Get row
final int nr = r + y;
final Row newRow = rows.get(nr);
// Insert missing cells in row
final int xstart = y == 0 ? 1 : 0;
// Insert virtual cells, potentially
// shifting others to the right.
for (int cc = xstart; cc < colspan; cc++) {
final int nc = c + cc;
while (newRow.size() < nc) {
newRow.add(null);
}
newRow.add(nc, new VirtualCell(ac, false));
}
if (row == newRow) {
numCols = row.size();
}
}
}
}
}
}
// Adjust row and column of virtual cells
for (int r = 0; r < numRows; r++) {
final Row row = rows.get(r);
final int numCols = row.size();
for (int c = 0; c < numCols; c++) {
final VirtualCell vc = row.get(c);
if (vc != null) {
vc.setColumn(c);
vc.setRow(r);
}
}
}
}
/**
* Populates the columnSizes and rowSizes arrays, setting htmlLength in each
* element.
*/
private void createSizeArrays() {
int numCols = 0;
final ArrayList<Row> rows = this.ROWS;
final int numRows = rows.size();
{
final RowSizeInfo[] rowSizes = new RowSizeInfo[numRows];
this.rowSizes = rowSizes;
for (int i = 0; i < numRows; i++) {
final Row row = rows.get(i);
final int numColsInThisRow = row.size();
if (numColsInThisRow > numCols) {
numCols = numColsInThisRow;
}
final RowSizeInfo rowSizeInfo = new RowSizeInfo();
rowSizes[i] = rowSizeInfo;
HtmlLength bestHeightLength = null;
for (int x = 0; x < numColsInThisRow; x++) {
final VirtualCell vc = row.get(x);
if (vc != null) {
final HtmlLength vcHeightLength = vc.getHeightLength();
if ((vcHeightLength != null) && vcHeightLength.isPreferredOver(bestHeightLength)) {
bestHeightLength = vcHeightLength;
}
rowSizeInfo.offsetX = maxRowGroupLeft;
}
}
rowSizeInfo.htmlLength = bestHeightLength;
@Nullable HtmlInsets rowGroupInsets = row.rowGroup.getGroupBorderInsets();
if (row.firstInGroup && rowGroupInsets != null) {
rowSizeInfo.marginTop = Math.max(0, rowGroupInsets.top);
}
if (row.lastInGroup && rowGroupInsets != null) {
rowSizeInfo.marginBottom = Math.max(0, rowGroupInsets.bottom - row.maxCellBorderBottom);
}
}
}
final ColSizeInfo[] columnSizes = new ColSizeInfo[numCols];
this.columnSizes = columnSizes;
for (int i = 0; i < numCols; i++) {
HtmlLength bestWidthLength = null;
// Cells with colspan==1 first.
for (int y = 0; y < numRows; y++) {
final Row row = rows.get(y);
VirtualCell vc;
try {
vc = row.get(i);
} catch (final IndexOutOfBoundsException iob) {
vc = null;
}
if (vc != null) {
final RAbstractCell ac = vc.getActualCell();
if (ac.getColSpan() == 1) {
final HtmlLength vcWidthLength = vc.getWidthLength();
if ((vcWidthLength != null) && vcWidthLength.isPreferredOver(bestWidthLength)) {
bestWidthLength = vcWidthLength;
}
}
}
}
// Now cells with colspan>1.
if (bestWidthLength == null) {
for (int y = 0; y < numRows; y++) {
final Row row = rows.get(y);
VirtualCell vc;
try {
vc = row.get(i);
} catch (final IndexOutOfBoundsException iob) {
vc = null;
}
if (vc != null) {
final RAbstractCell ac = vc.getActualCell();
if (ac.getColSpan() > 1) {
final HtmlLength vcWidthLength = vc.getWidthLength();
if ((vcWidthLength != null) && vcWidthLength.isPreferredOver(bestWidthLength)) {
bestWidthLength = vcWidthLength;
}
}
}
}
}
final ColSizeInfo colSizeInfo = new ColSizeInfo();
colSizeInfo.htmlLength = bestWidthLength;
columnSizes[i] = colSizeInfo;
}
}
/**
* Determines the size of each column, and the table width. Does the
* following:
* <ol>
* <li>Determine tentative widths. This is done by looking at declared column
* widths, any table width, and filling in the blanks. No rendering is done.
* The tentative width of columns with no declared width is zero.
*
* <li>Render all cell blocks. It uses the tentative widths from the previous
* step as a desired width. The resulting width is considered a sort of
* minimum. If the column width is not defined, use a NOWRAP override flag to
* render.
*
* <li>Check if cell widths are too narrow for the rendered width. In the case
* of columns without a declared width, check if they are too wide.
*
* <li>Finally, adjust widths considering the expected max table size. Columns
* are layed out again if necessary to determine if they can really be shrunk.
* </ol>
*
* @param renderState
* @param border
* @param cellSpacingX
* @param cellSpacingY
* @param availWidth
*/
private void determineColumnSizes(final int hasBorder, final int cellSpacingX, final int cellSpacingY, final int availWidth) {
final HtmlLength tableWidthLength = this.tableWidthLength;
int tableWidth;
boolean widthKnown;
if (tableWidthLength != null) {
tableWidth = tableWidthLength.getLength(availWidth);
widthKnown = true;
} else {
tableWidth = availWidth;
widthKnown = false;
}
tableWidth -= (this.maxRowGroupLeft + this.maxRowGroupRight) / 2;
final ColSizeInfo[] columnSizes = this.columnSizes;
final int widthsOfExtras = this.widthsOfExtras;
int cellAvailWidth = tableWidth - widthsOfExtras;
if (cellAvailWidth < 0) {
tableWidth += (-cellAvailWidth);
cellAvailWidth = 0;
}
// Determine tentative column widths based on specified cell widths
determineTentativeSizes(columnSizes, widthsOfExtras, cellAvailWidth, widthKnown);
// Pre-layout cells. This will give the minimum width of each cell,
// in addition to the minimum height.
this.preLayout(hasBorder, cellSpacingX, cellSpacingY, widthKnown);
// Increases column widths if they are less than minimums of each cell.
adjustForLayoutWidths(columnSizes, hasBorder, cellSpacingX, widthKnown);
// Adjust for expected total width
this.adjustWidthsForExpectedMax(columnSizes, cellAvailWidth, widthKnown);
}
/**
* This method sets the tentative actual sizes of columns (rows) based on
* specified widths (heights) if available.
*
* @param columnSizes
* @param widthsOfExtras
* @param cellAvailWidth
*/
private static void determineTentativeSizes(final ColSizeInfo[] columnSizes, final int widthsOfExtras, final int cellAvailWidth,
final boolean setNoWidthColumns) {
final int numCols = columnSizes.length;
// Look at percentages first
int widthUsedByPercent = 0;
for (int i = 0; i < numCols; i++) {
final ColSizeInfo colSizeInfo = columnSizes[i];
final HtmlLength widthLength = colSizeInfo.htmlLength;
if ((widthLength != null) && (widthLength.getLengthType() == HtmlLength.LENGTH)) {
final int actualSizeInt = widthLength.getLength(cellAvailWidth);
widthUsedByPercent += actualSizeInt;
colSizeInfo.actualSize = actualSizeInt;
}
}
// Look at columns with absolute sizes
int widthUsedByAbsolute = 0;
int numNoWidthColumns = 0;
for (int i = 0; i < numCols; i++) {
final ColSizeInfo colSizeInfo = columnSizes[i];
final HtmlLength widthLength = colSizeInfo.htmlLength;
if ((widthLength != null) && (widthLength.getLengthType() != HtmlLength.LENGTH)) {
// TODO: MULTI-LENGTH not supported
final int actualSizeInt = widthLength.getRawValue();
widthUsedByAbsolute += actualSizeInt;
colSizeInfo.actualSize = actualSizeInt;
} else if (widthLength == null) {
numNoWidthColumns++;
}
}
// Tentative width of all columns without a declared
// width is set to zero. The pre-render will determine
// a better size.
// // Assign all columns without widths now
// int widthUsedByUnspecified = 0;
// if(setNoWidthColumns) {
// int remainingWidth = cellAvailWidth - widthUsedByAbsolute -
// widthUsedByPercent;
// if(remainingWidth > 0) {
// for(int i = 0; i < numCols; i++) {
// SizeInfo colSizeInfo = columnSizes[i];
// HtmlLength widthLength = colSizeInfo.htmlLength;
// if(widthLength == null) {
// int actualSizeInt = remainingWidth / numNoWidthColumns;
// widthUsedByUnspecified += actualSizeInt;
// colSizeInfo.actualSize = actualSizeInt;
// }
// }
// }
// }
// Contract if necessary. This is done again later, but this is
// an optimization, as it may prevent re-layout. It is only done
// if all columns have some kind of declared width.
if (numNoWidthColumns == 0) {
int totalWidthUsed = widthUsedByPercent + widthUsedByAbsolute;
int difference = totalWidthUsed - cellAvailWidth;
// See if absolutes need to be contracted
if (difference > 0) {
if (widthUsedByAbsolute > 0) {
int expectedAbsoluteWidthTotal = widthUsedByAbsolute - difference;
if (expectedAbsoluteWidthTotal < 0) {
expectedAbsoluteWidthTotal = 0;
}
final double ratio = (double) expectedAbsoluteWidthTotal / widthUsedByAbsolute;
for (int i = 0; i < numCols; i++) {
final ColSizeInfo sizeInfo = columnSizes[i];
final HtmlLength widthLength = columnSizes[i].htmlLength;
if ((widthLength != null) && (widthLength.getLengthType() != HtmlLength.LENGTH)) {
final int oldActualSize = sizeInfo.actualSize;
final int newActualSize = (int) Math.round(oldActualSize * ratio);
sizeInfo.actualSize = newActualSize;
totalWidthUsed += (newActualSize - oldActualSize);
}
}
difference = totalWidthUsed - cellAvailWidth;
}
// See if percentages need to be contracted
if (difference > 0) {
if (widthUsedByPercent > 0) {
int expectedPercentWidthTotal = widthUsedByPercent - difference;
if (expectedPercentWidthTotal < 0) {
expectedPercentWidthTotal = 0;
}
final double ratio = (double) expectedPercentWidthTotal / widthUsedByPercent;
for (int i = 0; i < numCols; i++) {
final ColSizeInfo sizeInfo = columnSizes[i];
final HtmlLength widthLength = columnSizes[i].htmlLength;
if ((widthLength != null) && (widthLength.getLengthType() == HtmlLength.LENGTH)) {
final int oldActualSize = sizeInfo.actualSize;
final int newActualSize = (int) Math.round(oldActualSize * ratio);
sizeInfo.actualSize = newActualSize;
totalWidthUsed += (newActualSize - oldActualSize);
}
}
}
}
}
}
}
/**
* Expands column sizes according to layout sizes.
*/
private static void adjustForLayoutWidths(final ColSizeInfo[] columnSizes, final int hasBorder, final int cellSpacing,
final boolean tableWidthKnown) {
final int numCols = columnSizes.length;
for (int i = 0; i < numCols; i++) {
final ColSizeInfo si = columnSizes[i];
if (si.actualSize < si.layoutSize) {
si.actualSize = si.layoutSize;
}
if (si.fullActualSize < si.fullLayoutSize) {
si.fullActualSize = si.fullLayoutSize;
}
// else if(si.htmlLength == null) {
// // For cells without a declared width, see if
// // their tentative width is a bit too big.
// if(si.actualSize > si.layoutSize) {
// si.actualSize = si.layoutSize;
// }
// }
}
}
private void layoutColumn(final ColSizeInfo[] columnSizes, final ColSizeInfo colSize, final int col, final int cellSpacingX, final int hasBorder) {
final RowSizeInfo[] rowSizes = this.rowSizes;
final ArrayList<Row> rows = this.ROWS;
final int numRows = rows.size();
final int actualSize = colSize.actualSize;
colSize.layoutSize = 0;
for (int rowIndx = 0; rowIndx < numRows;) {
// SizeInfo rowSize = rowSizes[row];
final Row row = rows.get(rowIndx);
VirtualCell vc = null;
try {
vc = row.get(col);
} catch (final IndexOutOfBoundsException iob) {
vc = null;
}
final RAbstractCell ac = vc == null ? null : vc.getActualCell();
if (ac != null) {
if (ac.getVirtualRow() == rowIndx) {
// Only process actual cells with a row
// beginning at the current row being processed.
final int colSpan = ac.getColSpan();
if (colSpan > 1) {
final int firstCol = ac.getVirtualColumn();
final int cellExtras = (colSpan - 1) * (cellSpacingX + (2 * hasBorder));
int vcActualWidth = cellExtras;
for (int x = 0; x < colSpan; x++) {
vcActualWidth += columnSizes[firstCol + x].actualSize;
}
// TODO: better height possible
final Dimension size = ac.doCellLayout(vcActualWidth, 0, true, true, true);
final int vcRenderWidth = size.width;
final int denominator = (vcActualWidth - cellExtras);
int newTentativeCellWidth;
if (denominator > 0) {
newTentativeCellWidth = (actualSize * (vcRenderWidth - cellExtras)) / denominator;
} else {
newTentativeCellWidth = (vcRenderWidth - cellExtras) / colSpan;
}
if (newTentativeCellWidth > colSize.layoutSize) {
colSize.layoutSize = newTentativeCellWidth;
}
final int rowSpan = ac.getRowSpan();
final int vch = (size.height - ((rowSpan - 1) * (this.cellSpacingY + (2 * hasBorder)))) / rowSpan;
for (int y = 0; y < rowSpan; y++) {
if (rowSizes[rowIndx + y].minSize < vch) {
rowSizes[rowIndx + y].minSize = vch;
}
}
} else {
// TODO: better height possible
final Dimension size = ac.doCellLayout(actualSize, 0, true, true, true);
if (size.width > colSize.layoutSize) {
colSize.layoutSize = size.width;
}
@NonNull Insets cbi = ac.getBorderInsets();
final int cellFullLayoutWidth = size.width + cbi.left + cbi.right;
if (cellFullLayoutWidth > colSize.fullLayoutSize) {
colSize.fullLayoutSize = cellFullLayoutWidth;
}
final int rowSpan = ac.getRowSpan();
final int vch = (size.height - ((rowSpan - 1) * (this.cellSpacingY + (2 * hasBorder)))) / rowSpan;
for (int y = 0; y < rowSpan; y++) {
if (rowSizes[rowIndx + y].minSize < vch) {
rowSizes[rowIndx + y].minSize = vch;
}
}
}
}
}
// row = (ac == null ? row + 1 : ac.getVirtualRow() + ac.getRowSpan());
rowIndx++;
}
}
private int adjustWidthsForExpectedMax(final ColSizeInfo[] columnSizes, final int cellAvailWidth, final boolean expand) {
final int hasBorder = this.hasOldStyleBorder;
final int cellSpacingX = this.cellSpacingX;
int currentTotal = 0;
final int numCols = columnSizes.length;
for (int i = 0; i < numCols; i++) {
currentTotal += columnSizes[i].fullActualSize;
}
int difference = currentTotal - (this.widthsOfExtras + cellAvailWidth);
// int difference = currentTotal - (cellAvailWidth);
if ((difference > 0) || ((difference < 0) && expand)) {
// First, try to contract/expand columns with no width
int noWidthTotal = 0;
int numNoWidth = 0;
for (int i = 0; i < numCols; i++) {
if (columnSizes[i].htmlLength == null) {
numNoWidth++;
noWidthTotal += columnSizes[i].fullActualSize;
}
}
if (numNoWidth > 0) {
// TODO: This is not shrinking correctly.
int expectedNoWidthTotal = noWidthTotal - difference - this.widthsOfExtras;
if (expectedNoWidthTotal < 0) {
expectedNoWidthTotal = 0;
}
final double ratio = ((double) expectedNoWidthTotal) / noWidthTotal;
int noWidthCount = 0;
for (int i = 0; i < numCols; i++) {
final ColSizeInfo sizeInfo = columnSizes[i];
if (sizeInfo.htmlLength == null) {
final int oldActualSize = sizeInfo.fullActualSize;
int newActualSize;
if (++noWidthCount == numNoWidth) {
// Last column without a width.
final int currentDiff = currentTotal - cellAvailWidth;
newActualSize = oldActualSize - currentDiff;
if (newActualSize < 0) {
newActualSize = 0;
}
} else {
newActualSize = (int) Math.round(oldActualSize * ratio);
}
sizeInfo.actualSize = newActualSize;
if (newActualSize < sizeInfo.fullLayoutSize) {
// See if it actually fits.
this.layoutColumn(columnSizes, sizeInfo, i, cellSpacingX, hasBorder);
if (newActualSize < sizeInfo.layoutSize) {
// Didn't fit.
newActualSize = sizeInfo.layoutSize;
sizeInfo.actualSize = newActualSize;
}
}
currentTotal += (newActualSize - oldActualSize);
}
}
difference = currentTotal - cellAvailWidth;
}
// See if absolutes need to be contracted
if ((difference > 0) || ((difference < 0) && expand)) {
int absoluteWidthTotal = 0;
for (int i = 0; i < numCols; i++) {
final HtmlLength widthLength = columnSizes[i].htmlLength;
if ((widthLength != null) && (widthLength.getLengthType() != HtmlLength.LENGTH)) {
absoluteWidthTotal += columnSizes[i].fullActualSize;
}
}
if (absoluteWidthTotal > 0) {
int expectedAbsoluteWidthTotal = absoluteWidthTotal - difference - this.widthsOfExtras;
if (expectedAbsoluteWidthTotal < 0) {
expectedAbsoluteWidthTotal = 0;
}
final double ratio = ((double) expectedAbsoluteWidthTotal) / absoluteWidthTotal;
for (int i = 0; i < numCols; i++) {
final ColSizeInfo sizeInfo = columnSizes[i];
final HtmlLength widthLength = columnSizes[i].htmlLength;
if ((widthLength != null) && (widthLength.getLengthType() != HtmlLength.LENGTH)) {
final int oldActualSize = sizeInfo.fullActualSize;
int newActualSize = (int) Math.round(oldActualSize * ratio);
sizeInfo.actualSize = newActualSize;
if (newActualSize < sizeInfo.fullLayoutSize) {
// See if it actually fits.
this.layoutColumn(columnSizes, sizeInfo, i, cellSpacingX, hasBorder);
if (newActualSize < sizeInfo.layoutSize) {
// Didn't fit.
newActualSize = sizeInfo.layoutSize;
sizeInfo.actualSize = newActualSize;
}
}
currentTotal += (newActualSize - oldActualSize);
}
}
difference = currentTotal - cellAvailWidth;
}
// See if percentages need to be contracted
if ((difference > 0) || ((difference < 0) && expand)) {
int percentWidthTotal = 0;
for (int i = 0; i < numCols; i++) {
final HtmlLength widthLength = columnSizes[i].htmlLength;
if ((widthLength != null) && (widthLength.getLengthType() == HtmlLength.LENGTH)) {
percentWidthTotal += columnSizes[i].actualSize;
}
}
if (percentWidthTotal > 0) {
int expectedPercentWidthTotal = percentWidthTotal - difference;
if (expectedPercentWidthTotal < 0) {
expectedPercentWidthTotal = 0;
}
final double ratio = (double) expectedPercentWidthTotal / percentWidthTotal;
for (int i = 0; i < numCols; i++) {
final ColSizeInfo sizeInfo = columnSizes[i];
final HtmlLength widthLength = columnSizes[i].htmlLength;
if ((widthLength != null) && (widthLength.getLengthType() == HtmlLength.LENGTH)) {
final int oldActualSize = sizeInfo.actualSize;
int newActualSize = (int) Math.round(oldActualSize * ratio);
sizeInfo.actualSize = newActualSize;
if (newActualSize < sizeInfo.layoutSize) {
// See if it actually fits.
this.layoutColumn(columnSizes, sizeInfo, i, cellSpacingX, hasBorder);
if (newActualSize < sizeInfo.layoutSize) {
// Didn't fit.
newActualSize = sizeInfo.layoutSize;
sizeInfo.actualSize = newActualSize;
}
}
currentTotal += (newActualSize - oldActualSize);
}
}
}
}
}
} else {
if (expand) {
for (int i = 0; i < numCols; i++) {
final ColSizeInfo sizeInfo = columnSizes[i];
sizeInfo.actualSize = sizeInfo.fullActualSize;
}
}
}
return currentTotal;
}
/**
* This method renders each cell using already set actual column widths. It
* sets minimum row heights based on this.
*/
private final void preLayout(final int hasBorder, final int cellSpacingX, final int cellSpacingY, final boolean tableWidthKnown) {
// TODO: Fix for table without width that has a subtable with width=100%.
// TODO: Maybe it can be addressed when NOWRAP is implemented.
// TODO: Maybe it's possible to eliminate this pre-layout altogether.
final ColSizeInfo[] colSizes = this.columnSizes;
final RowSizeInfo[] rowSizes = this.rowSizes;
// Initialize minSize in rows
final int numRows = rowSizes.length;
for (int i = 0; i < numRows; i++) {
rowSizes[i].minSize = 0;
}
// Initialize layoutSize in columns
final int numCols = colSizes.length;
for (int i = 0; i < numCols; i++) {
colSizes[i].layoutSize = 0;
colSizes[i].fullLayoutSize = 0;
}
for (@NonNull RAbstractCell cell: this.ALL_CELLS) {
final int col = cell.getVirtualColumn();
final int colSpan = cell.getColSpan();
int cellsTotalWidth;
int cellsUsedWidth;
boolean widthDeclared = false;
if (colSpan > 1) {
cellsUsedWidth = 0;
for (int x = 0; x < colSpan; x++) {
final ColSizeInfo colSize = colSizes[col + x];
if (colSize.htmlLength != null) {
widthDeclared = true;
}
cellsUsedWidth += colSize.actualSize;
}
cellsTotalWidth = cellsUsedWidth + ((colSpan - 1) * (cellSpacingX + (2 * hasBorder)));
} else {
final ColSizeInfo colSize = colSizes[col];
if (colSize.htmlLength != null) {
widthDeclared = true;
}
cellsUsedWidth = cellsTotalWidth = colSize.actualSize;
}
// TODO: A tentative height could be used here: Height of
// table divided by number of rows.
java.awt.Dimension size;
final RenderThreadState state = RenderThreadState.getState();
final boolean prevOverrideNoWrap = state.overrideNoWrap;
try {
if (!prevOverrideNoWrap) {
state.overrideNoWrap = !widthDeclared;
}
size = cell.doCellLayout(cellsTotalWidth, 0, true, true, true);
} finally {
state.overrideNoWrap = prevOverrideNoWrap;
}
// Set render widths
final int cellLayoutWidth = size.width;
@NonNull Insets cbi = cell.getBorderInsets();
final int cellFullLayoutWidth = size.width + cbi.left + cbi.right;
if (colSpan > 1) {
// TODO: set fullLayoutSize
if (cellsUsedWidth > 0) {
final double ratio = (double) cellLayoutWidth / cellsUsedWidth;
for (int x = 0; x < colSpan; x++) {
final ColSizeInfo si = colSizes[col + x];
final int newLayoutSize = (int) Math.round(si.actualSize * ratio);
if (si.layoutSize < newLayoutSize) {
si.layoutSize = newLayoutSize;
}
}
} else {
final int newLayoutSize = cellLayoutWidth / colSpan;
for (int x = 0; x < colSpan; x++) {
final ColSizeInfo si = colSizes[col + x];
if (si.layoutSize < newLayoutSize) {
si.layoutSize = newLayoutSize;
}
}
}
} else {
final ColSizeInfo colSizeInfo = colSizes[col];
if (colSizeInfo.layoutSize < cellLayoutWidth) {
colSizeInfo.layoutSize = cellLayoutWidth;
}
if (colSizeInfo.fullLayoutSize < cellFullLayoutWidth) {
colSizeInfo.fullLayoutSize = cellFullLayoutWidth;
}
}
// Set minimum heights
final int actualCellHeight = size.height;
final int row = cell.getVirtualRow();
final int rowSpan = cell.getRowSpan();
if (rowSpan > 1) {
final int vch = (actualCellHeight - ((rowSpan - 1) * (cellSpacingY + (2 * hasBorder)))) / rowSpan;
for (int y = 0; y < rowSpan; y++) {
if (rowSizes[row + y].minSize < vch) {
rowSizes[row + y].minSize = vch;
}
}
} else {
if (rowSizes[row].minSize < actualCellHeight) {
rowSizes[row].minSize = actualCellHeight;
}
}
}
}
private void determineRowSizes(final int hasBorder, final int cellSpacing, final int availHeight, final boolean sizeOnly) {
final HtmlLength tableHeightLength = TableMatrix.getHeightLength(this.tableElement, availHeight);
int tableHeight;
final RowSizeInfo[] rowSizes = this.rowSizes;
final int numRows = rowSizes.length;
final int heightsOfExtras = this.heightsOfExtras;
if (tableHeightLength != null) {
tableHeight = tableHeightLength.getLength(availHeight);
this.determineRowSizesFixedTH(hasBorder, cellSpacing, availHeight, tableHeight, sizeOnly);
} else {
tableHeight = heightsOfExtras;
for (int row = 0; row < numRows; row++) {
tableHeight += rowSizes[row].minSize;
}
this.determineRowSizesFlexibleTH(hasBorder, cellSpacing, availHeight, sizeOnly);
}
}
private void determineRowSizesFixedTH(final int hasBorder, final int cellSpacing, final int availHeight, final int tableHeight,
final boolean sizeOnly) {
final RowSizeInfo[] rowSizes = this.rowSizes;
final int numRows = rowSizes.length;
final int heightsOfExtras = this.heightsOfExtras;
int cellAvailHeight = tableHeight - heightsOfExtras;
if (cellAvailHeight < 0) {
cellAvailHeight = 0;
}
// Look at percentages first
int heightUsedbyPercent = 0;
int otherMinSize = 0;
for (int i = 0; i < numRows; i++) {
final RowSizeInfo rowSizeInfo = rowSizes[i];
final HtmlLength heightLength = rowSizeInfo.htmlLength;
if ((heightLength != null) && (heightLength.getLengthType() == HtmlLength.LENGTH)) {
int actualSizeInt = heightLength.getLength(cellAvailHeight);
if (actualSizeInt < rowSizeInfo.minSize) {
actualSizeInt = rowSizeInfo.minSize;
}
heightUsedbyPercent += actualSizeInt;
rowSizeInfo.actualSize = actualSizeInt;
} else {
otherMinSize += rowSizeInfo.minSize;
}
}
// Check if rows with percent are bigger than they should be
if ((heightUsedbyPercent + otherMinSize) > cellAvailHeight) {
final double ratio = (double) (cellAvailHeight - otherMinSize) / heightUsedbyPercent;
for (int i = 0; i < numRows; i++) {
final RowSizeInfo rowSizeInfo = rowSizes[i];
final HtmlLength heightLength = rowSizeInfo.htmlLength;
if ((heightLength != null) && (heightLength.getLengthType() == HtmlLength.LENGTH)) {
final int actualSize = rowSizeInfo.actualSize;
final int prevActualSize = actualSize;
int newActualSize = (int) Math.round(prevActualSize * ratio);
if (newActualSize < rowSizeInfo.minSize) {
newActualSize = rowSizeInfo.minSize;
}
heightUsedbyPercent += (newActualSize - prevActualSize);
rowSizeInfo.actualSize = newActualSize;
}
}
}
// Look at rows with absolute sizes
int heightUsedByAbsolute = 0;
int noHeightMinSize = 0;
int numNoHeightColumns = 0;
for (int i = 0; i < numRows; i++) {
final RowSizeInfo rowSizeInfo = rowSizes[i];
final HtmlLength heightLength = rowSizeInfo.htmlLength;
if ((heightLength != null) && (heightLength.getLengthType() != HtmlLength.LENGTH)) {
// TODO: MULTI-LENGTH not supported
int actualSizeInt = heightLength.getRawValue();
if (actualSizeInt < rowSizeInfo.minSize) {
actualSizeInt = rowSizeInfo.minSize;
}
heightUsedByAbsolute += actualSizeInt;
rowSizeInfo.actualSize = actualSizeInt;
} else if (heightLength == null) {
numNoHeightColumns++;
noHeightMinSize += rowSizeInfo.minSize;
}
}
// Check if absolute sizing is too much
if ((heightUsedByAbsolute + heightUsedbyPercent + noHeightMinSize) > cellAvailHeight) {
final double ratio = (double) (cellAvailHeight - noHeightMinSize - heightUsedbyPercent) / heightUsedByAbsolute;
for (int i = 0; i < numRows; i++) {
final RowSizeInfo rowSizeInfo = rowSizes[i];
final HtmlLength heightLength = rowSizeInfo.htmlLength;
if ((heightLength != null) && (heightLength.getLengthType() != HtmlLength.LENGTH)) {
final int actualSize = rowSizeInfo.actualSize;
final int prevActualSize = actualSize;
int newActualSize = (int) Math.round(prevActualSize * ratio);
if (newActualSize < rowSizeInfo.minSize) {
newActualSize = rowSizeInfo.minSize;
}
heightUsedByAbsolute += (newActualSize - prevActualSize);
rowSizeInfo.actualSize = newActualSize;
}
}
}
// Assign all rows without heights now
final int remainingHeight = cellAvailHeight - heightUsedByAbsolute - heightUsedbyPercent;
int heightUsedByRemaining = 0;
for (int i = 0; i < numRows; i++) {
final RowSizeInfo rowSizeInfo = rowSizes[i];
final HtmlLength heightLength = rowSizeInfo.htmlLength;
if (heightLength == null) {
int actualSizeInt = remainingHeight / numNoHeightColumns;
if (actualSizeInt < rowSizeInfo.minSize) {
actualSizeInt = rowSizeInfo.minSize;
}
heightUsedByRemaining += actualSizeInt;
rowSizeInfo.actualSize = actualSizeInt;
}
}
// Calculate actual table width
final int totalUsed = heightUsedByAbsolute + heightUsedbyPercent + heightUsedByRemaining;
if (totalUsed >= cellAvailHeight) {
this.tableHeight = totalUsed + heightsOfExtras;
} else {
// Rows too short; expand them
final double ratio = (double) cellAvailHeight / totalUsed;
for (int i = 0; i < numRows; i++) {
final RowSizeInfo rowSizeInfo = rowSizes[i];
final int actualSize = rowSizeInfo.actualSize;
rowSizeInfo.actualSize = (int) Math.round(actualSize * ratio);
}
this.tableHeight = tableHeight;
}
// TODO:
// This final render is probably unnecessary. Avoid exponential rendering
// by setting a single height of subcell. Verify that IE only sets height
// of subcells when height of row or table are specified.
this.finalLayout(hasBorder, cellSpacing, sizeOnly);
}
private void determineRowSizesFlexibleTH(final int hasBorder, final int cellSpacing, final int availHeight, final boolean sizeOnly) {
final RowSizeInfo[] rowSizes = this.rowSizes;
final int numRows = rowSizes.length;
final int heightsOfExtras = this.heightsOfExtras;
// Look at rows with absolute sizes
int heightUsedByAbsolute = 0;
int percentSum = 0;
for (int i = 0; i < numRows; i++) {
final RowSizeInfo rowSizeInfo = rowSizes[i];
final HtmlLength heightLength = rowSizeInfo.htmlLength;
if ((heightLength != null) && (heightLength.getLengthType() == HtmlLength.PIXELS)) {
// TODO: MULTI-LENGTH not supported
int actualSizeInt = heightLength.getRawValue();
if (actualSizeInt < rowSizeInfo.minSize) {
actualSizeInt = rowSizeInfo.minSize;
}
heightUsedByAbsolute += actualSizeInt;
rowSizeInfo.actualSize = actualSizeInt;
} else if ((heightLength != null) && (heightLength.getLengthType() == HtmlLength.LENGTH)) {
percentSum += heightLength.getRawValue();
}
}
// Look at rows with no specified heights
int heightUsedByNoSize = 0;
// Set sizes to in row height
for (int i = 0; i < numRows; i++) {
final RowSizeInfo rowSizeInfo = rowSizes[i];
final HtmlLength widthLength = rowSizeInfo.htmlLength;
if (widthLength == null) {
final int actualSizeInt = rowSizeInfo.minSize;
heightUsedByNoSize += actualSizeInt;
rowSizeInfo.actualSize = actualSizeInt;
}
}
// Calculate actual total cell width
final int expectedTotalCellHeight = (int) Math.round((heightUsedByAbsolute + heightUsedByNoSize) / (1 - (percentSum / 100.0)));
// Set widths of columns with percentages
int heightUsedByPercent = 0;
for (int i = 0; i < numRows; i++) {
final RowSizeInfo rowSizeInfo = rowSizes[i];
final HtmlLength heightLength = rowSizeInfo.htmlLength;
if ((heightLength != null) && (heightLength.getLengthType() == HtmlLength.LENGTH)) {
int actualSizeInt = heightLength.getLength(expectedTotalCellHeight);
if (actualSizeInt < rowSizeInfo.minSize) {
actualSizeInt = rowSizeInfo.minSize;
}
heightUsedByPercent += actualSizeInt;
rowSizeInfo.actualSize = actualSizeInt;
}
}
// Set width of table
this.tableHeight = heightUsedByAbsolute + heightUsedByNoSize + heightUsedByPercent + heightsOfExtras;
// Do a final layouts to set actual cell sizes
this.finalLayout(hasBorder, cellSpacing, sizeOnly);
}
/**
* This method layouts each cell using already set actual column widths. It
* sets minimum row heights based on this.
*/
private final void finalLayout(final int hasBorder, final int cellSpacing, final boolean sizeOnly) {
// finalLayout needs to adjust actualSize of columns and rows
// given that things might change as we layout one last time.
final ColSizeInfo[] colSizes = this.columnSizes;
final RowSizeInfo[] rowSizes = this.rowSizes;
for (@NonNull RAbstractCell cell : this.ALL_CELLS) {
final int col = cell.getVirtualColumn();
final int colSpan = cell.getColSpan();
int totalCellWidth;
if (colSpan > 1) {
totalCellWidth = (colSpan - 1) * (cellSpacing + (2 * hasBorder));
for (int x = 0; x < colSpan; x++) {
totalCellWidth += colSizes[col + x].actualSize;
}
} else {
totalCellWidth = colSizes[col].actualSize;
}
final int row = cell.getVirtualRow();
final int rowSpan = cell.getRowSpan();
int totalCellHeight;
if (rowSpan > 1) {
totalCellHeight = (rowSpan - 1) * (cellSpacing + (2 * hasBorder));
for (int y = 0; y < rowSpan; y++) {
totalCellHeight += rowSizes[row + y].actualSize;
}
} else {
totalCellHeight = rowSizes[row].actualSize;
}
final Dimension size = cell.doCellLayout(totalCellWidth, totalCellHeight, true, true, sizeOnly);
if (size.width > totalCellWidth) {
if (colSpan == 1) {
colSizes[col].actualSize = size.width;
} else {
colSizes[col].actualSize += (size.width - totalCellWidth);
}
}
if (size.height > totalCellHeight) {
if (rowSpan == 1) {
rowSizes[row].actualSize = size.height;
} else {
rowSizes[row].actualSize += (size.height - totalCellHeight);
}
}
}
}
// public final void adjust() {
// // finalRender needs to adjust actualSize of columns and rows
// // given that things might change as we render one last time.
// int hasBorder = this.hasOldStyleBorder;
// int cellSpacingX = this.cellSpacingX;
// int cellSpacingY = this.cellSpacingY;
// ArrayList allCells = this.ALL_CELLS;
// SizeInfo[] colSizes = this.columnSizes;
// SizeInfo[] rowSizes = this.rowSizes;
// int numCells = allCells.size();
// for(int i = 0; i < numCells; i++) {
// RTableCell cell = (RTableCell) allCells.get(i);
// int col = cell.getVirtualColumn();
// int colSpan = cell.getColSpan();
// int totalCellWidth;
// if(colSpan > 1) {
// totalCellWidth = (colSpan - 1) * (cellSpacingX + 2 * hasBorder);
// for(int x = 0; x < colSpan; x++) {
// totalCellWidth += colSizes[col + x].actualSize;
// }
// }
// else {
// totalCellWidth = colSizes[col].actualSize;
// }
// int row = cell.getVirtualRow();
// int rowSpan = cell.getRowSpan();
// int totalCellHeight;
// if(rowSpan > 1) {
// totalCellHeight = (rowSpan - 1) * (cellSpacingY + 2 * hasBorder);
// for(int y = 0; y < rowSpan; y++) {
// totalCellHeight += rowSizes[row + y].actualSize;
// }
// }
// else {
// totalCellHeight = rowSizes[row].actualSize;
// }
// cell.adjust();
// Dimension size = cell.getSize();
// if(size.width > totalCellWidth) {
// if(colSpan == 1) {
// colSizes[col].actualSize = size.width;
// }
// else {
// colSizes[col].actualSize += (size.width - totalCellWidth);
// }
// }
// if(size.height > totalCellHeight) {
// if(rowSpan == 1) {
// rowSizes[row].actualSize = size.height;
// }
// else {
// rowSizes[row].actualSize += (size.height - totalCellHeight);
// }
// }
// }
// }
/**
* Sets bounds of each cell's component, and sums up table width and height.
*/
public final void doLayout(final Insets insets) {
// Set row offsets
final RowSizeInfo[] rowSizes = this.rowSizes;
final int numRows = rowSizes.length;
int yoffset = insets.top;
final int cellSpacingY = this.cellSpacingY;
final int hasBorder = this.hasOldStyleBorder;
for (int i = 0; i < numRows; i++) {
yoffset += cellSpacingY;
yoffset += hasBorder;
final RowSizeInfo rowSizeInfo = rowSizes[i];
yoffset += rowSizeInfo.marginTop;
rowSizeInfo.offsetY = yoffset;
rowSizeInfo.insetLeft = insets.left;
rowSizeInfo.insetRight = insets.right;
yoffset += rowSizeInfo.actualSize;
yoffset += hasBorder;
yoffset += rowSizeInfo.marginBottom;
}
this.tableHeight = yoffset + cellSpacingY + insets.bottom;
// Set column offsets
final ColSizeInfo[] colSizes = this.columnSizes;
final int numColumns = colSizes.length;
int xoffset = insets.left;
final int cellSpacingX = this.cellSpacingX;
for (int i = 0; i < numColumns; i++) {
xoffset += cellSpacingX;
xoffset += hasBorder;
final ColSizeInfo colSizeInfo = colSizes[i];
colSizeInfo.offsetX = xoffset;
xoffset += colSizeInfo.actualSize;
xoffset += hasBorder;
}
this.tableWidth = xoffset + cellSpacingX + insets.right + (maxRowGroupRight / 2);
// Set offsets of each cell
for (@NonNull RAbstractCell cell : this.ALL_CELLS) {
cell.setCellBounds(colSizes, rowSizes, hasBorder, cellSpacingX, cellSpacingY);
}
this.rowGroupSizes = prepareRowGroupSizes();
}
static class RTableRowGroup extends BaseElementRenderable {
public RTableRowGroup(RenderableContainer container, ModelNode modelNode, UserAgentContext ucontext, final BorderOverrider borderOverrider) {
super(container, modelNode, ucontext);
this.borderOverrider.copyFrom(borderOverrider);
}
@Override
public Iterator<@NonNull ? extends Renderable> getRenderables(boolean topFirst) {
return null;
}
@Override
public RenderableSpot getLowestRenderableSpot(int x, int y) {
return null;
}
@Override
public boolean onMouseReleased(MouseEvent event, int x, int y) {
return false;
}
@Override
public boolean onMouseDisarmed(MouseEvent event) {
return false;
}
@Override
public boolean onDoubleClick(MouseEvent event, int x, int y) {
return false;
}
@Override
public void repaint() {
container.repaint(x, y, width, height);
}
@Override
public void repaint(ModelNode modelNode) {
// TODO Auto-generated method stub
}
@Override
public Color getPaintedBackgroundColor() {
// TODO Auto-generated method stub
return null;
}
@Override
protected void paintShifted(Graphics g) {
// TODO Auto-generated method stub
}
@Override
protected void doLayout(int availWidth, int availHeight, boolean sizeOnly) {
// TODO Auto-generated method stub
}
@Override
public @NonNull Insets getBorderInsets() {
return borderOverrider.get(super.getBorderInsets());
}
}
public final void paint(final Graphics g, final Dimension size) {
// Paint row group backgrounds
for (final RowGroupSizeInfo rgsi : rowGroupSizes) {
rgsi.prePaintBackground(g);
}
for (final @NonNull RAbstractCell cell : this.ALL_CELLS) {
// Should clip table cells, just in case.
final Graphics newG = g.create(cell.x, cell.y, cell.width, cell.height);
try {
cell.paint(newG);
} finally {
newG.dispose();
}
}
if (this.hasOldStyleBorder > 0) {
// // Paint table border
//
// int tableWidth = this.tableWidth;
// int tableHeight = this.tableHeight;
// g.setColor(Color.BLACK); //TODO: Actual border color
// int x = insets.left;
// int y = insets.top;
// for(int i = 0; i < border; i++) {
// g.drawRect(x + i, y + i, tableWidth - i * 2 - 1, tableHeight - i * 2 -
// 1);
// }
// Paint cell borders
g.setColor(Color.GRAY);
for (@NonNull RAbstractCell cell : this.ALL_CELLS) {
final int cx = cell.getX() - 1;
final int cy = cell.getY() - 1;
final int cwidth = cell.getWidth() + 1;
final int cheight = cell.getHeight() + 1;
g.drawRect(cx, cy, cwidth, cheight);
}
}
// Paint row group borders
for (final RowGroupSizeInfo rgsi : rowGroupSizes) {
rgsi.prePaintBorder(g);
}
}
// Called during paint
private ArrayList<RowGroupSizeInfo> prepareRowGroupSizes() {
final ArrayList<RowGroupSizeInfo> rowGroupSizes = new ArrayList<>();
{
final RowSizeInfo[] rowSizesLocal = this.rowSizes;
for (final RowGroup rowGroup : this.ROW_GROUPS) {
if (rowGroup.rowGroupElem != null) {
final Row firstRow = rowGroup.rows.get(0);
final Row lastRow = rowGroup.rows.get(rowGroup.rows.size() - 1);
final RowSizeInfo firstRowSize = rowSizesLocal[firstRow.rowIndex];
final RowSizeInfo lastRowSize = rowSizesLocal[lastRow.rowIndex];
final int groupHeight = lastRowSize.actualSize + lastRowSize.offsetY - (firstRowSize.offsetY);
final int groupWidth = this.tableWidth - (firstRowSize.insetRight + firstRowSize.insetLeft);
final RTableRowGroup rRowGroup = new RTableRowGroup(this.container, firstRow.rowGroupElem, this.uaContext,
rowGroup.borderOverrider);
final int x = firstRowSize.offsetX + firstRowSize.insetLeft;
final int y = firstRowSize.offsetY;
rRowGroup.setX(x);
rRowGroup.setY(y);
rRowGroup.setWidth(groupWidth);
rRowGroup.setHeight(groupHeight);
rRowGroup.applyStyle(groupWidth, groupHeight, true);
final RowGroupSizeInfo rgsi = new RowGroupSizeInfo(groupWidth, groupHeight, rRowGroup, x, y);
rowGroupSizes.add(rgsi);
}
}
}
return rowGroupSizes;
}
// public boolean paintSelection(Graphics g, boolean inSelection,
// RenderableSpot startPoint, RenderableSpot endPoint) {
// ArrayList allCells = this.ALL_CELLS;
// int numCells = allCells.size();
// for(int i = 0; i < numCells; i++) {
// RTableCell cell = (RTableCell) allCells.get(i);
// Rectangle bounds = cell.getBounds();
// int offsetX = bounds.x;
// int offsetY = bounds.y;
// g.translate(offsetX, offsetY);
// try {
// boolean newInSelection = cell.paintSelection(g, inSelection, startPoint,
// endPoint);
// if(inSelection && !newInSelection) {
// return false;
// }
// inSelection = newInSelection;
// } finally {
// g.translate(-offsetX, -offsetY);
// }
// }
// return inSelection;
// }
//
// public boolean extractSelectionText(StringBuffer buffer, boolean
// inSelection, RenderableSpot startPoint, RenderableSpot endPoint) {
// ArrayList allCells = this.ALL_CELLS;
// int numCells = allCells.size();
// for(int i = 0; i < numCells; i++) {
// RTableCell cell = (RTableCell) allCells.get(i);
// boolean newInSelection = cell.extractSelectionText(buffer, inSelection,
// startPoint, endPoint);
// if(inSelection && !newInSelection) {
// return false;
// }
// inSelection = newInSelection;
// }
// return inSelection;
// }
/*
* (non-Javadoc)
*
* @see org.xamjwg.html.renderer.BoundableRenderable#getRenderablePoint(int,
* int)
*/
public RenderableSpot getLowestRenderableSpot(final int x, final int y) {
for (@NonNull RAbstractCell cell : this.ALL_CELLS) {
final Rectangle bounds = cell.getVisualBounds();
if (bounds.contains(x, y)) {
final RenderableSpot rp = cell.getLowestRenderableSpot(x - bounds.x, y - bounds.y);
if (rp != null) {
return rp;
}
}
}
return null;
}
/*
* (non-Javadoc)
*
* @see
* org.xamjwg.html.renderer.BoundableRenderable#onMouseClick(java.awt.event
* .MouseEvent, int, int)
*/
public boolean onMouseClick(final MouseEvent event, final int x, final int y) {
for (@NonNull RAbstractCell cell : this.ALL_CELLS) {
final Rectangle bounds = cell.getVisualBounds();
if (bounds.contains(x, y)) {
if (!cell.onMouseClick(event, x - bounds.x, y - bounds.y)) {
return false;
}
break;
}
}
return true;
}
public boolean onDoubleClick(final MouseEvent event, final int x, final int y) {
for (@NonNull RAbstractCell cell : this.ALL_CELLS) {
final Rectangle bounds = cell.getVisualBounds();
if (bounds.contains(x, y)) {
if (!cell.onDoubleClick(event, x - bounds.x, y - bounds.y)) {
return false;
}
break;
}
}
return true;
}
private BoundableRenderable armedRenderable;
private int maxRowGroupLeft;
private int maxRowGroupRight;
/*
* (non-Javadoc)
*
* @see
* org.xamjwg.html.renderer.BoundableRenderable#onMouseDisarmed(java.awt.event
* .MouseEvent)
*/
public boolean onMouseDisarmed(final MouseEvent event) {
final BoundableRenderable ar = this.armedRenderable;
if (ar != null) {
this.armedRenderable = null;
return ar.onMouseDisarmed(event);
} else {
return true;
}
}
/*
* (non-Javadoc)
*
* @see
* org.xamjwg.html.renderer.BoundableRenderable#onMousePressed(java.awt.event
* .MouseEvent, int, int)
*/
public boolean onMousePressed(final MouseEvent event, final int x, final int y) {
final ArrayList<RAbstractCell> allCells = this.ALL_CELLS;
final int numCells = allCells.size();
for (int i = 0; i < numCells; i++) {
final RAbstractCell cell = allCells.get(i);
final Rectangle bounds = cell.getVisualBounds();
if (bounds.contains(x, y)) {
if (!cell.onMousePressed(event, x - bounds.x, y - bounds.y)) {
this.armedRenderable = cell;
return false;
}
break;
}
}
return true;
}
/*
* (non-Javadoc)
*
* @see
* org.xamjwg.html.renderer.BoundableRenderable#onMouseReleased(java.awt.event
* .MouseEvent, int, int)
*/
public boolean onMouseReleased(final MouseEvent event, final int x, final int y) {
final ArrayList<RAbstractCell> allCells = this.ALL_CELLS;
final int numCells = allCells.size();
boolean found = false;
for (int i = 0; i < numCells; i++) {
final RAbstractCell cell = allCells.get(i);
final Rectangle bounds = cell.getVisualBounds();
if (bounds.contains(x, y)) {
found = true;
final BoundableRenderable oldArmedRenderable = this.armedRenderable;
if ((oldArmedRenderable != null) && (cell != oldArmedRenderable)) {
oldArmedRenderable.onMouseDisarmed(event);
this.armedRenderable = null;
}
if (!cell.onMouseReleased(event, x - bounds.x, y - bounds.y)) {
return false;
}
break;
}
}
if (!found) {
final BoundableRenderable oldArmedRenderable = this.armedRenderable;
if (oldArmedRenderable != null) {
oldArmedRenderable.onMouseDisarmed(event);
this.armedRenderable = null;
}
}
return true;
}
Iterator<@NonNull RAbstractCell> getCells() {
return this.ALL_CELLS.iterator();
}
static final class ColSizeInfo {
HtmlLength htmlLength;
int actualSize;
int fullActualSize; // Full size including border and padding
int layoutSize;
int fullLayoutSize; // Full size including border and padding
int minSize;
int offsetX;
}
static final class RowSizeInfo {
int insetLeft;
int insetRight;
HtmlLength htmlLength;
int actualSize;
int minSize;
int offsetX;
int offsetY;
int marginTop;
int marginBottom;
}
private static final class RowGroupSizeInfo {
private final int height;
private final int width;
private final int x;
private final int y;
private final @NonNull RTableRowGroup r;
RowGroupSizeInfo(final int width, final int height, final @NonNull RTableRowGroup r, final int x, final int y) {
this.height = height;
this.width = width;
this.r = r;
this.x = x;
this.y = y;
}
void prePaintBackground(final Graphics g) {
final Insets bi = r.getBorderInsets();
final ModelNode rowGroupElem = r.getModelNode();
r.prePaintBackground(g, width - (bi.left/2), height, x, y, rowGroupElem, rowGroupElem.getRenderState(), bi);
}
void prePaintBorder(final Graphics g) {
final Insets bi = r.getBorderInsets();
r.prePaintBorder(g, width + (bi.left)/2 + bi.right, height + bi.top + bi.bottom, x - bi.left, y - bi.top , bi);
}
}
public Iterator<@NonNull RTableRowGroup> getRowGroups() {
return this.rowGroupSizes.stream().map(rgs -> rgs.r).iterator();
}
}