blob: 86c3fccf8778f18fc20a2887941d8cf7f0a32f82 [file] [log] [blame]
/*
* 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;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import org.apache.fop.area.Block;
import org.apache.fop.area.Footnote;
import org.apache.fop.area.PageViewport;
import org.apache.fop.fo.Constants;
import org.apache.fop.fo.FObj;
import org.apache.fop.fo.pagination.Region;
import org.apache.fop.fo.pagination.RegionBody;
import org.apache.fop.fo.pagination.StaticContent;
import org.apache.fop.layoutmgr.PageBreakingAlgorithm.PageBreakingLayoutListener;
import org.apache.fop.traits.MinOptMax;
/**
* Handles the breaking of pages in an fo:flow
*/
public class PageBreaker extends AbstractBreaker {
private PageSequenceLayoutManager pslm;
private boolean firstPart = true;
private boolean pageBreakHandled;
private boolean needColumnBalancing;
private PageProvider pageProvider;
private Block separatorArea;
/**
* The FlowLayoutManager object, which processes
* the single fo:flow of the fo:page-sequence
*/
private FlowLayoutManager childFLM = null;
private StaticContentLayoutManager footnoteSeparatorLM = null;
public PageBreaker(PageSequenceLayoutManager pslm) {
this.pslm = pslm;
this.pageProvider = pslm.getPageProvider();
this.childFLM = pslm.getLayoutManagerMaker().makeFlowLayoutManager(
pslm, pslm.getPageSequence().getMainFlow());
}
/** {@inheritDoc} */
protected void updateLayoutContext(LayoutContext context) {
int flowIPD = pslm.getCurrentPV().getCurrentSpan().getColumnWidth();
context.setRefIPD(flowIPD);
}
/** {@inheritDoc} */
protected LayoutManager getTopLevelLM() {
return pslm;
}
/** {@inheritDoc} */
protected PageProvider getPageProvider() {
return pslm.getPageProvider();
}
/**
* Starts the page breaking process.
* @param flowBPD the constant available block-progression-dimension (used for every part)
*/
void doLayout(int flowBPD) {
doLayout(flowBPD, false);
}
/** {@inheritDoc} */
protected PageBreakingLayoutListener createLayoutListener() {
return new PageBreakingLayoutListener() {
public void notifyOverflow(int part, int amount, FObj obj) {
Page p = pageProvider.getPage(
false, part, PageProvider.RELTO_CURRENT_ELEMENT_LIST);
RegionBody body = (RegionBody)p.getSimplePageMaster().getRegion(
Region.FO_REGION_BODY);
BlockLevelEventProducer eventProducer = BlockLevelEventProducer.Provider.get(
body.getUserAgent().getEventBroadcaster());
boolean canRecover = (body.getOverflow() != Constants.EN_ERROR_IF_OVERFLOW);
boolean needClip = (body.getOverflow() == Constants.EN_HIDDEN
|| body.getOverflow() == Constants.EN_ERROR_IF_OVERFLOW);
eventProducer.regionOverflow(this, body.getName(),
p.getPageViewport().getPageNumberString(),
amount, needClip, canRecover,
body.getLocator());
}
};
}
/** {@inheritDoc} */
protected int handleSpanChange(LayoutContext childLC, int nextSequenceStartsOn) {
needColumnBalancing = false;
if (childLC.getNextSpan() != Constants.NOT_SET) {
//Next block list will have a different span.
nextSequenceStartsOn = childLC.getNextSpan();
needColumnBalancing = childLC.getNextSpan() == Constants.EN_ALL
&& childLC.getDisableColumnBalancing() == Constants.EN_FALSE;
}
if (needColumnBalancing) {
AbstractBreaker.log.debug(
"Column balancing necessary for the next element list!!!");
}
return nextSequenceStartsOn;
}
/** {@inheritDoc} */
protected int getNextBlockList(LayoutContext childLC,
int nextSequenceStartsOn) {
return getNextBlockList(childLC, nextSequenceStartsOn, null, null, null);
}
/** {@inheritDoc} */
protected int getNextBlockList(LayoutContext childLC, int nextSequenceStartsOn,
Position positionAtIPDChange, LayoutManager restartLM, List firstElements) {
if (!firstPart) {
// if this is the first page that will be created by
// the current BlockSequence, it could have a break
// condition that must be satisfied;
// otherwise, we may simply need a new page
handleBreakTrait(nextSequenceStartsOn);
}
firstPart = false;
pageBreakHandled = true;
pageProvider.setStartOfNextElementList(pslm.getCurrentPageNum(),
pslm.getCurrentPV().getCurrentSpan().getCurrentFlowIndex());
return super.getNextBlockList(childLC, nextSequenceStartsOn, positionAtIPDChange,
restartLM, firstElements);
}
private boolean containsFootnotes(List contentList, LayoutContext context) {
boolean containsFootnotes = false;
if (contentList != null) {
ListIterator contentListIterator = contentList.listIterator();
while (contentListIterator.hasNext()) {
ListElement element = (ListElement) contentListIterator.next();
if (element instanceof KnuthBlockBox
&& ((KnuthBlockBox) element).hasAnchors()) {
// element represents a line with footnote citations
containsFootnotes = true;
LayoutContext footnoteContext = new LayoutContext(context);
footnoteContext.setStackLimitBP(context.getStackLimitBP());
footnoteContext.setRefIPD(pslm.getCurrentPV()
.getRegionReference(Constants.FO_REGION_BODY).getIPD());
List footnoteBodyLMs = ((KnuthBlockBox) element).getFootnoteBodyLMs();
ListIterator footnoteBodyIterator = footnoteBodyLMs.listIterator();
// store the lists of elements representing the footnote bodies
// in the box representing the line containing their references
while (footnoteBodyIterator.hasNext()) {
FootnoteBodyLayoutManager fblm
= (FootnoteBodyLayoutManager) footnoteBodyIterator.next();
fblm.setParent(childFLM);
fblm.initialize();
((KnuthBlockBox) element).addElementList(
fblm.getNextKnuthElements(footnoteContext, alignment));
}
}
}
}
return containsFootnotes;
}
private void handleFootnoteSeparator() {
StaticContent footnoteSeparator;
footnoteSeparator = pslm.getPageSequence().getStaticContent("xsl-footnote-separator");
if (footnoteSeparator != null) {
// the footnote separator can contain page-dependent content such as
// page numbers or retrieve markers, so its areas cannot simply be
// obtained now and repeated in each page;
// we need to know in advance the separator bpd: the actual separator
// could be different from page to page, but its bpd would likely be
// always the same
// create a Block area that will contain the separator areas
separatorArea = new Block();
separatorArea.setIPD(pslm.getCurrentPV()
.getRegionReference(Constants.FO_REGION_BODY).getIPD());
// create a StaticContentLM for the footnote separator
footnoteSeparatorLM
= pslm.getLayoutManagerMaker().makeStaticContentLayoutManager(
pslm, footnoteSeparator, separatorArea);
footnoteSeparatorLM.doLayout();
footnoteSeparatorLength = new MinOptMax(separatorArea.getBPD());
}
}
/** {@inheritDoc} */
protected List getNextKnuthElements(LayoutContext context, int alignment) {
List contentList = null;
while (!childFLM.isFinished() && contentList == null) {
contentList = childFLM.getNextKnuthElements(context, alignment);
}
// scan contentList, searching for footnotes
if (containsFootnotes(contentList, context)) {
// handle the footnote separator
handleFootnoteSeparator();
}
return contentList;
}
/** {@inheritDoc} */
protected List getNextKnuthElements(LayoutContext context, int alignment,
Position positionAtIPDChange, LayoutManager restartAtLM) {
List contentList = null;
do {
contentList = childFLM.getNextKnuthElements(context, alignment, positionAtIPDChange,
restartAtLM);
} while (!childFLM.isFinished() && contentList == null);
// scan contentList, searching for footnotes
if (containsFootnotes(contentList, context)) {
// handle the footnote separator
handleFootnoteSeparator();
}
return contentList;
}
/**
* @return current display alignment
*/
protected int getCurrentDisplayAlign() {
return pslm.getCurrentPage().getSimplePageMaster().getRegion(
Constants.FO_REGION_BODY).getDisplayAlign();
}
/**
* @return whether or not this flow has more page break opportunities
*/
protected boolean hasMoreContent() {
return !childFLM.isFinished();
}
/**
* Adds an area to the flow layout manager
* @param posIter the position iterator
* @param context the layout context
*/
protected void addAreas(PositionIterator posIter, LayoutContext context) {
if (footnoteSeparatorLM != null) {
StaticContent footnoteSeparator = pslm.getPageSequence().getStaticContent(
"xsl-footnote-separator");
// create a Block area that will contain the separator areas
separatorArea = new Block();
separatorArea.setIPD(
pslm.getCurrentPV().getRegionReference(Constants.FO_REGION_BODY).getIPD());
// create a StaticContentLM for the footnote separator
footnoteSeparatorLM = (StaticContentLayoutManager)
pslm.getLayoutManagerMaker().makeStaticContentLayoutManager(
pslm, footnoteSeparator, separatorArea);
footnoteSeparatorLM.doLayout();
}
childFLM.addAreas(posIter, context);
}
/**
* {@inheritDoc}
* This implementation checks whether to trigger column-balancing,
* or whether to take into account a 'last-page' condition.
*/
protected void doPhase3(PageBreakingAlgorithm alg, int partCount,
BlockSequence originalList, BlockSequence effectiveList) {
if (needColumnBalancing) {
//column balancing for the last part
doPhase3(alg, partCount, originalList, effectiveList, false);
return;
}
boolean lastPageMasterDefined = pslm.getPageSequence().hasPagePositionLast();
if (!hasMoreContent()) {
//last part is reached
if (lastPageMasterDefined) {
//last-page condition
doPhase3(alg, partCount, originalList, effectiveList, true);
return;
}
}
//nothing special: just add the areas now
addAreas(alg, partCount, originalList, effectiveList);
}
/**
* Restart the algorithm at the break corresponding
* to the given partCount
* (currently only used to redo the part after the
* last break in case of column-balancing
* and/or a last page-master)
*/
private void doPhase3(PageBreakingAlgorithm alg, int partCount,
BlockSequence originalList, BlockSequence effectiveList,
boolean isLastPart) {
int newStartPos = 0;
int restartPoint = pageProvider.getStartingPartIndexForLastPage(partCount);
if (restartPoint > 0) {
//Add definitive areas for the parts before the
//restarting point
addAreas(alg, restartPoint, originalList, effectiveList);
//Get page break from which we restart
PageBreakPosition pbp = (PageBreakPosition)
alg.getPageBreaks().get(restartPoint - 1);
newStartPos = pbp.getLeafPos() + 1;
//Handle page break right here to avoid any side-effects
if (newStartPos > 0) {
handleBreakTrait(Constants.EN_PAGE);
}
}
AbstractBreaker.log.debug("Restarting at " + restartPoint
+ ", new start position: " + newStartPos);
pageBreakHandled = true;
//Update so the available BPD is reported correctly
int currentPageNum = pslm.getCurrentPageNum();
pageProvider.setStartOfNextElementList(currentPageNum,
pslm.getCurrentPV().getCurrentSpan().getCurrentFlowIndex());
PageBreakingAlgorithm algRestart = null;
int optimalPageCount;
//Make sure we only add the areas we haven't added already
effectiveList.ignoreAtStart = newStartPos;
if (isLastPart) {
pageProvider.setLastPageIndex(currentPageNum);
}
if (needColumnBalancing) {
AbstractBreaker.log.debug("Column balancing now!!!");
AbstractBreaker.log.debug("===================================================");
//Restart last page
algRestart = new BalancingColumnBreakingAlgorithm(
getTopLevelLM(), getPageProvider(), createLayoutListener(),
alignment, Constants.EN_START, footnoteSeparatorLength,
isPartOverflowRecoveryActivated(),
pslm.getCurrentPV().getBodyRegion().getColumnCount());
AbstractBreaker.log.debug("===================================================");
} else {
//plain last page, no column balancing
AbstractBreaker.log.debug("Last page handling now!!!");
AbstractBreaker.log.debug("===================================================");
//Restart last page
algRestart = new PageBreakingAlgorithm(
getTopLevelLM(), getPageProvider(), createLayoutListener(),
alg.getAlignment(), alg.getAlignmentLast(),
footnoteSeparatorLength,
isPartOverflowRecoveryActivated(), false, false);
AbstractBreaker.log.debug("===================================================");
}
optimalPageCount = algRestart.findBreakingPoints(effectiveList,
newStartPos,
1, true, BreakingAlgorithm.ALL_BREAKS);
AbstractBreaker.log.debug("restart: optimalPageCount= " + optimalPageCount
+ " pageBreaks.size()= " + algRestart.getPageBreaks().size());
boolean fitsOnePage
= optimalPageCount <= pslm.getCurrentPV().getBodyRegion().getColumnCount();
if (isLastPart) {
if (fitsOnePage) {
//Replace last page
pslm.setCurrentPage(pageProvider.getPage(false, currentPageNum));
} else {
//Last page-master cannot hold the content.
//Add areas now...
addAreas(alg, restartPoint, partCount - restartPoint, originalList, effectiveList);
//...and add a blank last page
pageProvider.setLastPageIndex(currentPageNum + 1);
pslm.setCurrentPage(pslm.makeNewPage(true, true));
return;
}
} else {
if (!fitsOnePage) {
AbstractBreaker.log.warn(
"Breaking algorithm produced more columns than are available.");
/* reenable when everything works
throw new IllegalStateException(
"Breaking algorithm must not produce more columns than available.");
*/
}
}
addAreas(algRestart, optimalPageCount, originalList, effectiveList);
}
protected void startPart(BlockSequence list, int breakClass) {
AbstractBreaker.log.debug("startPart() breakClass=" + getBreakClassName(breakClass));
if (pslm.getCurrentPage() == null) {
throw new IllegalStateException("curPage must not be null");
}
if (!pageBreakHandled) {
//firstPart is necessary because we need the first page before we start the
//algorithm so we have a BPD and IPD. This may subject to change later when we
//start handling more complex cases.
if (!firstPart) {
// if this is the first page that will be created by
// the current BlockSequence, it could have a break
// condition that must be satisfied;
// otherwise, we may simply need a new page
handleBreakTrait(breakClass);
}
pageProvider.setStartOfNextElementList(pslm.getCurrentPageNum(),
pslm.getCurrentPV().getCurrentSpan().getCurrentFlowIndex());
}
pageBreakHandled = false;
// add static areas and resolve any new id areas
// finish page and add to area tree
firstPart = false;
}
/** {@inheritDoc} */
protected void handleEmptyContent() {
pslm.getCurrentPV().getPage().fakeNonEmpty();
}
protected void finishPart(PageBreakingAlgorithm alg, PageBreakPosition pbp) {
// add footnote areas
if (pbp.footnoteFirstListIndex < pbp.footnoteLastListIndex
|| pbp.footnoteFirstElementIndex <= pbp.footnoteLastElementIndex) {
// call addAreas() for each FootnoteBodyLM
for (int i = pbp.footnoteFirstListIndex; i <= pbp.footnoteLastListIndex; i++) {
List elementList = alg.getFootnoteList(i);
int firstIndex = (i == pbp.footnoteFirstListIndex
? pbp.footnoteFirstElementIndex : 0);
int lastIndex = (i == pbp.footnoteLastListIndex
? pbp.footnoteLastElementIndex : elementList.size() - 1);
SpaceResolver.performConditionalsNotification(elementList,
firstIndex, lastIndex, -1);
LayoutContext childLC = new LayoutContext(0);
AreaAdditionUtil.addAreas(null,
new KnuthPossPosIter(elementList, firstIndex, lastIndex + 1),
childLC);
}
// set the offset from the top margin
Footnote parentArea = pslm.getCurrentPV().getBodyRegion().getFootnote();
int topOffset = pslm.getCurrentPV().getBodyRegion().getBPD() - parentArea.getBPD();
if (separatorArea != null) {
topOffset -= separatorArea.getBPD();
}
parentArea.setTop(topOffset);
parentArea.setSeparator(separatorArea);
}
pslm.getCurrentPV().getCurrentSpan().notifyFlowsFinished();
}
/** @return the current child flow layout manager */
protected LayoutManager getCurrentChildLM() {
return childFLM;
}
/** {@inheritDoc} */
protected void observeElementList(List elementList) {
ElementListObserver.observe(elementList, "breaker",
pslm.getFObj().getId());
}
/**
* Depending on the kind of break condition, move to next column
* or page. May need to make an empty page if next page would
* not have the desired "handedness".
* @param breakVal - value of break-before or break-after trait.
*/
private void handleBreakTrait(int breakVal) {
Page curPage = pslm.getCurrentPage();
switch (breakVal) {
case Constants.EN_ALL:
//break due to span change in multi-column layout
curPage.getPageViewport().createSpan(true);
return;
case Constants.EN_NONE:
curPage.getPageViewport().createSpan(false);
return;
case Constants.EN_COLUMN:
case Constants.EN_AUTO:
case Constants.EN_PAGE:
case -1:
PageViewport pv = curPage.getPageViewport();
//Check if previous page was spanned
boolean forceNewPageWithSpan = false;
RegionBody rb = (RegionBody)curPage.getSimplePageMaster().getRegion(
Constants.FO_REGION_BODY);
forceNewPageWithSpan
= (rb.getColumnCount() > 1
&& pv.getCurrentSpan().getColumnCount() == 1);
if (forceNewPageWithSpan) {
log.trace("Forcing new page with span");
curPage = pslm.makeNewPage(false, false);
curPage.getPageViewport().createSpan(true);
} else if (pv.getCurrentSpan().hasMoreFlows()) {
log.trace("Moving to next flow");
pv.getCurrentSpan().moveToNextFlow();
} else {
log.trace("Making new page");
/*curPage = */pslm.makeNewPage(false, false);
}
return;
default:
log.debug("handling break-before after page " + pslm.getCurrentPageNum()
+ " breakVal=" + getBreakClassName(breakVal));
if (needBlankPageBeforeNew(breakVal)) {
log.trace("Inserting blank page");
/*curPage = */pslm.makeNewPage(true, false);
}
if (needNewPage(breakVal)) {
log.trace("Making new page");
/*curPage = */pslm.makeNewPage(false, false);
}
}
}
/**
* Check if a blank page is needed to accomodate
* desired even or odd page number.
* @param breakVal - value of break-before or break-after trait.
*/
private boolean needBlankPageBeforeNew(int breakVal) {
if (breakVal == Constants.EN_PAGE || (pslm.getCurrentPage().getPageViewport().getPage().isEmpty())) {
// any page is OK or we already have an empty page
return false;
} else {
/* IF we are on the kind of page we need, we'll need a new page. */
if (pslm.getCurrentPageNum() % 2 == 0) { // even page
return (breakVal == Constants.EN_EVEN_PAGE);
} else { // odd page
return (breakVal == Constants.EN_ODD_PAGE);
}
}
}
/**
* See if need to generate a new page
* @param breakVal - value of break-before or break-after trait.
*/
private boolean needNewPage(int breakVal) {
if (pslm.getCurrentPage().getPageViewport().getPage().isEmpty()) {
if (breakVal == Constants.EN_PAGE) {
return false;
} else if (pslm.getCurrentPageNum() % 2 == 0) { // even page
return (breakVal == Constants.EN_ODD_PAGE);
} else { // odd page
return (breakVal == Constants.EN_EVEN_PAGE);
}
} else {
return true;
}
}
}