org.cobraparser.html.renderer.TableMatrix Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of Cobra Show documentation
Show all versions of Cobra Show documentation
Cobra is the rendering engine designed for LoboBrowser
/*
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: [email protected]
*/
package org.cobraparser.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.cobraparser.html.HtmlRendererContext;
import org.cobraparser.html.domimpl.AnonymousNodeImpl;
import org.cobraparser.html.domimpl.HTMLElementImpl;
import org.cobraparser.html.domimpl.ModelNode;
import org.cobraparser.html.domimpl.NodeImpl;
import org.cobraparser.html.domimpl.TextImpl;
import org.cobraparser.html.style.BorderInfo;
import org.cobraparser.html.style.HtmlInsets;
import org.cobraparser.html.style.HtmlLength;
import org.cobraparser.html.style.HtmlValues;
import org.cobraparser.html.style.JStyleProperties;
import org.cobraparser.html.style.RenderState;
import org.cobraparser.html.style.RenderThreadState;
import org.cobraparser.ua.UserAgentContext;
final class TableMatrix {
private final ArrayList ROWS = new ArrayList<>();
private final ArrayList 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 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 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 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 elementToRow = new HashMap<>(2);
private Row currentFallbackRow = null;
private final ArrayList listOfRows;
private final ArrayList listOfRowGroups;
public TableRelation(final ArrayList listOfRows, final ArrayList 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 populateRows() {
final HTMLElementImpl te = this.tableElement;
final ArrayList 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 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 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:
*
* - 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.
*
*
- 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.
*
*
- 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.
*
*
- Finally, adjust widths considering the expected max table size. Columns
* are layed out again if necessary to determine if they can really be shrunk.
*
*
* @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 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.
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 prepareRowGroupSizes() {
final ArrayList 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 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 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();
}
}