| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| /* $Id$ */ |
| |
| package org.apache.fop.layoutmgr.table; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.List; |
| import java.util.ListIterator; |
| |
| import org.apache.commons.logging.Log; |
| import org.apache.commons.logging.LogFactory; |
| |
| import org.apache.fop.area.Block; |
| import org.apache.fop.area.Trait; |
| import org.apache.fop.fo.flow.table.ConditionalBorder; |
| import org.apache.fop.fo.flow.table.EffRow; |
| import org.apache.fop.fo.flow.table.EmptyGridUnit; |
| import org.apache.fop.fo.flow.table.GridUnit; |
| import org.apache.fop.fo.flow.table.PrimaryGridUnit; |
| import org.apache.fop.fo.flow.table.Table; |
| import org.apache.fop.fo.flow.table.TableColumn; |
| import org.apache.fop.fo.flow.table.TablePart; |
| import org.apache.fop.fo.properties.CommonBorderPaddingBackground; |
| import org.apache.fop.fo.properties.CommonBorderPaddingBackground.BorderInfo; |
| import org.apache.fop.layoutmgr.ElementListUtils; |
| import org.apache.fop.layoutmgr.KnuthElement; |
| import org.apache.fop.layoutmgr.KnuthPossPosIter; |
| import org.apache.fop.layoutmgr.LayoutContext; |
| import org.apache.fop.layoutmgr.SpaceResolver; |
| import org.apache.fop.layoutmgr.TraitSetter; |
| |
| class RowPainter { |
| private static Log log = LogFactory.getLog(RowPainter.class); |
| private int colCount; |
| private int currentRowOffset; |
| /** Currently handled row (= last encountered row). */ |
| private EffRow currentRow; |
| private LayoutContext layoutContext; |
| /** |
| * Index of the first row of the current part present on the current page. |
| */ |
| private int firstRowIndex; |
| |
| /** |
| * Index of the very first row on the current page. Needed to properly handle |
| * {@link BorderProps#COLLAPSE_OUTER}. This is not the same as {@link #firstRowIndex} |
| * when the table has headers! |
| */ |
| private int firstRowOnPageIndex; |
| |
| /** |
| * Keeps track of the y-offsets of each row on a page. |
| * This is particularly needed for spanned cells where you need to know the y-offset |
| * of the starting row when the area is generated at the time the cell is closed. |
| */ |
| private List rowOffsets = new ArrayList(); |
| |
| private int[] cellHeights; |
| private boolean[] firstCellOnPage; |
| private CellPart[] firstCellParts; |
| private CellPart[] lastCellParts; |
| |
| /** y-offset of the current table part. */ |
| private int tablePartOffset; |
| /** See {@link RowPainter#registerPartBackgroundArea(Block)}. */ |
| private CommonBorderPaddingBackground tablePartBackground; |
| /** See {@link RowPainter#registerPartBackgroundArea(Block)}. */ |
| private List tablePartBackgroundAreas; |
| |
| private TableContentLayoutManager tclm; |
| |
| RowPainter(TableContentLayoutManager tclm, LayoutContext layoutContext) { |
| this.tclm = tclm; |
| this.layoutContext = layoutContext; |
| this.colCount = tclm.getColumns().getColumnCount(); |
| this.cellHeights = new int[colCount]; |
| this.firstCellOnPage = new boolean[colCount]; |
| this.firstCellParts = new CellPart[colCount]; |
| this.lastCellParts = new CellPart[colCount]; |
| this.firstRowIndex = -1; |
| this.firstRowOnPageIndex = -1; |
| } |
| |
| void startTablePart(TablePart tablePart) { |
| CommonBorderPaddingBackground background = tablePart.getCommonBorderPaddingBackground(); |
| if (background.hasBackground()) { |
| tablePartBackground = background; |
| if (tablePartBackgroundAreas == null) { |
| tablePartBackgroundAreas = new ArrayList(); |
| } |
| } |
| tablePartOffset = currentRowOffset; |
| } |
| |
| /** |
| * Signals that the end of the current table part is reached. |
| * |
| * @param lastInBody true if the part is the last table-body element to be displayed |
| * on the current page. In which case all the cells must be flushed even if they |
| * aren't finished, plus the proper collapsed borders must be selected (trailing |
| * instead of normal, or rest if the cell is unfinished) |
| * @param lastOnPage true if the part is the last to be displayed on the current page. |
| * In which case collapsed after borders for the cells on the last row must be drawn |
| * in the outer mode |
| */ |
| void endTablePart(boolean lastInBody, boolean lastOnPage) { |
| addAreasAndFlushRow(lastInBody, lastOnPage); |
| |
| if (tablePartBackground != null) { |
| TableLayoutManager tableLM = tclm.getTableLM(); |
| for (Object tablePartBackgroundArea : tablePartBackgroundAreas) { |
| Block backgroundArea = (Block) tablePartBackgroundArea; |
| TraitSetter.addBackground(backgroundArea, tablePartBackground, tableLM, |
| -backgroundArea.getXOffset(), tablePartOffset - backgroundArea.getYOffset(), |
| tableLM.getContentAreaIPD(), currentRowOffset - tablePartOffset); |
| } |
| tablePartBackground = null; |
| tablePartBackgroundAreas.clear(); |
| } |
| } |
| |
| int getAccumulatedBPD() { |
| return currentRowOffset; |
| } |
| |
| /** |
| * Records the fragment of row represented by the given position. If it belongs to |
| * another (grid) row than the current one, that latter is painted and flushed first. |
| * |
| * @param tcpos a position representing the row fragment |
| */ |
| void handleTableContentPosition(TableContentPosition tcpos) { |
| if (log.isDebugEnabled()) { |
| log.debug("===handleTableContentPosition(" + tcpos); |
| } |
| if (currentRow == null) { |
| currentRow = tcpos.getNewPageRow(); |
| } else { |
| EffRow row = tcpos.getRow(); |
| if (row.getIndex() > currentRow.getIndex()) { |
| addAreasAndFlushRow(false, false); |
| currentRow = row; |
| } |
| } |
| if (firstRowIndex < 0) { |
| firstRowIndex = currentRow.getIndex(); |
| if (firstRowOnPageIndex < 0) { |
| firstRowOnPageIndex = firstRowIndex; |
| } |
| } |
| //Iterate over all grid units in the current step |
| for (Object cellPart1 : tcpos.cellParts) { |
| CellPart cellPart = (CellPart) cellPart1; |
| if (log.isDebugEnabled()) { |
| log.debug(">" + cellPart); |
| } |
| int colIndex = cellPart.pgu.getColIndex(); |
| if (firstCellParts[colIndex] == null) { |
| firstCellParts[colIndex] = cellPart; |
| cellHeights[colIndex] = cellPart.getBorderPaddingBefore(firstCellOnPage[colIndex]); |
| } else { |
| assert firstCellParts[colIndex].pgu == cellPart.pgu; |
| cellHeights[colIndex] += cellPart.getConditionalBeforeContentLength(); |
| } |
| cellHeights[colIndex] += cellPart.getLength(); |
| lastCellParts[colIndex] = cellPart; |
| } |
| } |
| |
| /** |
| * Creates the areas corresponding to the last row. That is, an area with background |
| * for the row, plus areas for all the cells that finish on the row (not spanning over |
| * further rows). |
| * |
| * @param lastInPart true if the row is the last from its table part to be displayed |
| * on the current page. In which case all the cells must be flushed even if they |
| * aren't finished, plus the proper collapsed borders must be selected (trailing |
| * instead of normal, or rest if the cell is unfinished) |
| * @param lastOnPage true if the row is the very last row of the table that will be |
| * displayed on the current page. In which case collapsed after borders must be drawn |
| * in the outer mode |
| */ |
| private void addAreasAndFlushRow(boolean lastInPart, boolean lastOnPage) { |
| if (currentRow == null) { |
| return; |
| } |
| if (log.isDebugEnabled()) { |
| log.debug("Remembering yoffset for row " + currentRow.getIndex() + ": " |
| + currentRowOffset); |
| } |
| recordRowOffset(currentRow.getIndex(), currentRowOffset); |
| |
| // Need to compute the actual row height first |
| // and determine border behaviour for empty cells |
| boolean firstCellPart = true; |
| boolean lastCellPart = true; |
| int actualRowHeight = 0; |
| for (int i = 0; i < colCount; i++) { |
| GridUnit currentGU = currentRow.getGridUnit(i); |
| if (currentGU.isEmpty()) { |
| continue; |
| } |
| if (currentGU.getColSpanIndex() == 0 |
| && (lastInPart || currentGU.isLastGridUnitRowSpan()) |
| && firstCellParts[i] != null) { |
| // TODO |
| // The last test above is a workaround for the stepping algorithm's |
| // fundamental flaw making it unable to produce the right element list for |
| // multiple breaks inside a same row group. |
| // (see http://wiki.apache.org/xmlgraphics-fop/TableLayout/KnownProblems) |
| // In some extremely rare cases (forced breaks, very small page height), a |
| // TableContentPosition produced during row delaying may end up alone on a |
| // page. It will not contain the CellPart instances for the cells starting |
| // the next row, so firstCellParts[i] will still be null for those ones. |
| int cellHeight = cellHeights[i]; |
| cellHeight += lastCellParts[i].getConditionalAfterContentLength(); |
| cellHeight += lastCellParts[i].getBorderPaddingAfter(lastInPart); |
| int cellOffset = getRowOffset(Math.max(firstCellParts[i].pgu.getRowIndex(), |
| firstRowIndex)); |
| actualRowHeight = Math.max(actualRowHeight, cellOffset + cellHeight |
| - currentRowOffset); |
| } |
| |
| if (firstCellParts[i] != null && !firstCellParts[i].isFirstPart()) { |
| firstCellPart = false; |
| } |
| if (lastCellParts[i] != null && !lastCellParts[i].isLastPart()) { |
| lastCellPart = false; |
| } |
| } |
| |
| // Then add areas for cells finishing on the current row |
| for (int i = 0; i < colCount; i++) { |
| GridUnit currentGU = currentRow.getGridUnit(i); |
| if (currentGU.isEmpty() && !tclm.isSeparateBorderModel()) { |
| int borderBeforeWhich; |
| if (firstCellPart) { |
| if (firstCellOnPage[i]) { |
| borderBeforeWhich = ConditionalBorder.LEADING_TRAILING; |
| } else { |
| borderBeforeWhich = ConditionalBorder.NORMAL; |
| } |
| } else { |
| borderBeforeWhich = ConditionalBorder.REST; |
| } |
| int borderAfterWhich; |
| if (lastCellPart) { |
| if (lastInPart) { |
| borderAfterWhich = ConditionalBorder.LEADING_TRAILING; |
| } else { |
| borderAfterWhich = ConditionalBorder.NORMAL; |
| } |
| } else { |
| borderAfterWhich = ConditionalBorder.REST; |
| } |
| assert (currentGU instanceof EmptyGridUnit); |
| addAreaForEmptyGridUnit((EmptyGridUnit)currentGU, |
| currentRow.getIndex(), i, |
| actualRowHeight, |
| borderBeforeWhich, borderAfterWhich, |
| lastOnPage); |
| |
| firstCellOnPage[i] = false; |
| } else if (currentGU.getColSpanIndex() == 0 |
| && (lastInPart || currentGU.isLastGridUnitRowSpan()) |
| && firstCellParts[i] != null) { |
| assert firstCellParts[i].pgu == currentGU.getPrimary(); |
| |
| int borderBeforeWhich; |
| if (firstCellParts[i].isFirstPart()) { |
| if (firstCellOnPage[i]) { |
| borderBeforeWhich = ConditionalBorder.LEADING_TRAILING; |
| } else { |
| borderBeforeWhich = ConditionalBorder.NORMAL; |
| } |
| } else { |
| assert firstCellOnPage[i]; |
| borderBeforeWhich = ConditionalBorder.REST; |
| } |
| int borderAfterWhich; |
| if (lastCellParts[i].isLastPart()) { |
| if (lastInPart) { |
| borderAfterWhich = ConditionalBorder.LEADING_TRAILING; |
| } else { |
| borderAfterWhich = ConditionalBorder.NORMAL; |
| } |
| } else { |
| borderAfterWhich = ConditionalBorder.REST; |
| } |
| |
| // when adding the areas for the TableCellLayoutManager this helps with the isLast trait |
| // if, say, the first cell of a row has content that fits in the page, but the content of |
| // the second cell does not fit this will assure that the isLast trait for the first cell |
| // will also be false |
| lastCellParts[i].pgu.getCellLM().setLastTrait(lastCellParts[i].isLastPart()); |
| addAreasForCell(firstCellParts[i].pgu, |
| firstCellParts[i].start, lastCellParts[i].end, |
| actualRowHeight, borderBeforeWhich, borderAfterWhich, |
| lastOnPage); |
| firstCellParts[i] = null; // why? what about the lastCellParts[i]? |
| Arrays.fill(firstCellOnPage, i, i + currentGU.getCell().getNumberColumnsSpanned(), |
| false); |
| } |
| } |
| currentRowOffset += actualRowHeight; |
| if (lastInPart) { |
| /* |
| * Either the end of the page is reached, then this was the last call of this |
| * method and we no longer care about currentRow; or the end of a table-part |
| * (header, footer, body) has been reached, and the next row will anyway be |
| * different from the current one, and this is unnecessary to call this method |
| * again in the first lines of handleTableContentPosition, so we may reset the |
| * following variables. |
| */ |
| currentRow = null; |
| firstRowIndex = -1; |
| rowOffsets.clear(); |
| /* |
| * The current table part has just been handled. Be it the first one or not, |
| * the header or the body, in any case the borders-before of the next row |
| * (i.e., the first row of the next part if any) must be painted in |
| * COLLAPSE_INNER mode. So the firstRowOnPageIndex indicator must be kept |
| * disabled. The following way is not the most elegant one but will be good |
| * enough. |
| */ |
| firstRowOnPageIndex = Integer.MAX_VALUE; |
| } |
| } |
| |
| // TODO this is not very efficient and should probably be done another way |
| // this method is only necessary when display-align = center or after, in which case |
| // the exact content length is needed to compute the size of the empty block that will |
| // be used as padding. |
| // This should be handled automatically by a proper use of Knuth elements |
| private int computeContentLength(PrimaryGridUnit pgu, int startIndex, int endIndex) { |
| if (startIndex > endIndex) { |
| // May happen if the cell contributes no content on the current page (empty |
| // cell, in most cases) |
| return 0; |
| } else { |
| ListIterator iter = pgu.getElements().listIterator(startIndex); |
| // Skip from the content length calculation glues and penalties occurring at the |
| // beginning of the page |
| boolean nextIsBox = false; |
| while (iter.nextIndex() <= endIndex && !nextIsBox) { |
| nextIsBox = ((KnuthElement) iter.next()).isBox(); |
| } |
| int len = 0; |
| if (((KnuthElement) iter.previous()).isBox()) { |
| while (iter.nextIndex() < endIndex) { |
| KnuthElement el = (KnuthElement) iter.next(); |
| if (el.isBox() || el.isGlue()) { |
| len += el.getWidth(); |
| } |
| } |
| len += ActiveCell.getElementContentLength((KnuthElement) iter.next()); |
| } |
| return len; |
| } |
| } |
| |
| private void addAreasForCell(PrimaryGridUnit pgu, int startPos, int endPos, |
| int rowHeight, int borderBeforeWhich, int borderAfterWhich, boolean lastOnPage) { |
| /* |
| * Determine the index of the first row of this cell that will be displayed on the |
| * current page. |
| */ |
| int currentRowIndex = currentRow.getIndex(); |
| int startRowIndex; |
| int firstRowHeight; |
| if (pgu.getRowIndex() >= firstRowIndex) { |
| startRowIndex = pgu.getRowIndex(); |
| if (startRowIndex < currentRowIndex) { |
| firstRowHeight = getRowOffset(startRowIndex + 1) - getRowOffset(startRowIndex); |
| } else { |
| firstRowHeight = rowHeight; |
| } |
| } else { |
| startRowIndex = firstRowIndex; |
| firstRowHeight = 0; |
| } |
| |
| /* |
| * In collapsing-border model, if the cell spans over several columns/rows then |
| * dedicated areas will be created for each grid unit to hold the corresponding |
| * borders. For that we need to know the height of each grid unit, that is of each |
| * grid row spanned over by the cell |
| */ |
| int[] spannedGridRowHeights = null; |
| if (!tclm.getTableLM().getTable().isSeparateBorderModel() && pgu.hasSpanning()) { |
| spannedGridRowHeights = new int[currentRowIndex - startRowIndex + 1]; |
| int prevOffset = getRowOffset(startRowIndex); |
| for (int i = 0; i < currentRowIndex - startRowIndex; i++) { |
| int newOffset = getRowOffset(startRowIndex + i + 1); |
| spannedGridRowHeights[i] = newOffset - prevOffset; |
| prevOffset = newOffset; |
| } |
| spannedGridRowHeights[currentRowIndex - startRowIndex] = rowHeight; |
| } |
| int cellOffset = getRowOffset(startRowIndex); |
| int cellTotalHeight = rowHeight + currentRowOffset - cellOffset; |
| if (log.isDebugEnabled()) { |
| log.debug("Creating area for cell:"); |
| log.debug(" start row: " + pgu.getRowIndex() + " " + currentRowOffset + " " |
| + cellOffset); |
| log.debug(" rowHeight=" + rowHeight + " cellTotalHeight=" + cellTotalHeight); |
| } |
| TableCellLayoutManager cellLM = pgu.getCellLM(); |
| cellLM.setXOffset(tclm.getXOffsetOfGridUnit(pgu)); |
| cellLM.setYOffset(cellOffset); |
| cellLM.setContentHeight(computeContentLength(pgu, startPos, endPos)); |
| cellLM.setTotalHeight(cellTotalHeight); |
| int prevBreak = ElementListUtils.determinePreviousBreak(pgu.getElements(), startPos); |
| if (endPos >= 0) { |
| SpaceResolver.performConditionalsNotification(pgu.getElements(), |
| startPos, endPos, prevBreak); |
| } |
| cellLM.addAreas(new KnuthPossPosIter(pgu.getElements(), startPos, endPos + 1), |
| layoutContext, spannedGridRowHeights, startRowIndex - pgu.getRowIndex(), |
| currentRowIndex - pgu.getRowIndex(), borderBeforeWhich, borderAfterWhich, |
| startRowIndex == firstRowOnPageIndex, lastOnPage, this, firstRowHeight); |
| } |
| |
| private void addAreaForEmptyGridUnit(EmptyGridUnit gu, int rowIndex, int colIndex, |
| int actualRowHeight, |
| int borderBeforeWhich, int borderAfterWhich, boolean lastOnPage) { |
| |
| //get effective borders |
| BorderInfo borderBefore = gu.getBorderBefore(borderBeforeWhich); |
| BorderInfo borderAfter = gu.getBorderAfter(borderAfterWhich); |
| BorderInfo borderStart = gu.getBorderStart(); |
| BorderInfo borderEnd = gu.getBorderEnd(); |
| if (borderBefore.getRetainedWidth() == 0 |
| && borderAfter.getRetainedWidth() == 0 |
| && borderStart.getRetainedWidth() == 0 |
| && borderEnd.getRetainedWidth() == 0) { |
| return; //no borders, no area necessary |
| } |
| |
| TableLayoutManager tableLM = tclm.getTableLM(); |
| Table table = tableLM.getTable(); |
| TableColumn col = tclm.getColumns().getColumn(colIndex + 1); |
| |
| //position information |
| boolean firstOnPage = (rowIndex == firstRowOnPageIndex); |
| boolean inFirstColumn = (colIndex == 0); |
| boolean inLastColumn = (colIndex == table.getNumberOfColumns() - 1); |
| |
| //determine the block area's size |
| int ipd = col.getColumnWidth().getValue(tableLM); |
| ipd -= (borderStart.getRetainedWidth() + borderEnd.getRetainedWidth()) / 2; |
| int bpd = actualRowHeight; |
| bpd -= (borderBefore.getRetainedWidth() + borderAfter.getRetainedWidth()) / 2; |
| |
| //generate the block area |
| Block block = new Block(); |
| block.setChangeBarList(tclm.getTableLM().getFObj().getChangeBarList()); |
| block.setPositioning(Block.ABSOLUTE); |
| block.addTrait(Trait.IS_REFERENCE_AREA, Boolean.TRUE); |
| block.setIPD(ipd); |
| block.setBPD(bpd); |
| block.setXOffset(tclm.getXOffsetOfGridUnit(colIndex, 1) |
| + (borderStart.getRetainedWidth() / 2)); |
| block.setYOffset(getRowOffset(rowIndex) |
| - (borderBefore.getRetainedWidth() / 2)); |
| boolean[] outer = new boolean[] {firstOnPage, lastOnPage, inFirstColumn, |
| inLastColumn}; |
| TraitSetter.addCollapsingBorders(block, |
| borderBefore, |
| borderAfter, |
| borderStart, |
| borderEnd, outer); |
| tableLM.addChildArea(block); |
| } |
| |
| /** |
| * Registers the given area, that will be used to render the part of |
| * table-header/footer/body background covered by a table-cell. If percentages are |
| * used to place the background image, the final bpd of the (fraction of) table part |
| * that will be rendered on the current page must be known. The traits can't then be |
| * set when the areas for the cell are created since at that moment this bpd is yet |
| * unknown. So they will instead be set in |
| * {@link #addAreasAndFlushRow(boolean, boolean)}. |
| * |
| * @param backgroundArea the block of the cell's dimensions that will hold the part |
| * background |
| */ |
| void registerPartBackgroundArea(Block backgroundArea) { |
| tclm.getTableLM().addBackgroundArea(backgroundArea); |
| tablePartBackgroundAreas.add(backgroundArea); |
| } |
| |
| /** |
| * Records the y-offset of the row with the given index. |
| * |
| * @param rowIndex index of the row |
| * @param offset y-offset of the row on the page |
| */ |
| private void recordRowOffset(int rowIndex, int offset) { |
| /* |
| * In some very rare cases a row may be skipped. See for example Bugzilla #43633: |
| * in a two-column table, a row contains a row-spanning cell and a missing cell. |
| * In TableStepper#goToNextRowIfCurrentFinished this row will immediately be |
| * considered as finished, since it contains no cell ending on this row. Thus no |
| * TableContentPosition will be created for this row. Thus its index will never be |
| * recorded by the #handleTableContentPosition method. |
| * |
| * The offset of such a row is the same as the next non-empty row. It's needed |
| * to correctly offset blocks for cells starting on this row. Hence the loop |
| * below. |
| */ |
| for (int i = rowOffsets.size(); i <= rowIndex - firstRowIndex; i++) { |
| rowOffsets.add(offset); |
| } |
| } |
| |
| /** |
| * Returns the offset of the row with the given index. |
| * |
| * @param rowIndex index of the row |
| * @return its y-offset on the page |
| */ |
| private int getRowOffset(int rowIndex) { |
| return (Integer) rowOffsets.get(rowIndex - firstRowIndex); |
| } |
| |
| // TODO get rid of that |
| /** Signals that the first table-body instance has started. */ |
| void startBody() { |
| Arrays.fill(firstCellOnPage, true); |
| } |
| |
| // TODO get rid of that |
| /** Signals that the last table-body instance has ended. */ |
| void endBody() { |
| Arrays.fill(firstCellOnPage, false); |
| } |
| } |