Skip to content

Add smart row wrapping and cleanup table drawing #282

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions src/main/java/be/quodlibet/boxable/DefaultRowWrappingFunction.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package be.quodlibet.boxable;

import org.apache.pdfbox.pdmodel.PDPage;

import java.util.Arrays;
import java.util.List;

/**
* This Wrapping function allows you to create advanced table layouts by hiding borders,
* without braking them on pagebreaks. e.g.:
* ┌────┬──────┬──────┐
* ├────┼──────┤      │
* ├────┼──────┤      │
* ├────┼──────┤      │
* ├────┼──────┤      │
* ├────┼──────┤      │
* └────┴──────┴──────┘
* <p>
* is not broken between pages
*/
public class DefaultRowWrappingFunction implements RowWrappingFunction {


@Override
public <T extends PDPage> int[] getWrappableRows(List<Row<T>> rows) {
int[] wrapableRows = new int[rows.size()];
wrapableRows[0] = 0;
int j = 1;
for (int i = 1; i < rows.size(); i++) {
Row<?> row = rows.get(i);
if (isProperTableRowStart(row)) {
wrapableRows[j++] = i;
}
}
return Arrays.copyOf(wrapableRows, j);
}

/*
* Checks if this is a proper start of a table row,
* i.e. every cell is visibly closed by a border on top or has no sides anyway.
*
* allowed:
* "┌────┐", " "
* not allowed:
* "│ │", "│ "," │"
* */
private boolean isProperTableRowStart(Row<?> row) {
if (row.getCells().isEmpty()) {
return true;
}
boolean previousClosed = false;
for (Cell<?> cell : row.getCells()) {

if (cell.getTopBorder() == null && (cell.getLeftBorder() != null && !previousClosed)) {
return false;
}
previousClosed = cell.getTopBorder() != null;
}
Cell<?> lastCell = row.getCells().get(row.getCells().size() - 1);

return lastCell.getTopBorder() != null || (lastCell.getRightBorder() == null);
}

}
29 changes: 29 additions & 0 deletions src/main/java/be/quodlibet/boxable/Row.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public class Row<T extends PDPage> {
private boolean headerRow = false;
float height;
private float lineSpacing = 1;
private float wrapHeight = -1;

Row(Table<T> table, List<Cell<T>> cells, float height) {
this.table = table;
Expand Down Expand Up @@ -285,4 +286,32 @@ public float getLineSpacing() {
public void setLineSpacing(float lineSpacing) {
this.lineSpacing = lineSpacing;
}


/**
* Finds out, taking restricted row wrapping into account, how much vertical space this row,
* together with all un-wrappable rows that follow, will take.
*
* @return wrapHeight
*/
public float getWrapHeight() {
return table.calcWrapHeight(this);
}

protected void setWrapHeight(float wrapHeight) {
this.wrapHeight = wrapHeight;
}

protected float getSavedWrapHeight() {
return this.wrapHeight;
}

public boolean hasBottomBorder() {
for (Cell<T> cell : cells) {
if (cell.getBottomBorder() == null) {
return false;
}
}
return !cells.isEmpty();
}
}
19 changes: 19 additions & 0 deletions src/main/java/be/quodlibet/boxable/RowWrappingFunction.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package be.quodlibet.boxable;

import org.apache.pdfbox.pdmodel.PDPage;

import java.util.List;

/**
* This interface allows you to specify where in the table page breaks may be inserted.
*/
public interface RowWrappingFunction {


/**
* Allows you to specify at which indexes rows that can be moved to the next page are.
* @param rows list of rows to consider
* @return indexes of rows that may be moved to the next page, if the current page is full
*/
<T extends PDPage> int[] getWrappableRows(List<Row<T>> rows);
}
109 changes: 61 additions & 48 deletions src/main/java/be/quodlibet/boxable/Table.java
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

/*
Quodlibet.be
*/
Expand Down Expand Up @@ -56,6 +55,8 @@ public abstract class Table<T extends PDPage> {

private PageProvider<T> pageProvider;

private RowWrappingFunction rowWrappingFunction = new DefaultRowWrappingFunction();

// page margins
private final float pageTopMargin;
private final float pageBottomMargin;
Expand Down Expand Up @@ -83,7 +84,7 @@ public Table(float yStart, float yStartNewPage, float pageBottomMargin, float wi
this(yStart, yStartNewPage, 0, pageBottomMargin, width, margin, document, currentPage, drawLines, drawContent,
null);
}

/**
* @deprecated Use one of the constructors that pass a {@link PageProvider}
* @param yStartNewPage Y position where possible new page of {@link Table}
Expand Down Expand Up @@ -223,11 +224,16 @@ public Row<T> createRow(List<Cell<T>> cells, float height) {
public float draw() throws IOException {
ensureStreamIsOpen();

calcWrapHeightsAndRowHeights();

// for the first row in the table, we have to draw the top border
removeTopBorders = false;

for (Row<T> row : rows) {
if (header.contains(row)) {
// check if header row height and first data row height can fit
// the page
// if not draw them on another side
// if not draw them on another page
if (isEndOfPage(getMinimumHeight())) {
pageBreak();
tableStartedAtNewPage = true;
Expand All @@ -241,17 +247,9 @@ public float draw() throws IOException {
}

private void drawRow(Row<T> row) throws IOException {
// row.getHeight is currently an extremely expensive function so get the value
// once during drawing and reuse it, since it will not change during drawing
float rowHeight = row.getHeight();

// if it is not header row or first row in the table then remove row's
// top border
if (row != header && row != rows.get(0)) {
if (!isEndOfPage(rowHeight)) {
row.removeTopBorders();
}
}
// row.getHeight is currently an extremely expensive function, so we get the value
// calculated in calcWrapHeightsAndRowHeights, since it will not change during drawing
float rowHeight = row.getLineHeight();

// draw the bookmark
if (row.getBookmark() != null) {
Expand All @@ -262,49 +260,28 @@ private void drawRow(Row<T> row) throws IOException {
this.addBookmark(row.getBookmark());
}

// we want to remove the borders as often as possible
removeTopBorders = true;

// check also if we want all borders removed
if (allBordersRemoved()) {
row.removeAllBorders();
}

if (isEndOfPage(rowHeight) && !header.contains(row)) {
if (isEndOfPage(row.getSavedWrapHeight()) && !header.contains(row)) {

// Draw line at bottom of table
endTable();

// insert page break
pageBreak();

// redraw all headers on each currentPage
if (!header.isEmpty()) {
for (Row<T> headerRow : header) {
drawRow(headerRow);
}
// after you draw all header rows on next page please keep
// removing top borders to avoid double border drawing
removeTopBorders = true;
} else {
// after a page break, we have to ensure that top borders get
// drawn
removeTopBorders = false;
}
}
// if it is first row in the table, we have to draw the top border
if (row == rows.get(0)) {
// after a page break, we have to ensure that top borders get
// drawn
removeTopBorders = false;
}

if (removeTopBorders) {
row.removeTopBorders();
}
// redraw all headers on each page
for (Row<T> headerRow : header) {
drawRow(headerRow);
}

// if it is header row or first row in the table, we have to draw the
// top border
if (row == rows.get(0)) {
removeTopBorders = false;
}

if (removeTopBorders) {
Expand All @@ -318,6 +295,9 @@ private void drawRow(Row<T> row) throws IOException {
if (drawContent) {
drawCellContent(row, rowHeight);
}
//would be better to check line presence between rows and
//draw exactly what's needed between two rows in one go.
removeTopBorders = row.hasBottomBorder();
}

/**
Expand Down Expand Up @@ -398,9 +378,9 @@ private void drawCellContent(Row<T> row, float rowHeight) throws IOException {
break;
}
imageCell.getImage().draw(document, tableContentStream, cursorX, cursorY);

if (imageCell.getUrl() != null) {
List<PDAnnotation> annotations = ((PDPage)currentPage).getAnnotations();
List<PDAnnotation> annotations = ((PDPage) currentPage).getAnnotations();

PDBorderStyleDictionary borderULine = new PDBorderStyleDictionary();
borderULine.setStyle(PDBorderStyleDictionary.STYLE_UNDERLINE);
Expand All @@ -411,7 +391,7 @@ private void drawCellContent(Row<T> row, float rowHeight) throws IOException {

// Set the rectangle containing the link
// PDRectangle sets a the x,y and the width and height extend upwards from that!
PDRectangle position = new PDRectangle(cursorX, cursorY, (float)(imageCell.getImage().getWidth()), -(float)(imageCell.getImage().getHeight()));
PDRectangle position = new PDRectangle(cursorX, cursorY, (float) (imageCell.getImage().getWidth()), -(float) (imageCell.getImage().getHeight()));
txtLink.setRectangle(position);

// add an action
Expand Down Expand Up @@ -564,14 +544,14 @@ private void drawCellContent(Row<T> row, float rowHeight) throws IOException {
cursorY -= cell.getVerticalFreeSpace();
break;
}

if (cell.getUrl() != null) {
List<PDAnnotation> annotations = ((PDPage)currentPage).getAnnotations();
List<PDAnnotation> annotations = ((PDPage) currentPage).getAnnotations();
PDAnnotationLink txtLink = new PDAnnotationLink();

// Set the rectangle containing the link
// PDRectangle sets a the x,y and the width and height extend upwards from that!
PDRectangle position = new PDRectangle(cursorX - 5, cursorY + 10, (float)(cell.getWidth()), -(float)(cell.getHeight()));
PDRectangle position = new PDRectangle(cursorX - 5, cursorY + 10, (float) (cell.getWidth()), -(float) (cell.getHeight()));
txtLink.setRectangle(position);

// add an action
Expand Down Expand Up @@ -938,6 +918,10 @@ protected void setYStart(float yStart) {
this.yStart = yStart;
}

public void setRowWrappingFunction(RowWrappingFunction rowWrappingFunction) {
this.rowWrappingFunction = rowWrappingFunction;
}

public boolean isDrawDebug() {
return drawDebug;
}
Expand Down Expand Up @@ -978,4 +962,33 @@ public void removeAllBorders(boolean removeAllBorders) {
this.removeAllBorders = removeAllBorders;
}

protected float calcWrapHeight(Row<T> tRow) {
int[] wrappableRows = rowWrappingFunction.getWrappableRows(rows);
int currentRow = rows.indexOf(tRow);
for (int wrappableRow : wrappableRows) {
if (wrappableRow > currentRow) {
float wrapHeight = 0;
for (int i = currentRow; i < wrappableRow; i++) {
wrapHeight += rows.get(i).getHeight();
}
return wrapHeight;
}
}

return tRow.getHeight();
}

protected void calcWrapHeightsAndRowHeights() {
int[] wrappableRows = rowWrappingFunction.getWrappableRows(rows);
int prevRow = rows.size();
for (int i = wrappableRows.length - 1; i >= 0; i--) {
int wrappableRow = wrappableRows[i];
float height = 0;
for (int j = prevRow - 1; j >= wrappableRow; j--) {
height += rows.get(j).getHeight();
rows.get(j).setWrapHeight(height);
}
prevRow = wrappableRow;
}
}
}
1 change: 0 additions & 1 deletion src/test/java/be/quodlibet/boxable/DataTableTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import be.quodlibet.boxable.datatable.DataTable;
import be.quodlibet.boxable.datatable.UpdateCellProperty;
import com.google.common.io.Files;
import java.awt.Color;
import java.io.File;
import java.io.IOException;
Expand Down
Loading