/* * 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.Collections; import java.util.LinkedList; import java.util.List; import java.util.Map; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.fop.area.Area; import org.apache.fop.area.Block; import org.apache.fop.datatypes.LengthBase; import org.apache.fop.fo.Constants; import org.apache.fop.fo.FONode; import org.apache.fop.fo.FObj; import org.apache.fop.fo.flow.Marker; import org.apache.fop.fo.flow.Markers; import org.apache.fop.fo.flow.RetrieveTableMarker; import org.apache.fop.fo.flow.table.Table; import org.apache.fop.fo.flow.table.TableColumn; import org.apache.fop.fo.properties.CommonBorderPaddingBackground; import org.apache.fop.fo.properties.KeepProperty; import org.apache.fop.layoutmgr.BlockLevelEventProducer; import org.apache.fop.layoutmgr.BreakElement; import org.apache.fop.layoutmgr.BreakOpportunity; import org.apache.fop.layoutmgr.KnuthElement; import org.apache.fop.layoutmgr.KnuthGlue; import org.apache.fop.layoutmgr.LayoutContext; import org.apache.fop.layoutmgr.LeafPosition; import org.apache.fop.layoutmgr.ListElement; import org.apache.fop.layoutmgr.Position; import org.apache.fop.layoutmgr.PositionIterator; import org.apache.fop.layoutmgr.SpacedBorderedPaddedBlockLayoutManager; import org.apache.fop.layoutmgr.TraitSetter; import org.apache.fop.traits.MinOptMax; import org.apache.fop.traits.SpaceVal; import org.apache.fop.util.BreakUtil; /** * LayoutManager for a table FO. * A table consists of columns, table header, table footer and multiple * table bodies. * The header, footer and body add the areas created from the table cells. * The table then creates areas for the columns, bodies and rows * the render background. */ public class TableLayoutManager extends SpacedBorderedPaddedBlockLayoutManager implements BreakOpportunity { /** * logging instance */ private static Log log = LogFactory.getLog(TableLayoutManager.class); private TableContentLayoutManager contentLM; private ColumnSetup columns; private Block curBlockArea; private double tableUnit; private boolean autoLayout = true; private int halfBorderSeparationBPD; private int halfBorderSeparationIPD; /** See {@link TableLayoutManager#registerColumnBackgroundArea(TableColumn, Block, int)}. */ private List columnBackgroundAreas; private Position auxiliaryPosition; // this holds a possible list of TCLMs that needed to have their addAreas() repeated private List<TableCellLayoutManager> savedTCLMs; private boolean areAllTCLMsSaved; private Markers tableMarkers; private Markers tableFragmentMarkers; private boolean hasRetrieveTableMarker; private boolean repeatedHeader; private List<List<KnuthElement>> headerFootnotes = Collections.emptyList(); private List<List<KnuthElement>> footerFootnotes = Collections.emptyList(); /** * Temporary holder of column background informations for a table-cell's area. * * @see TableLayoutManager#registerColumnBackgroundArea(TableColumn, Block, int) */ private static final class ColumnBackgroundInfo { private TableColumn column; private Block backgroundArea; private int xShift; private ColumnBackgroundInfo(TableColumn column, Block backgroundArea, int xShift) { this.column = column; this.backgroundArea = backgroundArea; this.xShift = xShift; } } /** * Create a new table layout manager. * @param node the table FO */ public TableLayoutManager(Table node) { super(node); this.columns = new ColumnSetup(node); } @Override protected CommonBorderPaddingBackground getCommonBorderPaddingBackground() { return getTable().getCommonBorderPaddingBackground(); } /** @return the table FO */ public Table getTable() { return (Table)this.fobj; } /** * @return the column setup for this table. */ public ColumnSetup getColumns() { return this.columns; } /** {@inheritDoc} */ public void initialize() { foSpaceBefore = new SpaceVal( getTable().getCommonMarginBlock().spaceBefore, this).getSpace(); foSpaceAfter = new SpaceVal( getTable().getCommonMarginBlock().spaceAfter, this).getSpace(); startIndent = getTable().getCommonMarginBlock().startIndent.getValue(this); endIndent = getTable().getCommonMarginBlock().endIndent.getValue(this); if (getTable().isSeparateBorderModel()) { this.halfBorderSeparationBPD = getTable().getBorderSeparation().getBPD().getLength() .getValue(this) / 2; this.halfBorderSeparationIPD = getTable().getBorderSeparation().getIPD().getLength() .getValue(this) / 2; } else { this.halfBorderSeparationBPD = 0; this.halfBorderSeparationIPD = 0; } if (!getTable().isAutoLayout() && getTable().getInlineProgressionDimension().getOptimum(this).getEnum() != EN_AUTO) { autoLayout = false; } } private void resetSpaces() { this.discardBorderBefore = false; this.discardBorderAfter = false; this.discardPaddingBefore = false; this.discardPaddingAfter = false; this.effSpaceBefore = null; this.effSpaceAfter = null; } /** * @return half the value of border-separation.block-progression-dimension, or 0 if * border-collapse="collapse". */ public int getHalfBorderSeparationBPD() { return halfBorderSeparationBPD; } /** * @return half the value of border-separation.inline-progression-dimension, or 0 if * border-collapse="collapse". */ public int getHalfBorderSeparationIPD() { return halfBorderSeparationIPD; } /** {@inheritDoc} */ public List getNextKnuthElements(LayoutContext context, int alignment) { List returnList = new LinkedList(); /* * Compute the IPD and adjust it if necessary (overconstrained) */ referenceIPD = context.getRefIPD(); if (getTable().getInlineProgressionDimension().getOptimum(this).getEnum() != EN_AUTO) { int contentIPD = getTable().getInlineProgressionDimension().getOptimum(this) .getLength().getValue(this); updateContentAreaIPDwithOverconstrainedAdjust(contentIPD); } else { if (!getTable().isAutoLayout()) { BlockLevelEventProducer eventProducer = BlockLevelEventProducer.Provider.get( getTable().getUserAgent().getEventBroadcaster()); eventProducer.tableFixedAutoWidthNotSupported(this, getTable().getLocator()); } updateContentAreaIPDwithOverconstrainedAdjust(); } int sumOfColumns = columns.getSumOfColumnWidths(this); if (!autoLayout && sumOfColumns > getContentAreaIPD()) { log.debug(FONode.decorateWithContextInfo( "The sum of all column widths is larger than the specified table width.", getTable())); updateContentAreaIPDwithOverconstrainedAdjust(sumOfColumns); } int availableIPD = referenceIPD - getIPIndents(); if (getContentAreaIPD() > availableIPD) { BlockLevelEventProducer eventProducer = BlockLevelEventProducer.Provider.get( getTable().getUserAgent().getEventBroadcaster()); eventProducer.objectTooWide(this, getTable().getName(), getContentAreaIPD(), context.getRefIPD(), getTable().getLocator()); } /* initialize unit to determine computed values * for proportional-column-width() */ if (tableUnit == 0.0) { this.tableUnit = columns.computeTableUnit(this); } if (!firstVisibleMarkServed) { addKnuthElementsForSpaceBefore(returnList, alignment); } if (getTable().isSeparateBorderModel()) { addKnuthElementsForBorderPaddingBefore(returnList, !firstVisibleMarkServed); firstVisibleMarkServed = true; // Border and padding to be repeated at each break // This must be done only in the separate-border model, as in collapsing // tables have no padding and borders are determined at the cell level addPendingMarks(context); } // Elements for the table-header/footer/body List contentKnuthElements; contentLM = new TableContentLayoutManager(this); LayoutContext childLC = LayoutContext.newInstance(); /* childLC.setStackLimit( MinOptMax.subtract(context.getStackLimit(), stackSize));*/ childLC.setRefIPD(context.getRefIPD()); childLC.copyPendingMarksFrom(context); contentKnuthElements = contentLM.getNextKnuthElements(childLC, alignment); //Set index values on elements coming from the content LM for (Object contentKnuthElement : contentKnuthElements) { ListElement el = (ListElement) contentKnuthElement; notifyPos(el.getPosition()); } log.debug(contentKnuthElements); wrapPositionElements(contentKnuthElements, returnList); context.updateKeepWithPreviousPending(getKeepWithPrevious()); context.updateKeepWithPreviousPending(childLC.getKeepWithPreviousPending()); context.updateKeepWithNextPending(getKeepWithNext()); context.updateKeepWithNextPending(childLC.getKeepWithNextPending()); if (getTable().isSeparateBorderModel()) { addKnuthElementsForBorderPaddingAfter(returnList, true); } addKnuthElementsForSpaceAfter(returnList, alignment); if (!context.suppressBreakBefore()) { //addKnuthElementsForBreakBefore(returnList, context); int breakBefore = BreakUtil.compareBreakClasses(getTable().getBreakBefore(), childLC.getBreakBefore()); if (breakBefore != Constants.EN_AUTO) { returnList.add(0, new BreakElement(new LeafPosition(getParent(), 0), 0, -KnuthElement.INFINITE, breakBefore, context)); } } //addKnuthElementsForBreakAfter(returnList, context); int breakAfter = BreakUtil.compareBreakClasses(getTable().getBreakAfter(), childLC.getBreakAfter()); if (breakAfter != Constants.EN_AUTO) { returnList.add(new BreakElement(new LeafPosition(getParent(), 0), 0, -KnuthElement.INFINITE, breakAfter, context)); } setFinished(true); resetSpaces(); return returnList; } /** {@inheritDoc} */ public Position getAuxiliaryPosition() { /* * Redefined to return a LeafPosition instead of a NonLeafPosition. The * SpaceResolver.SpaceHandlingBreakPosition constructors unwraps all * NonLeafPositions, which can lead to a NPE when a break in a table occurs at a * page with different ipd. */ if (auxiliaryPosition == null) { auxiliaryPosition = new LeafPosition(this, 0); } return auxiliaryPosition; } /** * Registers the given area, that will be used to render the part of column background * covered by a table-cell. If percentages are used to place the background image, the * final bpd of the (fraction of) table 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 * TableLM's {@link #addAreas(PositionIterator, LayoutContext)} method. * * @param column the table-column element from which the cell gets background * informations * @param backgroundArea the block of the cell's dimensions that will hold the column * background * @param xShift additional amount by which the image must be shifted to be correctly * placed (to counterbalance the cell's start border) */ void registerColumnBackgroundArea(TableColumn column, Block backgroundArea, int xShift) { addBackgroundArea(backgroundArea); if (columnBackgroundAreas == null) { columnBackgroundAreas = new ArrayList(); } columnBackgroundAreas.add(new ColumnBackgroundInfo(column, backgroundArea, xShift)); } /** * The table area is a reference area that contains areas for * columns, bodies, rows and the contents are in cells. * * @param parentIter the position iterator * @param layoutContext the layout context for adding areas */ public void addAreas(PositionIterator parentIter, LayoutContext layoutContext) { getParentArea(null); addId(); // add space before, in order to implement display-align = "center" or "after" if (layoutContext.getSpaceBefore() != 0) { addBlockSpacing(0.0, MinOptMax.getInstance(layoutContext.getSpaceBefore())); } int startXOffset = getTable().getCommonMarginBlock().startIndent.getValue(this); // add column, body then row areas // BPD of the table, i.e., height of its content; table's borders and paddings not counted int tableHeight = 0; //Body childLM; LayoutContext lc = LayoutContext.offspringOf(layoutContext); lc.setRefIPD(getContentAreaIPD()); contentLM.setStartXOffset(startXOffset); contentLM.addAreas(parentIter, lc); tableHeight += contentLM.getUsedBPD(); curBlockArea.setBPD(tableHeight); if (columnBackgroundAreas != null) { for (Object columnBackgroundArea : columnBackgroundAreas) { ColumnBackgroundInfo b = (ColumnBackgroundInfo) columnBackgroundArea; TraitSetter.addBackground(b.backgroundArea, b.column.getCommonBorderPaddingBackground(), this, b.xShift, -b.backgroundArea.getYOffset(), b.column.getColumnWidth().getValue(this), tableHeight); } columnBackgroundAreas.clear(); } if (getTable().isSeparateBorderModel()) { TraitSetter.addBorders(curBlockArea, getTable().getCommonBorderPaddingBackground(), discardBorderBefore, discardBorderAfter, false, false, this); TraitSetter.addPadding(curBlockArea, getTable().getCommonBorderPaddingBackground(), discardPaddingBefore, discardPaddingAfter, false, false, this); } TraitSetter.addBackground(curBlockArea, getTable().getCommonBorderPaddingBackground(), this); TraitSetter.addMargins(curBlockArea, getTable().getCommonBorderPaddingBackground(), startIndent, endIndent, this); TraitSetter.addBreaks(curBlockArea, getTable().getBreakBefore(), getTable().getBreakAfter()); TraitSetter.addSpaceBeforeAfter(curBlockArea, layoutContext.getSpaceAdjust(), effSpaceBefore, effSpaceAfter); flush(); resetSpaces(); curBlockArea = null; notifyEndOfLayout(); } /** * Return an Area which can contain the passed childArea. The childArea * may not yet have any content, but it has essential traits set. * In general, if the LayoutManager already has an Area it simply returns * it. Otherwise, it makes a new Area of the appropriate class. * It gets a parent area for its area by calling its parent LM. * Finally, based on the dimensions of the parent area, it initializes * its own area. This includes setting the content IPD and the maximum * BPD. * * @param childArea the child area * @return the parent area of the child */ public Area getParentArea(Area childArea) { if (curBlockArea == null) { curBlockArea = new Block(); // Set up dimensions // Must get dimensions from parent area /*Area parentArea =*/ parentLayoutManager.getParentArea(curBlockArea); TraitSetter.setProducerID(curBlockArea, getTable().getId()); curBlockArea.setIPD(getContentAreaIPD()); setCurrentArea(curBlockArea); } return curBlockArea; } /** * Add the child area to this layout manager. * * @param childArea the child area to add */ public void addChildArea(Area childArea) { if (curBlockArea != null) { curBlockArea.addBlock((Block) childArea); } } /** * Adds the given area to this layout manager's area, without updating the used bpd. * * @param background an area */ void addBackgroundArea(Block background) { curBlockArea.addChildArea(background); } /** {@inheritDoc} */ public int negotiateBPDAdjustment(int adj, KnuthElement lastElement) { // TODO Auto-generated method stub return 0; } /** {@inheritDoc} */ public void discardSpace(KnuthGlue spaceGlue) { // TODO Auto-generated method stub } /** {@inheritDoc} */ public KeepProperty getKeepTogetherProperty() { return getTable().getKeepTogether(); } /** {@inheritDoc} */ public KeepProperty getKeepWithPreviousProperty() { return getTable().getKeepWithPrevious(); } /** {@inheritDoc} */ public KeepProperty getKeepWithNextProperty() { return getTable().getKeepWithNext(); } // --------- Property Resolution related functions --------- // /** * {@inheritDoc} */ public int getBaseLength(int lengthBase, FObj fobj) { // Special handler for TableColumn width specifications if (fobj instanceof TableColumn && fobj.getParent() == getFObj()) { switch (lengthBase) { case LengthBase.CONTAINING_BLOCK_WIDTH: return getContentAreaIPD(); case LengthBase.TABLE_UNITS: return (int) this.tableUnit; default: log.error("Unknown base type for LengthBase."); return 0; } } else { switch (lengthBase) { case LengthBase.TABLE_UNITS: return (int) this.tableUnit; default: return super.getBaseLength(lengthBase, fobj); } } } /** {@inheritDoc} */ public void reset() { super.reset(); curBlockArea = null; tableUnit = 0.0; } /** * Saves a TableCellLayoutManager for later use. * * @param tclm a TableCellLayoutManager that has a RetrieveTableMarker */ protected void saveTableHeaderTableCellLayoutManagers(TableCellLayoutManager tclm) { if (savedTCLMs == null) { savedTCLMs = new ArrayList<TableCellLayoutManager>(); } if (!areAllTCLMsSaved) { savedTCLMs.add(tclm); } } /** * Calls addAreas() for each of the saved TableCellLayoutManagers. */ protected void repeatAddAreasForSavedTableHeaderTableCellLayoutManagers() { if (savedTCLMs == null) { return; } // if we get to this stage then we are at the footer of the table fragment; this means that no more // different TCLM need to be saved (we already have all); we flag the list as being complete then areAllTCLMsSaved = true; for (TableCellLayoutManager tclm : savedTCLMs) { if (this.repeatedHeader) { tclm.setHasRepeatedHeader(true); } tclm.repeatAddAreas(); } } /** * Resolves a RetrieveTableMarker by finding a qualifying Marker to which it is bound to. * @param rtm the RetrieveTableMarker to be resolved * @return a bound RetrieveTableMarker instance or null if no qualifying Marker found */ public RetrieveTableMarker resolveRetrieveTableMarker(RetrieveTableMarker rtm) { String name = rtm.getRetrieveClassName(); int originalPosition = rtm.getPosition(); boolean changedPosition = false; Marker mark = null; // try the primary retrieve scope area, which is the same as table-fragment mark = (tableFragmentMarkers == null) ? null : tableFragmentMarkers.resolve(rtm); if (mark == null && rtm.getBoundary() != Constants.EN_TABLE_FRAGMENT) { rtm.changePositionTo(Constants.EN_LAST_ENDING); changedPosition = true; // try the page scope area mark = getCurrentPV().resolveMarker(rtm); if (mark == null && rtm.getBoundary() != Constants.EN_PAGE) { // try the table scope area mark = (tableMarkers == null) ? null : tableMarkers.resolve(rtm); } } if (changedPosition) { // so that the next time it is called looks unchanged rtm.changePositionTo(originalPosition); } if (mark == null) { log.debug("found no marker with name: " + name); return null; } else { rtm.bindMarker(mark); return rtm; } } /** * Register the markers for this table. * * @param marks the map of markers to add * @param starting if the area being added is starting or ending * @param isfirst if the area being added has is-first trait * @param islast if the area being added has is-last trait */ public void registerMarkers(Map<String, Marker> marks, boolean starting, boolean isfirst, boolean islast) { if (tableMarkers == null) { tableMarkers = new Markers(); } tableMarkers.register(marks, starting, isfirst, islast); if (tableFragmentMarkers == null) { tableFragmentMarkers = new Markers(); } tableFragmentMarkers.register(marks, starting, isfirst, islast); } /** * Clears the list of markers in the current table fragment. Should be called just before starting a new * header (that belongs to the next table fragment). */ protected void clearTableFragmentMarkers() { tableFragmentMarkers = null; } public void flagAsHavingRetrieveTableMarker() { hasRetrieveTableMarker = true; } protected void possiblyRegisterMarkersForTables(Map<String, Marker> markers, boolean isStarting, boolean isFirst, boolean isLast) { // note: if we allow table-footer after a table-body this check should not be made and the markers // should be registered regardless because the retrieval may be done only in the footer if (hasRetrieveTableMarker) { registerMarkers(markers, isStarting, isFirst, isLast); } super.possiblyRegisterMarkersForTables(markers, isStarting, isFirst, isLast); } void setHeaderFootnotes(List<List<KnuthElement>> footnotes) { this.headerFootnotes = footnotes; } List<List<KnuthElement>> getHeaderFootnotes() { return headerFootnotes; } void setFooterFootnotes(List<List<KnuthElement>> footnotes) { this.footerFootnotes = footnotes; } public void setRepeateHeader(boolean repeateHeader) { this.repeatedHeader = repeateHeader; } List<List<KnuthElement>> getFooterFootnotes() { return footerFootnotes; } }