com.itextpdf.layout.element.Table Maven / Gradle / Ivy
/*
This file is part of the iText (R) project.
Copyright (c) 1998-2024 Apryse Group NV
Authors: Apryse Software.
This program is offered under a commercial and under the AGPL license.
For commercial licensing, contact us at https://itextpdf.com/sales. For AGPL licensing, see below.
AGPL licensing:
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see .
*/
package com.itextpdf.layout.element;
import com.itextpdf.kernel.exceptions.PdfException;
import com.itextpdf.kernel.pdf.tagging.StandardRoles;
import com.itextpdf.kernel.pdf.tagutils.AccessibilityProperties;
import com.itextpdf.kernel.pdf.tagutils.DefaultAccessibilityProperties;
import com.itextpdf.layout.Document;
import com.itextpdf.layout.borders.Border;
import com.itextpdf.layout.exceptions.LayoutExceptionMessageConstant;
import com.itextpdf.layout.properties.BorderCollapsePropertyValue;
import com.itextpdf.layout.properties.CaptionSide;
import com.itextpdf.layout.properties.Property;
import com.itextpdf.layout.properties.UnitValue;
import com.itextpdf.layout.renderer.IRenderer;
import com.itextpdf.layout.renderer.TableRenderer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
/**
* A {@link Table} is a layout element that represents data in a two-dimensional
* grid. It is filled with {@link Cell cells}, ordered in rows and columns.
*
* It is an implementation of {@link ILargeElement}, which means it can be flushed
* to the canvas, in order to reclaim memory that is locked up.
*/
public class Table extends BlockElement
implements ILargeElement {
protected DefaultAccessibilityProperties tagProperties;
private List rows;
private UnitValue[] columnWidths;
private int currentColumn = 0;
private int currentRow = -1;
private Table header;
private Table footer;
private boolean skipFirstHeader;
private boolean skipLastFooter;
private boolean isComplete;
private List lastAddedRowGroups;
// Start number of the row "window" (range) that this table currently contain.
// For large tables we might contain only a few rows, not all of them, other ones might have been flushed.
private int rowWindowStart = 0;
private Document document;
private Cell[] lastAddedRow;
private Div caption;
/**
* Constructs a {@code Table} with the preferable column widths.
*
* Since 7.0.2 table layout algorithms were introduced. Auto layout is default, except large tables.
* For large table 100% width and fixed layout set implicitly.
*
* Note, the eventual columns width depends on selected layout, table width,
* cell's width, cell's min-widths, and cell's max-widths.
* Table layout algorithm has the same behaviour as expected for CSS table-layout property,
* where {@code columnWidths} is <colgroup>'s widths.
* For more information see {@link #setAutoLayout()} and {@link #setFixedLayout()}.
*
* @param columnWidths preferable column widths in points. Values must be greater than or equal to zero,
* otherwise it will be interpreted as undefined.
* @param largeTable whether parts of the table will be written before all data is added.
* Note, large table does not support auto layout, table width shall not be removed.
* @see #setAutoLayout()
* @see #setFixedLayout()
*/
public Table(float[] columnWidths, boolean largeTable) {
if (columnWidths == null) {
throw new IllegalArgumentException("The widths array in table constructor can not be null.");
}
if (columnWidths.length == 0) {
throw new IllegalArgumentException("The widths array in table constructor can not have zero length.");
}
this.columnWidths = normalizeColumnWidths(columnWidths);
initializeLargeTable(largeTable);
initializeRows();
}
/**
* Constructs a {@code Table} with the preferable column widths.
*
* Since 7.0.2 table layout algorithms were introduced. Auto layout is default, except large tables.
* For large table 100% width and fixed layout set implicitly.
*
* Note, the eventual columns width depends on selected layout, table width,
* cell's width, cell's min-widths, and cell's max-widths.
* Table layout algorithm has the same behaviour as expected for CSS table-layout property,
* where {@code columnWidths} is <colgroup>'s widths.
* For more information see {@link #setAutoLayout()} and {@link #setFixedLayout()}.
*
* @param columnWidths preferable column widths, points and/or percents. Values must be greater than or equal to zero,
* otherwise it will be interpreted as undefined.
* @param largeTable whether parts of the table will be written before all data is added.
* Note, large table does not support auto layout, table width shall not be removed.
* @see #setAutoLayout()
* @see #setFixedLayout()
*/
public Table(UnitValue[] columnWidths, boolean largeTable) {
if (columnWidths == null) {
throw new IllegalArgumentException("The widths array in table constructor can not be null.");
}
if (columnWidths.length == 0) {
throw new IllegalArgumentException("The widths array in table constructor can not have zero length.");
}
this.columnWidths = normalizeColumnWidths(columnWidths);
initializeLargeTable(largeTable);
initializeRows();
}
/**
* Constructs a {@code Table} with the preferable column widths.
*
* Since 7.0.2 table layout algorithms were introduced. Auto layout is default.
*
* Note, the eventual columns width depends on selected layout, table width,
* cell's width, cell's min-widths, and cell's max-widths.
* Table layout algorithm has the same behaviour as expected for CSS table-layout property,
* where {@code columnWidths} is <colgroup>'s widths.
* For more information see {@link #setAutoLayout()} and {@link #setFixedLayout()}.
*
* @param columnWidths preferable column widths, points and/or percents. Values must be greater than or equal to zero,
* otherwise it will be interpreted as undefined.
* @see #setAutoLayout()
* @see #setFixedLayout()
*/
public Table(UnitValue[] columnWidths) {
this(columnWidths, false);
}
/**
* Constructs a {@code Table} with the preferable column widths.
*
* Since 7.0.2 table layout algorithms were introduced. Auto layout is default.
*
* Note, the eventual columns width depends on selected layout, table width,
* cell's width, cell's min-widths, and cell's max-widths.
* Table layout algorithm has the same behaviour as expected for CSS table-layout property,
* where {@code columnWidths} is <colgroup>'s widths.
* For more information see {@link #setAutoLayout()} and {@link #setFixedLayout()}.
*
* @param pointColumnWidths preferable column widths in points. Values must be greater than or equal to zero,
* otherwise it will be interpreted as undefined.
* @see #setAutoLayout()
* @see #setFixedLayout()
*/
public Table(float[] pointColumnWidths) {
this(pointColumnWidths, false);
}
/**
* Constructs a {@code Table} with specified number of columns.
* The final column widths depend on selected table layout.
*
* Since 7.0.2 table layout algorithms were introduced. Auto layout is default, except large tables.
* For large table fixed layout set implicitly.
*
* Since 7.1 table will have undefined column widths, that will be determined during layout.
* In oder to set equal percent width as column width, use {@link UnitValue#createPercentArray(int)}
*
* Note, the eventual columns width depends on selected layout, table width,
* cell's width, cell's min-widths, and cell's max-widths.
* Table layout algorithm has the same behaviour as expected for CSS table-layout property,
* where {@code columnWidths} is <colgroup>'s widths.
* For more information see {@link #setAutoLayout()} and {@link #setFixedLayout()}.
*
* @param numColumns the number of columns, each column will have equal percent width.
* @param largeTable whether parts of the table will be written before all data is added.
* Note, large table does not support auto layout, table width shall not be removed.
* @see #setAutoLayout()
* @see #setFixedLayout()
*/
public Table(int numColumns, boolean largeTable) {
if (numColumns <= 0) {
throw new IllegalArgumentException("The number of columns in Table constructor must be greater than zero");
}
this.columnWidths = normalizeColumnWidths(numColumns);
initializeLargeTable(largeTable);
initializeRows();
}
/**
* Constructs a {@code Table} with specified number of columns.
* The final column widths depend on selected table layout.
*
* Since 7.0.2 table layout was introduced. Auto layout is default, except large tables.
* For large table fixed layout set implicitly.
*
* Since 7.1 table will have undefined column widths, that will be determined during layout.
* In oder to set equal percent width as column width, use {@link UnitValue#createPercentArray(int)}
*
* Note, the eventual columns width depends on selected layout, table width,
* cell's width, cell's min-widths, and cell's max-widths.
* Table layout algorithm has the same behaviour as expected for CSS table-layout property,
* where {@code columnWidths} is <colgroup>'s widths.
* For more information see {@link #setAutoLayout()} and {@link #setFixedLayout()}.
*
* @param numColumns the number of columns, each column will have equal percent width.
* @see #setAutoLayout()
* @see #setFixedLayout()
*/
public Table(int numColumns) {
this(numColumns, false);
}
/**
* Set fixed layout. Analog of {@code table-layout:fixed} CSS property.
* Note, the table must have width property, otherwise auto layout will be used.
*
* Algorithm description
*
* 1. Scan columns for width property and set it. All the rest columns get undefined value.
* Column width includes borders and paddings. Columns have set in constructor, analog of {@code } element in HTML.
*
* 2. Scan the very first row of table for width property and set it to undefined columns.
* Cell width has lower priority in comparing with column. Cell width doesn't include borders and paddings.
*
* 2.1 If cell has colspan and all columns are undefined, each column will get equal width: {@code width/colspan}.
*
* 2.2 If some columns already have width, equal remain (original width minus existed) width will be added {@code remainWidth/colspan} to each column.
*
* 3. If sum of columns is less, than table width, there are two options:
*
* 3.1. If undefined columns still exist, they will get the rest remaining width.
*
* 3.2. Otherwise all columns will be expanded proportionally based on its width.
*
* 4. If sum of columns is greater, than table width, nothing to do.
*
* @return this element.
*/
public Table setFixedLayout() {
setProperty(Property.TABLE_LAYOUT, "fixed");
return this;
}
/**
* Set auto layout. Analog of {@code table-layout:auto} CSS property.
* Note, large table does not support auto layout.
*
* Algorithm principles.
*
* 1. Column width cannot be less, than min-width of any cell in the column (calculated by layout).
*
* 2. Specified table width has higher priority, than sum of column and cell widths.
*
* 3. Percent value of cell and column width has higher priority, than point value.
*
* 4. Cell width has higher priority, than column width.
*
* 5. If column has no width, it will try to reach max-value (calculated by layout).
*
* @return this element.
*/
public Table setAutoLayout() {
setProperty(Property.TABLE_LAYOUT, "auto");
return this;
}
/**
* Set {@link Property#WIDTH} = 100%.
*
* @return this element
*/
public Table useAllAvailableWidth() {
setProperty(Property.WIDTH, UnitValue.createPercentValue(100));
return this;
}
/**
* Returns the column width for the specified column.
*
* @param column index of the column
* @return the width of the column
*/
public UnitValue getColumnWidth(int column) {
return columnWidths[column];
}
/**
* Returns the number of columns.
*
* @return the number of columns.
*/
public int getNumberOfColumns() {
return columnWidths.length;
}
/**
* Returns the number of rows.
*
* @return the number of rows.
*/
public int getNumberOfRows() {
return rows.size();
}
/**
* Adds a new cell to the header of the table.
* The header will be displayed in the top of every area of this table.
* See also {@link #setSkipFirstHeader(boolean)}.
*
* @param headerCell a header cell to be added
* @return this element
*/
public Table addHeaderCell(Cell headerCell) {
ensureHeaderIsInitialized();
header.addCell(headerCell);
return this;
}
/**
* Adds a new cell with received blockElement as a content to the header of the table.
* The header will be displayed in the top of every area of this table.
* See also {@link #setSkipFirstHeader(boolean)}.
*
* @param blockElement an element to be added to a header cell
* @param any IElement
* @return this element
*/
public Table addHeaderCell(BlockElement blockElement) {
ensureHeaderIsInitialized();
header.addCell(blockElement);
return this;
}
/**
* Adds a new cell with received image to the header of the table.
* The header will be displayed in the top of every area of this table.
* See also {@link #setSkipFirstHeader(boolean)}.
*
* @param image an element to be added to a header cell
* @return this element
*/
public Table addHeaderCell(Image image) {
ensureHeaderIsInitialized();
header.addCell(image);
return this;
}
/**
* Adds a new cell with received string as a content to the header of the table.
* The header will be displayed in the top of every area of this table.
* See also {@link #setSkipFirstHeader(boolean)}.
*
* @param content a string to be added to a header cell
* @return this element
*/
public Table addHeaderCell(String content) {
ensureHeaderIsInitialized();
header.addCell(content);
return this;
}
/**
* Gets the header of the table. The header is represented as a distinct table and might have its own properties.
*
* @return table header or {@code null}, if {@link #addHeaderCell(Cell)} hasn't been called.
*/
public Table getHeader() {
return header;
}
/**
* Adds a new cell to the footer of the table.
* The footer will be displayed in the bottom of every area of this table.
* See also {@link #setSkipLastFooter(boolean)}.
*
* @param footerCell a footer cell
* @return this element
*/
public Table addFooterCell(Cell footerCell) {
ensureFooterIsInitialized();
footer.addCell(footerCell);
return this;
}
/**
* Adds a new cell with received blockElement as a content to the footer of the table.
* The footer will be displayed in the bottom of every area of this table.
* See also {@link #setSkipLastFooter(boolean)}.
*
* @param blockElement an element to be added to a footer cell
* @param IElement instance
* @return this element
*/
public Table addFooterCell(BlockElement blockElement) {
ensureFooterIsInitialized();
footer.addCell(blockElement);
return this;
}
/**
* Adds a new cell with received image as a content to the footer of the table.
* The footer will be displayed in the bottom of every area of this table.
* See also {@link #setSkipLastFooter(boolean)}.
*
* @param image an image to be added to a footer cell
* @return this element
*/
public Table addFooterCell(Image image) {
ensureFooterIsInitialized();
footer.addCell(image);
return this;
}
/**
* Adds a new cell with received string as a content to the footer of the table.
* The footer will be displayed in the bottom of every area of this table.
* See also {@link #setSkipLastFooter(boolean)}.
*
* @param content a content string to be added to a footer cell
* @return this element
*/
public Table addFooterCell(String content) {
ensureFooterIsInitialized();
footer.addCell(content);
return this;
}
/**
* Gets the footer of the table. The footer is represented as a distinct table and might have its own properties.
*
* @return table footer or {@code null}, if {@link #addFooterCell(Cell)} hasn't been called.
*/
public Table getFooter() {
return footer;
}
/**
* Tells you if the first header needs to be skipped (for instance if the
* header says "continued from the previous page").
*
* @return Value of property skipFirstHeader.
*/
public boolean isSkipFirstHeader() {
return skipFirstHeader;
}
/**
* Skips the printing of the first header. Used when printing tables in
* succession belonging to the same printed table aspect.
*
* @param skipFirstHeader New value of property skipFirstHeader.
* @return this element
*/
public Table setSkipFirstHeader(boolean skipFirstHeader) {
this.skipFirstHeader = skipFirstHeader;
return this;
}
/**
* Tells you if the last footer needs to be skipped (for instance if the
* footer says "continued on the next page")
*
* @return Value of property skipLastFooter.
*/
public boolean isSkipLastFooter() {
return skipLastFooter;
}
/**
* Skips the printing of the last footer. Used when printing tables in
* succession belonging to the same printed table aspect.
*
* @param skipLastFooter New value of property skipLastFooter.
* @return this element
*/
public Table setSkipLastFooter(boolean skipLastFooter) {
this.skipLastFooter = skipLastFooter;
return this;
}
/** Sets the table's caption.
*
* If there is no {@link Property#CAPTION_SIDE} set (note that it's an inheritable property),
* {@link CaptionSide#TOP} will be used.
* Also the {@link StandardRoles#CAPTION} will be set on the element.
*
* @param caption The element to be set as a caption.
* @return this element
*/
public Table setCaption(Div caption) {
this.caption = caption;
if (null != caption) {
ensureCaptionPropertiesAreSet();
}
return this;
}
/**
* Sets the table's caption and its caption side.
*
* Also the {@link StandardRoles#CAPTION} will be set on the element.
*
* @param caption The element to be set as a caption.
* @param side The caption side to be set on the caption.
** @return this element
*/
public Table setCaption(Div caption, CaptionSide side) {
if (null != caption) {
caption.setProperty(Property.CAPTION_SIDE, side);
}
setCaption(caption);
return this;
}
private void ensureCaptionPropertiesAreSet() {
this.caption.getAccessibilityProperties().setRole(StandardRoles.CAPTION);
}
/**
* Gets the table's caption.
*
* @return the table's caption.
*/
public Div getCaption() {
return caption;
}
/**
* Starts new row. This mean that next cell will be added at the beginning of next line.
*
* @return this element
*/
public Table startNewRow() {
currentColumn = 0;
currentRow++;
if (currentRow >= rows.size()) {
rows.add(new Cell[columnWidths.length]);
}
return this;
}
/**
* Adds a new cell to the table. The implementation decides for itself which
* row the cell will be placed on.
*
* @param cell {@code Cell} to add.
* @return this element
*/
public Table addCell(Cell cell) {
if (isComplete && null != lastAddedRow) {
throw new PdfException(LayoutExceptionMessageConstant.CANNOT_ADD_CELL_TO_COMPLETED_LARGE_TABLE);
}
// Try to find first empty slot in table.
// We shall not use colspan or rowspan, 1x1 will be enough.
while (true) {
if (currentColumn >= columnWidths.length || currentColumn == -1) {
startNewRow();
}
if (rows.get(currentRow - rowWindowStart)[currentColumn] != null) {
currentColumn++;
} else {
break;
}
}
childElements.add(cell);
cell.updateCellIndexes(currentRow, currentColumn, columnWidths.length);
// extend bottom grid of slots to fix rowspan
while (currentRow - rowWindowStart + cell.getRowspan() > rows.size()) {
rows.add(new Cell[columnWidths.length]);
}
// set cell to all region include colspan and rowspan, except not-null cells.
// this will help to handle empty rows and columns.
for (int i = currentRow; i < currentRow + cell.getRowspan(); i++) {
Cell[] row = rows.get(i - rowWindowStart);
for (int j = currentColumn; j < currentColumn + cell.getColspan(); j++) {
if (row[j] == null) {
row[j] = cell;
}
}
}
currentColumn += cell.getColspan();
return this;
}
/**
* Adds a new cell with received blockElement as a content.
*
* @param blockElement a blockElement to add to the cell and then to the table
* @param IElement instance
* @return this element
*/
public Table addCell(BlockElement blockElement) {
return addCell(new Cell().add(blockElement));
}
/**
* Adds a new cell with received image as a content.
*
* @param image an image to add to the cell and then to the table
* @return this element
*/
public Table addCell(Image image) {
return addCell(new Cell().add(image));
}
/**
* Adds a new cell with received string as a content.
*
* @param content a string to add to the cell and then to the table
* @return this element
*/
public Table addCell(String content) {
return addCell(new Cell().add(new Paragraph(content)));
}
/**
* Returns a cell as specified by its location. If the cell is in a col-span
* or row-span and is not the top left cell, then null is returned.
*
* @param row the row of the cell. indexes are zero-based
* @param column the column of the cell. indexes are zero-based
* @return the cell at the specified position.
*/
public Cell getCell(int row, int column) {
if (row - rowWindowStart < rows.size()) {
Cell cell = rows.get(row - rowWindowStart)[column];
// make sure that it is top left corner of cell, even in case colspan or rowspan
if (cell != null && cell.getRow() == row && cell.getCol() == column) {
return cell;
}
}
return null;
}
/**
* Creates a renderer subtree with root in the current table element.
* Compared to {@link #getRenderer()}, the renderer returned by this method should contain all the child
* renderers for children of the current element.
*
* @return a {@link TableRenderer} subtree for this element
*/
@Override
public IRenderer createRendererSubTree() {
TableRenderer rendererRoot = (TableRenderer) getRenderer();
for (IElement child : childElements) {
boolean childShouldBeAdded = isComplete || cellBelongsToAnyRowGroup((Cell) child, lastAddedRowGroups);
if (childShouldBeAdded) {
rendererRoot.addChild(child.createRendererSubTree());
}
}
return rendererRoot;
}
/**
* Gets a table renderer for this element. Note that this method can be called more than once.
* By default each element should define its own renderer, but the renderer can be overridden by
* {@link #setNextRenderer(IRenderer)} method call.
*
* @return a table renderer for this element
*/
@Override
public IRenderer getRenderer() {
if (nextRenderer != null) {
if (nextRenderer instanceof TableRenderer) {
IRenderer renderer = nextRenderer;
nextRenderer = nextRenderer.getNextRenderer();
return renderer;
} else {
Logger logger = LoggerFactory.getLogger(Table.class);
logger.error("Invalid renderer for Table: must be inherited from TableRenderer");
}
}
// In case of large tables, we only add to the renderer the cells from complete row groups,
// for incomplete ones we may have problem with partial rendering because of cross-dependency.
if (isComplete) {
// if table was large we need to remove the last flushed group of rows, so we need to update lastAddedRowGroups
if (null != lastAddedRow && 0 != rows.size()) {
List allRows = new ArrayList<>();
allRows.add(new RowRange(rowWindowStart, rowWindowStart + rows.size() - 1));
lastAddedRowGroups = allRows;
}
} else {
lastAddedRowGroups = getRowGroups();
}
if (isComplete) {
return new TableRenderer(this, new RowRange(rowWindowStart, rowWindowStart + rows.size() - 1));
} else {
int rowWindowFinish = lastAddedRowGroups.size() != 0 ? lastAddedRowGroups.get(lastAddedRowGroups.size() - 1).finishRow : -1;
return new TableRenderer(this, new RowRange(rowWindowStart, rowWindowFinish));
}
}
@Override
public boolean isComplete() {
return isComplete;
}
/**
* Indicates that all the desired content has been added to this large element and no more content will be added.
* After this method is called, more precise rendering is activated.
* For instance, a table may have a {@link #setSkipLastFooter(boolean)} method set to true,
* and in case of large table on {@link #flush()} we do not know if any more content will be added,
* so we might not place the content in the bottom of the page where it would fit, but instead add a footer, and
* place that content in the start of the page. Technically such result would look all right, but it would be
* more concise if we placed the content in the bottom and did not start new page. For such cases to be
* renderered more accurately, one can call complete() when some content is still there and not flushed.
*/
@Override
public void complete() {
assert !isComplete;
isComplete = true;
flush();
}
/**
* Writes the newly added content to the document.
*/
@Override
public void flush() {
Cell[] row = null;
int rowNum = rows.size();
if (!rows.isEmpty()) {
row = rows.get(rows.size() - 1);
}
document.add(this);
if (row != null && rowNum != rows.size()) {
lastAddedRow = row;
}
}
@Override
public void flushContent() {
if (lastAddedRowGroups == null || lastAddedRowGroups.isEmpty())
return;
int firstRow = lastAddedRowGroups.get(0).startRow;
int lastRow = lastAddedRowGroups.get(lastAddedRowGroups.size() - 1).finishRow;
List toRemove = new ArrayList<>();
for (IElement cell : childElements) {
if (((Cell) cell).getRow() >= firstRow && ((Cell) cell).getRow() <= lastRow) {
toRemove.add(cell);
}
}
childElements.removeAll(toRemove);
for (int i = 0; i < lastRow - firstRow; i++) {
rows.remove(firstRow - rowWindowStart);
}
lastAddedRow = rows.remove(firstRow - rowWindowStart);
rowWindowStart = lastAddedRowGroups.get(lastAddedRowGroups.size() - 1).getFinishRow() + 1;
lastAddedRowGroups = null;
}
@Override
public void setDocument(Document document) {
this.document = document;
}
/**
* Gets the markup properties of the bottom border of the (current) last row.
*
* @return an array of {@link Border} objects
*/
public List getLastRowBottomBorder() {
List horizontalBorder = new ArrayList<>();
if (lastAddedRow != null) {
for (Cell cell : lastAddedRow) {
Border border = null;
if (cell != null) {
if (cell.hasProperty(Property.BORDER_BOTTOM)) {
border = cell.getProperty(Property.BORDER_BOTTOM);
} else {
border = cell.getDefaultProperty(Property.BORDER_BOTTOM);
}
}
horizontalBorder.add(border);
}
}
return horizontalBorder;
}
/**
* Defines whether the {@link Table} should be extended to occupy all the space left in the available area
* in case it is the last element in this area.
*
* @param isExtended defines whether the {@link Table} should be extended
* @return this {@link Table}
*/
public Table setExtendBottomRow(boolean isExtended) {
setProperty(Property.FILL_AVAILABLE_AREA, isExtended);
return this;
}
/**
* Defines whether the {@link Table} should be extended to occupy all the space left in the available area
* in case the area has been split and it is the last element in the split part of this area.
*
* @param isExtended defines whether the {@link Table} should be extended
* @return this {@link Table}
*/
public Table setExtendBottomRowOnSplit(boolean isExtended) {
setProperty(Property.FILL_AVAILABLE_AREA_ON_SPLIT, isExtended);
return this;
}
/**
* Sets the type of border collapse.
*
* @param collapsePropertyValue {@link BorderCollapsePropertyValue} to be set as the border collapse type
* @return this {@link Table}
*/
public Table setBorderCollapse(BorderCollapsePropertyValue collapsePropertyValue) {
setProperty(Property.BORDER_COLLAPSE, collapsePropertyValue);
if (null != header) {
header.setBorderCollapse(collapsePropertyValue);
}
if (null != footer) {
footer.setBorderCollapse(collapsePropertyValue);
}
return this;
}
/**
* Sets the horizontal spacing between this {@link Table table}'s {@link Cell cells}.
*
* @param spacing a horizontal spacing between this {@link Table table}'s {@link Cell cells}
* @return this {@link Table}
*/
public Table setHorizontalBorderSpacing(float spacing) {
setProperty(Property.HORIZONTAL_BORDER_SPACING, spacing);
if (null != header) {
header.setHorizontalBorderSpacing(spacing);
}
if (null != footer) {
footer.setHorizontalBorderSpacing(spacing);
}
return this;
}
/**
* Sets the vertical spacing between this {@link Table table}'s {@link Cell cells}.
*
* @param spacing a vertical spacing between this {@link Table table}'s {@link Cell cells}
* @return this {@link Table}
*/
public Table setVerticalBorderSpacing(float spacing) {
setProperty(Property.VERTICAL_BORDER_SPACING, spacing);
if (null != header) {
header.setVerticalBorderSpacing(spacing);
}
if (null != footer) {
footer.setVerticalBorderSpacing(spacing);
}
return this;
}
@Override
public AccessibilityProperties getAccessibilityProperties() {
if (tagProperties == null) {
tagProperties = new DefaultAccessibilityProperties(StandardRoles.TABLE);
}
return tagProperties;
}
@Override
protected IRenderer makeNewRenderer() {
return new TableRenderer(this);
}
private static UnitValue[] normalizeColumnWidths(float[] pointColumnWidths) {
UnitValue[] normalized = new UnitValue[pointColumnWidths.length];
for (int i = 0; i < normalized.length; i++) {
if (pointColumnWidths[i] >= 0) {
normalized[i] = UnitValue.createPointValue(pointColumnWidths[i]);
}
}
return normalized;
}
private static UnitValue[] normalizeColumnWidths(UnitValue[] unitColumnWidths) {
UnitValue[] normalized = new UnitValue[unitColumnWidths.length];
for (int i = 0; i < unitColumnWidths.length; i++) {
normalized[i] = unitColumnWidths[i] != null && unitColumnWidths[i].getValue() >= 0
? new UnitValue(unitColumnWidths[i])
: null;
}
return normalized;
}
private static UnitValue[] normalizeColumnWidths(int numberOfColumns) {
UnitValue[] normalized = new UnitValue[numberOfColumns];
return normalized;
}
/**
* Returns the list of all row groups.
*
* @return a list of a {@link RowRange} which holds the row numbers of a section of a table
*/
protected java.util.List getRowGroups() {
int lastRowWeCanFlush = currentColumn == columnWidths.length ? currentRow : currentRow - 1;
int[] cellBottomRows = new int[columnWidths.length];
int currentRowGroupStart = rowWindowStart;
java.util.List rowGroups = new ArrayList<>();
while (currentRowGroupStart <= lastRowWeCanFlush) {
for (int i = 0; i < columnWidths.length; i++) {
cellBottomRows[i] = currentRowGroupStart;
}
int maxRowGroupFinish = cellBottomRows[0] + rows.get(cellBottomRows[0] - rowWindowStart)[0].getRowspan() - 1;
boolean converged = false;
boolean rowGroupComplete = true;
while (!converged) {
converged = true;
for (int i = 0; i < columnWidths.length; i++) {
while (cellBottomRows[i] < lastRowWeCanFlush && cellBottomRows[i] + rows.get(cellBottomRows[i] - rowWindowStart)[i].getRowspan() - 1 < maxRowGroupFinish) {
cellBottomRows[i] += rows.get(cellBottomRows[i] - rowWindowStart)[i].getRowspan();
}
if (cellBottomRows[i] + rows.get(cellBottomRows[i] - rowWindowStart)[i].getRowspan() - 1 > maxRowGroupFinish) {
maxRowGroupFinish = cellBottomRows[i] + rows.get(cellBottomRows[i] - rowWindowStart)[i].getRowspan() - 1;
converged = false;
} else if (cellBottomRows[i] + rows.get(cellBottomRows[i] - rowWindowStart)[i].getRowspan() - 1 < maxRowGroupFinish) {
// don't have enough cells for a row group yet.
rowGroupComplete = false;
}
}
}
if (rowGroupComplete) {
rowGroups.add(new RowRange(currentRowGroupStart, maxRowGroupFinish));
}
currentRowGroupStart = maxRowGroupFinish + 1;
}
return rowGroups;
}
private void initializeRows() {
rows = new ArrayList<>();
currentColumn = -1;
}
private boolean cellBelongsToAnyRowGroup(Cell cell, List rowGroups) {
return rowGroups != null && rowGroups.size() > 0 && cell.getRow() >= rowGroups.get(0).getStartRow()
&& cell.getRow() <= rowGroups.get(rowGroups.size() - 1).getFinishRow();
}
private void ensureHeaderIsInitialized() {
if (header == null) {
header = new Table(columnWidths);
UnitValue width = getWidth();
if (width != null) header.setWidth(width);
header.getAccessibilityProperties().setRole(StandardRoles.THEAD);
if (hasOwnProperty(Property.BORDER_COLLAPSE)) {
header.setBorderCollapse((BorderCollapsePropertyValue) this.getProperty(Property.BORDER_COLLAPSE));
}
if (hasOwnProperty(Property.HORIZONTAL_BORDER_SPACING)) {
header.setHorizontalBorderSpacing((float) this.getProperty(Property.HORIZONTAL_BORDER_SPACING));
}
if (hasOwnProperty(Property.VERTICAL_BORDER_SPACING)) {
header.setVerticalBorderSpacing((float) this.getProperty(Property.VERTICAL_BORDER_SPACING));
}
}
}
private void ensureFooterIsInitialized() {
if (footer == null) {
footer = new Table(columnWidths);
UnitValue width = getWidth();
if (width != null) footer.setWidth(width);
footer.getAccessibilityProperties().setRole(StandardRoles.TFOOT);
if (hasOwnProperty(Property.BORDER_COLLAPSE)) {
footer.setBorderCollapse((BorderCollapsePropertyValue) this.getProperty(Property.BORDER_COLLAPSE));
}
if (hasOwnProperty(Property.HORIZONTAL_BORDER_SPACING)) {
footer.setHorizontalBorderSpacing((float) this.getProperty(Property.HORIZONTAL_BORDER_SPACING));
}
if (hasOwnProperty(Property.VERTICAL_BORDER_SPACING)) {
footer.setVerticalBorderSpacing((float) this.getProperty(Property.VERTICAL_BORDER_SPACING));
}
}
}
private void initializeLargeTable(boolean largeTable) {
this.isComplete = !largeTable;
if (largeTable) {
setWidth(UnitValue.createPercentValue(100));
setFixedLayout();
}
}
/**
* A simple object which holds the row numbers of a section of a table.
*/
public static class RowRange {
// The start number of the row group, inclusive
int startRow;
// The finish number of the row group, inclusive
int finishRow;
/**
* Creates a {@link RowRange}
*
* @param startRow the start number of the row group, inclusive
* @param finishRow the finish number of the row group, inclusive
*/
public RowRange(int startRow, int finishRow) {
this.startRow = startRow;
this.finishRow = finishRow;
}
/**
* Gets the starting row number of the table section
*
* @return the start number of the row group, inclusive
*/
public int getStartRow() {
return startRow;
}
/**
* Gets the finishing row number of the table section
*
* @return the finish number of the row group, inclusive
*/
public int getFinishRow() {
return finishRow;
}
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy
|