/*
* This program is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software
* Foundation.
*
* You should have received a copy of the GNU Lesser General Public License along with this
* program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
* or from the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* 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 Lesser General Public License for more details.
*
* Copyright (c) 2001 - 2013 Object Refinery Ltd, Pentaho Corporation and Contributors.. All rights reserved.
*/
package org.pentaho.reporting.engine.classic.core.layout.process;
import org.pentaho.reporting.engine.classic.core.layout.model.BlockRenderBox;
import org.pentaho.reporting.engine.classic.core.layout.model.CanvasRenderBox;
import org.pentaho.reporting.engine.classic.core.layout.model.InlineRenderBox;
import org.pentaho.reporting.engine.classic.core.layout.model.LayoutNodeTypes;
import org.pentaho.reporting.engine.classic.core.layout.model.LogicalPageBox;
import org.pentaho.reporting.engine.classic.core.layout.model.ParagraphPoolBox;
import org.pentaho.reporting.engine.classic.core.layout.model.ParagraphRenderBox;
import org.pentaho.reporting.engine.classic.core.layout.model.RenderBox;
import org.pentaho.reporting.engine.classic.core.layout.model.RenderNode;
import org.pentaho.reporting.engine.classic.core.layout.model.RenderableComplexText;
import org.pentaho.reporting.engine.classic.core.layout.model.RenderableReplacedContentBox;
import org.pentaho.reporting.engine.classic.core.layout.model.RenderableText;
import org.pentaho.reporting.engine.classic.core.layout.model.table.TableCellRenderBox;
import org.pentaho.reporting.engine.classic.core.layout.model.table.TableColumnGroupNode;
import org.pentaho.reporting.engine.classic.core.layout.model.table.TableRenderBox;
import org.pentaho.reporting.engine.classic.core.layout.model.table.TableRowRenderBox;
import org.pentaho.reporting.engine.classic.core.layout.model.table.TableSectionRenderBox;
import org.pentaho.reporting.engine.classic.core.layout.process.linebreak.EmptyLinebreaker;
import org.pentaho.reporting.engine.classic.core.layout.process.linebreak.FullLinebreaker;
import org.pentaho.reporting.engine.classic.core.layout.process.linebreak.ParagraphLinebreaker;
import org.pentaho.reporting.engine.classic.core.layout.process.linebreak.SimpleLinebreaker;
import org.pentaho.reporting.libraries.base.util.FastStack;
/**
* This static computation step performs manual linebreaks on all paragraphs. This transforms the pool-collection into
* the lines-collection.
* <p/>
* For now, we follow a very simple path: A paragraph cannot be validated, if it is not yet closed. The linebreaking, be
* it the static one here or the dynamic one later, must be redone when the paragraph changes.
* <p/>
* Splitting for linebreaks happens only between inline-boxes. BlockBoxes that are contained in inline-boxes (like
* 'inline-block' elements or 'inline-tables') are considered unbreakable according to the CSS specs. Linebreaking can
* be suspended in these cases.
* <p/>
* As paragraphs itself are block elements, the linebreaks can be done iterative, using a simple stack to store the
* context of possibly nested paragraphs. The paragraph's pool contains the elements that should be processed, and the
* line-container will receive the pool's content (contained in an artificial inline element, as the linecontainer is a
* block-level element).
* <p/>
* Change-tracking should take place on the paragraph's pool element instead of the paragraph itself. This way, only
* structural changes are taken into account.
*
* @author Thomas Morgner
*/
public final class ParagraphLineBreakStep extends IterateStructuralProcessStep {
private static final EmptyLinebreaker LEAF_BREAK_STATE = new EmptyLinebreaker();
private FastStack<ParagraphLinebreaker> paragraphNesting;
private ParagraphLinebreaker breakState;
private SimpleLinebreaker reusableSimpleLinebreaker;
public ParagraphLineBreakStep() {
paragraphNesting = new FastStack<ParagraphLinebreaker>( 50 );
}
public void compute( final LogicalPageBox root ) {
paragraphNesting.clear();
try {
startProcessing( root );
} finally {
paragraphNesting.clear();
breakState = null;
}
}
protected void processParagraphChilds( final ParagraphRenderBox box ) {
super.processParagraphChilds( box );
}
protected void processRenderableContent( final RenderableReplacedContentBox box ) {
if ( breakState != null ) {
breakState.addNode( box );
}
}
protected boolean startBlockBox( final BlockRenderBox box ) {
if ( box.getNodeType() == LayoutNodeTypes.TYPE_BOX_PARAGRAPH ) {
final ParagraphRenderBox paragraphBox = (ParagraphRenderBox) box;
final long poolChangeTracker = paragraphBox.getPool().getChangeTracker();
final boolean unchanged = poolChangeTracker == paragraphBox.getLineBoxAge();
if ( unchanged || paragraphBox.getPool().getFirstChild() == null ) {
// If the paragraph is unchanged (no new elements have been added to the pool) then we can take a
// shortcut. The childs of this paragraph will also be unchanged (as any structural change would have increased
// the change-tracker).
//
// We treat an empty paragraph (pool has no childs) as unchanged at any time.
paragraphNesting.push( ParagraphLineBreakStep.LEAF_BREAK_STATE );
breakState = ParagraphLineBreakStep.LEAF_BREAK_STATE;
return false;
}
// When the paragraph has changed, this can only be caused by someone adding a new node to the paragraph
// or to one of the childs.
// Paragraphs can be nested whenever a Inline-Level element declares to be a Block-Layouter. (This is an
// Inline-Block or Inline-Table case in CSS)
// It is guaranteed, that if a child is changed, the parent is marked as changed as well.
// So we have only two cases to deal with: (1) The child is unchanged (2) the child is changed.
if ( breakState == null ) {
final ParagraphPoolBox paragraphPoolBox = paragraphBox.getPool();
final RenderNode firstChild = paragraphPoolBox.getFirstChild();
if ( firstChild == null ) {
paragraphBox.setPoolSize( 0 );
paragraphBox.setLineBoxAge( paragraphPoolBox.getChangeTracker() );
breakState = ParagraphLineBreakStep.LEAF_BREAK_STATE;
return false;
}
if ( firstChild == paragraphPoolBox.getLastChild() ) {
if ( ( firstChild.getLayoutNodeType() & LayoutNodeTypes.MASK_BOX ) != LayoutNodeTypes.MASK_BOX ) {
// Optimize away: A single text-element or other content in a linebox. No need to dive deeper.
paragraphBox.setPoolSize( 1 );
paragraphBox.setLineBoxAge( paragraphPoolBox.getChangeTracker() );
breakState = ParagraphLineBreakStep.LEAF_BREAK_STATE;
return false;
}
}
if ( paragraphBox.isComplexParagraph() ) {
final ParagraphLinebreaker item = new FullLinebreaker( paragraphBox );
paragraphNesting.push( item );
breakState = item;
} else {
if ( reusableSimpleLinebreaker == null ) {
reusableSimpleLinebreaker = new SimpleLinebreaker( paragraphBox );
} else {
reusableSimpleLinebreaker.recycle( paragraphBox );
}
paragraphNesting.push( reusableSimpleLinebreaker );
breakState = reusableSimpleLinebreaker;
}
return true;
}
// The breakState indicates that there is a paragraph processing active at the moment. This means, the
// paragraph-box we are dealing with right now is a nested box.
if ( breakState.isWritable() == false ) {
// OK, should not happen, but you never know. I'm good at hiding
// bugs in the code ..
throw new IllegalStateException( "A child cannot be dirty, if the parent is clean" );
}
// The paragraph is somehow nested in an other paragraph.
// This cannot be handled by the simple implementation, as we will most likely start to deriveForAdvance childs
// sooner
// or later
if ( breakState instanceof FullLinebreaker == false ) {
// convert it ..
final FullLinebreaker fullBreaker = breakState.startComplexLayout();
paragraphNesting.pop();
paragraphNesting.push( fullBreaker );
breakState = fullBreaker;
}
final ParagraphLinebreaker subFlow = breakState.startParagraphBox( paragraphBox );
paragraphNesting.push( subFlow );
breakState = subFlow;
return true;
}
// some other block box ..
if ( breakState == null ) {
if ( box.isLinebreakCacheValid() ) {
return false;
}
// Not nested in a paragraph, thats easy ..
return true;
}
if ( breakState.isWritable() == false ) {
throw new IllegalStateException( "This cannot be: There is an active break-state, but the box is not writable." );
}
breakState.startBlockBox( box );
return true;
}
protected void finishBlockBox( final BlockRenderBox box ) {
box.setLinebreakAge( box.getChangeTracker() );
if ( box.getNodeType() == LayoutNodeTypes.TYPE_BOX_PARAGRAPH ) {
if ( breakState == ParagraphLineBreakStep.LEAF_BREAK_STATE && paragraphNesting.isEmpty() ) {
// this is a non-nested simple paragraph; no need for clean up ..
breakState = null;
return;
}
// do the linebreak jiggle ...
// This is the first test case whether it is possible to avoid
// composition-recursion on such computations. I'd prefer to have
// an iterator pattern here ...
// finally update the change tracker ..
breakState.finish();
paragraphNesting.pop();
if ( paragraphNesting.isEmpty() ) {
if ( reusableSimpleLinebreaker != null ) {
reusableSimpleLinebreaker.dispose();
}
breakState = null;
} else {
breakState = paragraphNesting.peek();
breakState.finishParagraphBox( (ParagraphRenderBox) box );
}
return;
}
if ( breakState == null ) {
return;
}
if ( breakState.isWritable() == false ) {
throw new IllegalStateException( "A child cannot be dirty, if the parent is clean" );
}
breakState.finishBlockBox( box );
}
protected boolean startCanvasBox( final CanvasRenderBox box ) {
return startBox( box );
}
protected void finishCanvasBox( final CanvasRenderBox box ) {
finishBox( box );
}
protected boolean startInlineBox( final InlineRenderBox box ) {
if ( breakState == null || breakState.isWritable() == false ) {
if ( box.isLinebreakCacheValid() ) {
return false;
}
return true;
}
breakState.startInlineBox( box );
return true;
}
protected void finishInlineBox( final InlineRenderBox box ) {
box.setLinebreakAge( box.getChangeTracker() );
if ( breakState == null || breakState.isWritable() == false ) {
return;
}
breakState.finishInlineBox( box );
if ( breakState.isBreakRequested() && isEndOfLine( box ) == false ) {
performBreak();
}
}
protected boolean startRowBox( final RenderBox box ) {
return startBox( box );
}
protected void finishRowBox( final RenderBox box ) {
finishBox( box );
}
protected boolean startTableRowBox( final TableRowRenderBox box ) {
return startBox( box );
}
protected void finishTableRowBox( final TableRowRenderBox box ) {
finishBox( box );
}
protected boolean startTableSectionBox( final TableSectionRenderBox box ) {
return startBox( box );
}
protected void finishTableSectionBox( final TableSectionRenderBox box ) {
finishBox( box );
}
protected boolean startAutoBox( final RenderBox box ) {
return startBox( box );
}
protected void finishAutoBox( final RenderBox box ) {
finishBox( box );
}
private void finishBox( final RenderBox box ) {
box.setLinebreakAge( box.getChangeTracker() );
if ( breakState == null ) {
return;
}
if ( breakState.isWritable() == false ) {
throw new IllegalStateException( "A child cannot be dirty, if the parent is clean" );
}
breakState.finishBlockBox( box );
}
private boolean startBox( final RenderBox box ) {
if ( breakState == null ) {
if ( box.isLinebreakCacheValid() ) {
return false;
}
return true;
}
// some other block box .. suspend.
if ( breakState.isWritable() == false ) {
throw new IllegalStateException( "A child cannot be dirty, if the parent is clean" );
}
breakState.startBlockBox( box );
return true;
}
private void processText( final RenderableText text ) {
breakState.addNode( text );
if ( text.isForceLinebreak() == false ) {
return;
}
if ( breakState.isBreakRequested() ) {
performBreak();
}
// OK, someone requested a manual linebreak.
// Fill a stack with the current context ..
// Check if we are at the end of the line
if ( text.getNext() == null ) {
// OK, if we are at the end of the line (for all contexts), so we
// dont have to perform a break. The text will end anyway ..
if ( isEndOfLine( text ) ) {
return;
}
// as soon as we are no longer the last element - break!
// According to the flow rules, that will happen in one of the next
// finishInlineBox events ..
breakState.setBreakRequested( true );
return;
}
performBreak();
}
private void processText( final RenderableComplexText text ) {
breakState.addNode( text );
if ( text.isForceLinebreak() == false ) {
return;
}
if ( breakState.isBreakRequested() ) {
performBreak();
}
// OK, someone requested a manual linebreak.
// Fill a stack with the current context ..
// Check if we are at the end of the line
if ( text.getNext() == null ) {
// OK, if we are at the end of the line (for all contexts), so we
// dont have to perform a break. The text will end anyway ..
if ( isEndOfLine( text ) ) {
return;
}
// as soon as we are no longer the last element - break!
// According to the flow rules, that will happen in one of the next
// finishInlineBox events ..
breakState.setBreakRequested( true );
return;
}
performBreak();
}
protected void processOtherNode( final RenderNode node ) {
if ( breakState == null || breakState.isWritable() == false ) {
return;
}
if ( breakState.isSuspended() ) {
breakState.addNode( node );
} else if ( node.getNodeType() == LayoutNodeTypes.TYPE_NODE_TEXT ) {
final RenderableText text = (RenderableText) node;
processText( text );
} else if ( node.getNodeType() == LayoutNodeTypes.TYPE_NODE_COMPLEX_TEXT ) {
final RenderableComplexText text = (RenderableComplexText) node;
processText( text );
} else {
breakState.addNode( node );
}
}
private boolean isEndOfLine( RenderNode node ) {
while ( node != null ) {
final int nodeType = node.getLayoutNodeType();
if ( ( nodeType & LayoutNodeTypes.MASK_BOX ) == LayoutNodeTypes.MASK_BOX
&& ( nodeType & LayoutNodeTypes.MASK_BOX_INLINE ) != LayoutNodeTypes.MASK_BOX_INLINE ) {
return true;
}
if ( node.getNext() != null ) {
return false;
}
node = node.getParent();
}
return true;
}
private void performBreak() {
if ( breakState instanceof FullLinebreaker == false ) {
final FullLinebreaker fullBreaker = breakState.startComplexLayout();
paragraphNesting.pop();
paragraphNesting.push( fullBreaker );
breakState = fullBreaker;
fullBreaker.performBreak();
} else {
final FullLinebreaker fullBreaker = (FullLinebreaker) breakState;
fullBreaker.performBreak();
}
}
protected boolean startOtherBox( final RenderBox box ) {
if ( breakState == null ) {
if ( box.isLinebreakCacheValid() ) {
return false;
}
return true;
}
if ( breakState.isWritable() == false ) {
return false;
}
breakState.startBlockBox( box );
return true;
}
protected void finishOtherBox( final RenderBox box ) {
box.setLinebreakAge( box.getChangeTracker() );
if ( breakState != null && breakState.isWritable() ) {
breakState.finishBlockBox( box );
}
}
protected boolean startTableCellBox( final TableCellRenderBox box ) {
if ( breakState == null ) {
if ( box.isLinebreakCacheValid() ) {
return false;
}
return true;
}
if ( breakState.isWritable() == false ) {
return false;
}
breakState.startBlockBox( box );
return true;
}
protected void finishTableCellBox( final TableCellRenderBox box ) {
box.setLinebreakAge( box.getChangeTracker() );
if ( breakState != null && breakState.isWritable() ) {
breakState.finishBlockBox( box );
}
}
protected boolean startTableColumnGroupBox( final TableColumnGroupNode box ) {
return false;
}
protected boolean startTableBox( final TableRenderBox box ) {
if ( breakState == null ) {
if ( box.isLinebreakCacheValid() ) {
return false;
}
return true;
}
if ( breakState.isWritable() == false ) {
return false;
}
breakState.startBlockBox( box );
return true;
}
protected void finishTableBox( final TableRenderBox box ) {
box.setLinebreakAge( box.getChangeTracker() );
if ( breakState != null && breakState.isWritable() ) {
breakState.finishBlockBox( box );
}
}
}