/*
This file belongs to the Servoy development and deployment environment, Copyright (C) 1997-2010 Servoy BV
This program is free software; you can redistribute it and/or modify it under
the terms of the GNU Affero General Public License as published by the Free
Software Foundation; either version 3 of the License, or (at your option) any
later version.
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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License along
with this program; if not, see http://www.gnu.org/licenses or write to the Free
Software Foundation,Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
*/
package com.servoy.j2db.printing;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.Shape;
import java.io.IOException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.swing.JComponent;
import javax.swing.JTextField;
import javax.swing.text.BadLocationException;
import javax.swing.text.JTextComponent;
import javax.swing.text.View;
import com.servoy.j2db.IApplication;
import com.servoy.j2db.dataprocessing.IDisplayData;
import com.servoy.j2db.dataprocessing.IFoundSetInternal;
import com.servoy.j2db.dataprocessing.IRecordInternal;
import com.servoy.j2db.persistence.ISupportPrintSliding;
import com.servoy.j2db.persistence.Part;
import com.servoy.j2db.smart.dataui.DataRenderer;
import com.servoy.j2db.smart.dataui.PortalComponent;
import com.servoy.j2db.util.Debug;
import com.servoy.j2db.util.IDelegate;
import com.servoy.j2db.util.PersistHelper;
import com.servoy.j2db.util.RendererParentWrapper;
import com.servoy.j2db.util.Utils;
/**
* This class tays together the renderer the data and the part in one enity which can be rendered onto a page(defination)
*
* @author jblok
*/
public class DataRendererDefinition implements Cloneable //cloneable for page border break
{
private Part part;
private final DataRenderer renderer;
private int startYOrgin;
private Dimension fixedSize;
private Dimension fullSize;
private int Ylocation;
private int Xlocation;
private final Map bounds;
private IRecordInternal state;
private IFoundSetInternal set;
private int index;
private int maxPageWidth = Integer.MAX_VALUE;
private final IApplication application;
DataRendererDefinition(IPrintInfo pi, RendererParentWrapper renderParent, Part p, DataRenderer r, IFoundSetInternal set, int index)
{
this(pi, renderParent, p, r);
this.set = set;
this.index = index;
init(renderParent);
}
DataRendererDefinition(IPrintInfo pi, RendererParentWrapper renderParent, Part p, DataRenderer r, IRecordInternal s)
{
this(pi, renderParent, p, r);
state = s;
init(renderParent);
}
public DataRendererDefinition(IPrintInfo pi, RendererParentWrapper renderParent, DataRenderer r)
{
application = pi.getApplication();
renderer = r;
bounds = new HashMap();
if (renderer != null)//renderer can be null if no body
{
List invokeLaterRunnables = new ArrayList();
renderer.notifyVisible(true, invokeLaterRunnables);
Utils.invokeLater(application, invokeLaterRunnables);
}
startYOrgin = 0;
init(renderParent);
}
private DataRendererDefinition(IPrintInfo pi, RendererParentWrapper renderParent, Part p, DataRenderer r)
{
maxPageWidth = (int)(pi.getPageFormat().getImageableWidth() * (1 / pi.getZoomFactor()));
application = pi.getApplication();
part = p;
renderer = r;
bounds = new HashMap();
if (renderer != null)//renderer can be null if no body
{
List invokeLaterRunnables = new ArrayList();
renderer.notifyVisible(true, invokeLaterRunnables);
Utils.invokeLater(pi.getApplication(), invokeLaterRunnables);
}
startYOrgin = 0;
}
private void init(RendererParentWrapper renderParent)
{
if (renderer != null)//can be empty, simulating body part
{
boolean isUsingPrintSliding = renderer.isUsingSliding();
boolean remainderIsLostAnyway = (part != null && part.getDiscardRemainderAfterBreak() && part.getPageBreakAfterOccurrence() == 1);
if (remainderIsLostAnyway)
{
isUsingPrintSliding = false;//optimize
}
try
{
tempAddToParent(renderParent, isUsingPrintSliding, true, true);
if (renderer == null)//can be empty, simulating body part
{
fullSize = new Dimension();
}
else
{
fullSize = renderer.getPreferredSize();
}
}
finally
{
tempRemoveFromParent(renderParent);
}
}
}
Dimension computeNewWidthForXLocation(RendererParentWrapper renderParent, int xLocation, boolean saveNewLayout)
{
ComputeNewWidthForXLocationRunnable r = new ComputeNewWidthForXLocationRunnable(renderParent, xLocation, saveNewLayout);
if (application.isEventDispatchThread())
{
r.run();
}
else
{
application.invokeAndWait(r);
}
return r.result;
}
class ComputeNewWidthForXLocationRunnable implements Runnable
{
private final int xLocation;
private final boolean saveNewLayout;
private Dimension result;
private final RendererParentWrapper renderParent;
public ComputeNewWidthForXLocationRunnable(RendererParentWrapper renderParent, int xLocation, boolean saveNewLayout)
{
this.renderParent = renderParent;
this.xLocation = xLocation;
this.saveNewLayout = saveNewLayout;
}
public void run()
{
result = computeNewWidthForXLocation();
}
public Dimension getResult()
{
return result;
}
private Dimension computeNewWidthForXLocation()
{
try
{
int slide;
boolean mustLayoutAgain = false;
ArrayList restrainedComponents = new ArrayList();
// see if some components that grow in width need and can to be limited to the page's right margin;
// if such components are found and they also
// need to change their height due to this limitation, compute new size
Map sliding = renderer.getComponentsUsingSliding();
if (sliding == null) return fullSize; // cannot do needed work, so return current width
Iterator slidingComponents = sliding.keySet().iterator();
while (slidingComponents.hasNext())
{
Component component = (Component)slidingComponents.next();
slide = ((Integer)sliding.get(component)).intValue();
// set max border width, so fields right hand side do not grow out of page
if ((slide & ISupportPrintSliding.GROW_WIDTH) == ISupportPrintSliding.GROW_WIDTH)
{
Rectangle r = (Rectangle)bounds.get(component);
if (r == null) return fullSize; // cannot do needed work, so return current width
r = new Rectangle(r);
// check if the "right hand border" of fields allowed to grow in width are still within page margin
int spaceToMargin = renderer.getSpaceToRightMargin();
if (xLocation + r.x + r.width + spaceToMargin > maxPageWidth)
{
r.width = maxPageWidth - r.x - xLocation - spaceToMargin;
if (r.width > 0) // make no sense to set negative size, component is already
// moved off page
{
if (saveNewLayout)
{
bounds.put(component, r); // for components that do not implement IFixedPreferredWidth
}
if (component instanceof IFixedPreferredWidth &&
((slide & ISupportPrintSliding.GROW_HEIGHT) == ISupportPrintSliding.GROW_HEIGHT || (slide & ISupportPrintSliding.SHRINK_HEIGHT) == ISupportPrintSliding.SHRINK_HEIGHT))
{
// if the component implements this interface and is allowed to grow/shrink vertically, it means that it might
// want to request a different height after it has been limited to fit in the page width
((IFixedPreferredWidth)component).setPreferredWidth(r.width);
restrainedComponents.add(component);
mustLayoutAgain = true;
}
}
}
}
} // end while
if (mustLayoutAgain)
{
try
{
tempAddToParent(renderParent, true, true, saveNewLayout);
Dimension newSize = renderer.getPreferredSize();
if (saveNewLayout)
{
fullSize = newSize;
}
return newSize;
}
finally
{
tempRemoveFromParent(renderParent);
for (int i = restrainedComponents.size() - 1; i >= 0; i--)
{
((IFixedPreferredWidth)restrainedComponents.get(i)).setPreferredWidth(-1);
}
}
}
else
{
return fullSize; // the size does not change if the page is added at the specified X location
}
}
catch (Exception ex)
{
Debug.error(ex);
if (fullSize != null) return fullSize;
return new Dimension(0, 0);
}
}
}
void tempAddToParent(final RendererParentWrapper renderParent, final boolean fillWithData, final boolean doLayout, final boolean saveComponentBounds)
{
try
{
Runnable r = new Runnable()
{
public void run()
{
try
{
if (!doLayout)
{
//restore the bound to match exactly to the init phase
//(and no fields are cut in half due to different html render positions)
restoreSavedComponentBounds();
}
//data is needed to see how (much) to slide
if (fillWithData)
{
IRecordInternal a_state = getState();
//lookup all field needed, so the are present in the state when needed for rendering
if (a_state != null) renderer.getDataAdapterList().setRecord(a_state, true);
}
//always needed for correct layout of text-areas/mediafields
renderParent.add(renderer);
List<Component> invisibleComponents = new ArrayList<Component>();
Component[] comps = renderer.getComponents();
for (int i = 0; i < comps.length; i++)
{
int slide = ISupportPrintSliding.NO_SLIDING;
Map componentsListToBeHandeld = renderer.getComponentsUsingSliding();
if (componentsListToBeHandeld != null && componentsListToBeHandeld.containsKey(comps[i]))
{
slide = ((Integer)componentsListToBeHandeld.get(comps[i])).intValue();
}
//set components invisible if empty/null and should shrink
if (((slide & ISupportPrintSliding.SHRINK_HEIGHT) == ISupportPrintSliding.SHRINK_HEIGHT) &&
((slide & ISupportPrintSliding.SHRINK_WIDTH) == ISupportPrintSliding.SHRINK_WIDTH) && comps[i] instanceof IDisplayData)
{
boolean visible = !(((IDisplayData)comps[i]).getValueObject() == null || ((IDisplayData)comps[i]).getValueObject().toString().trim().length() == 0);
comps[i].setVisible(visible);
if (!visible)
{
comps[i].setSize(0, 0);
invisibleComponents.add(comps[i]);
}
}
}
if (doLayout)
{
renderer.invalidate();
renderer.validate();
for (Component invisibleComponent : invisibleComponents)
{
invisibleComponent.setPreferredSize(new Dimension(0, 0));
}
//1) do first
renderer.doLayout();
//2) do second time, some times the after the fist time something is changed which causes stuff to render correctly
renderer.doLayout();
if (saveComponentBounds)
{
//store the prefered sizes so we don't have todo layouting again (by adding / removing)
for (Component element : comps)
{
bounds.put(element, new Rectangle(element.getBounds()));
}
}
for (Component invisibleComponent : invisibleComponents)
{
invisibleComponent.setPreferredSize(null);
}
}
else
{
//for bounds change, important for html/text-areas
Iterator it = bounds.keySet().iterator();
while (it.hasNext())
{
Component c = (Component)it.next();
c.validate();//relayouts the internals of html/text-areas
}
}
}
catch (Exception ex)
{
Debug.error(ex);
}
}
};
if (application.isEventDispatchThread())
{
r.run();
}
else
{
application.invokeAndWait(r);
}
}
catch (Exception e)
{
Debug.error(e);
}
}
void restoreSavedComponentBounds()
{
Iterator it = bounds.entrySet().iterator();
while (it.hasNext())
{
Map.Entry elem = (Map.Entry)it.next();
Component c = (Component)elem.getKey();
c.setBounds((Rectangle)elem.getValue());
}
}
void tempRemoveFromParent(final RendererParentWrapper renderParent)
{
try
{
Runnable r = new Runnable()
{
public void run()
{
//needed for correct drawing
renderParent.remove(renderer);
//clean panel
renderParent.invalidate();
}
};
if (application.isEventDispatchThread())
{
r.run();
}
else
{
application.invokeAndWait(r);
}
}
catch (Exception e)
{
Debug.error(e);
}
}
public DataRenderer getDataRenderer()
{
return renderer;
}
public IRecordInternal getState()
{
if (state == null)
{
if (set != null)
{
return set.getRecord(index);
}
return null;
}
else
{
return state;
}
}
/**
* Gets the size.
*
* @return Returns a Dimension
*/
public Dimension getFullSize()
{
return fullSize;
}
/**
* Gets the size.
*
* @return Returns a Dimension
*/
public Dimension getSize()
{
if (fixedSize == null)
{
return fullSize;
}
else
{
return fixedSize;
}
}
/**
* Sets the size. used for breaking parts
*
* @param size The size to set
*/
public void setFixedSize(Dimension size)
{
this.fixedSize = size;
}
/**
* Gets the startYOrgin,which is the starting Y draw location for the internal renderer
*/
public int getStartYOrgin()
{
return startYOrgin;
}
public void setStartYOrgin(int startYOrgin)
{
this.startYOrgin = startYOrgin;
}
@Override
public Object clone() throws CloneNotSupportedException
{
return super.clone();
}
@Override
public String toString()
{
return "prefsize: [" + PersistHelper.createDimensionString(fullSize) + "] size: [" + PersistHelper.createDimensionString(fixedSize) + "] Xlocation: " + Xlocation + " Ylocation: " + Ylocation + " startYOrgin: " + startYOrgin + " type: " + (part == null ? "" : part.toString()); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$
}
public void toXML(Writer w) throws IOException
{
if (part != null && getStartYOrgin() == 0)//t oprevent duplicates in normal printing not seen due to clipping
{
w.write("<PART type=\"" + Part.getDisplayName(part.getPartType()) + "\" x=\"" + getXlocation() + "\" y=\"" + getYlocation() + "\" width=\"" + getSize().width + "\" height=\"" + getSize().height + "\" >"); //$NON-NLS-1$ //$NON-NLS-2$//$NON-NLS-3$//$NON-NLS-4$//$NON-NLS-5$//$NON-NLS-6$
Iterator it = bounds.keySet().iterator();
while (it.hasNext())
{
Component element = (Component)it.next();
Rectangle rec = (Rectangle)bounds.get(element);
XMLPrintHelper.handleComponent(w, element, rec, null);
}
w.write("</PART>"); //$NON-NLS-1$
}
}
public boolean getAllowBreakAcrossPageBounds()
{
if (part != null)
{
return part.getAllowBreakAcrossPageBounds();
}
else
{
return true;
}
}
public boolean getDiscardRemainderAfterBreak()
{
if (part != null)
{
return part.getDiscardRemainderAfterBreak();
}
else
{
return false;
}
}
public boolean getPageBreakBefore()
{
if (part != null)
{
return part.getPageBreakBefore();
}
else
{
return false;
}
}
public int getPageBreakAfterOccurrence()
{
if (part != null)
{
return part.getPageBreakAfterOccurrence();
}
else
{
return 0;
}
}
public boolean getRestartPageNumber()
{
if (part != null)
{
return part.getRestartPageNumber();
}
else
{
return false;
}
}
public void printAll(Graphics g)
{
//print
renderer.printAll(g);
}
/**
* Gets the ylocation.
*
* @return Returns a int
*/
public int getYlocation()
{
return Ylocation;
}
/**
* Sets the ylocation.
*
* @param ylocation The ylocation to set
*/
public void setYlocation(int ylocation)
{
Ylocation = ylocation;
}
/**
* Returns the xlocation.
*
* @return int
*/
public int getXlocation()
{
return Xlocation;
}
/**
* Sets the xlocation.
*
* @param xlocation The xlocation to set
*/
public void setXlocation(int xlocation)
{
Xlocation = xlocation;
}
// return value must be <= then argument; return values < 0 mean that no good break was found
public int getPreferedBreak(RendererParentWrapper renderParent, int breakPosition, int normalPageBodyAreaHeight)
{
int validBreakPosition = getPreferedBreakInternal(renderParent, breakPosition);
// when you have the following situation: empty vertical space followed by compact block of component(s) (no valid break
// position), the result in breakPosition will be the end of the empty space... But this can lead for
// example to first page containing only a few pixels of empty space and the rest of the content moving
// to the second - where it will still break
if (validBreakPosition > 0 && validBreakPosition != breakPosition)
{
// so the initially suggested break position was moved up due to some intersecting components;
// if those components do not even fit on the next page, we might as well break them on this page
int nextPageValidBreak = getPreferedBreakInternal(renderParent, validBreakPosition + normalPageBodyAreaHeight);
if (nextPageValidBreak == validBreakPosition)
{
validBreakPosition = breakPosition; // did not find any valid breaks for leftovers on next page; so break it in this page
}
}
return validBreakPosition;
}
private int getPreferedBreakInternal(RendererParentWrapper renderParent, int breakPosition)
{
// the break position is relative to the remaining area to be used from the renderer; create the absolute breakk position for the renderer
Rectangle breakRect = new Rectangle(0, startYOrgin + breakPosition, getFullSize().width, 1);
int firstConsideredBreak = -1; // if we do not find a satisfactory break point, we use the first intersecting component desired break point
Component lastComponent = null; // the last component that changed the preferred break point
Iterator it = bounds.keySet().iterator();
while (it.hasNext())
{
Component component = (Component)it.next();
Rectangle cbounds = (Rectangle)bounds.get(component);
if (cbounds.intersects(breakRect) && component != lastComponent)
{
Component comp = component;
int pos;
if (comp instanceof PortalComponent)
{
Rectangle allocation = new Rectangle(cbounds);
PortalComponent pc = (PortalComponent)comp;
Insets ins2 = pc.getBorder().getBorderInsets(pc);
allocation.x += ins2.left;
allocation.y += ins2.top;
allocation.width -= ins2.right;
allocation.height -= ins2.bottom;
pos = pc.getPreferredBreak((startYOrgin + breakPosition - allocation.y)); // give free space and receive preferred break relative to component
if (pos != 0)
{
pos = pos + allocation.y - startYOrgin; // make preferred break relative to the renderer
Debug.trace("PortalComponent " + cbounds + " requested break for " + breakPosition + " used break " + pos); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
}
else
{
// no use breaking immediately after the border (no useful info)
pos = cbounds.getLocation().y - startYOrgin;
}
}
else
{
if (component instanceof IDelegate)
{
comp = (Component)((IDelegate)component).getDelegate();
}
if (comp instanceof JTextComponent)
{
if (comp instanceof JTextField)
{
pos = cbounds.getLocation().y - startYOrgin; //optimize, cannot break single line components, return the Y location
}
else
{
JTextComponent tcomp = (JTextComponent)comp;
Rectangle allocation = new Rectangle(cbounds);
if (component instanceof JComponent)
{
Insets ins2 = ((JComponent)component).getBorder().getBorderInsets(component);//==scrollpane border
if (ins2 != null)
{
allocation.x += ins2.left;
allocation.y += ins2.top;
allocation.width -= ins2.right;
allocation.height -= ins2.bottom;
}
}
Insets ins2 = tcomp.getBorder().getBorderInsets(component);//==margin
if (ins2 != null)
{
allocation.x += ins2.left;
allocation.y += ins2.top;
allocation.width -= ins2.right;
allocation.height -= ins2.bottom;
}
pos = searchDesiredBreakThroughViews((startYOrgin + breakPosition - allocation.y), 0, tcomp, 0, new Rectangle(0, 0,
allocation.width, allocation.height));
if (pos != 0)
{
pos = pos - startYOrgin + allocation.y; //correct for location
Debug.trace("JTextComponent " + cbounds + " requested break for " + breakPosition + " used break " + pos); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
}
else
{
// no use breaking immediately after the border (no useful info)
pos = cbounds.getLocation().y - startYOrgin;
}
}
}
else
// for all other components
{
pos = cbounds.getLocation().y - startYOrgin; //optimize, better stop before comp
}
}
// the current intersected component decided where it would want the page to break (pos); compare with the current break position
if (breakPosition != pos)
{
// we have to modify the desired (page) break point
breakPosition = pos;
breakRect.y = startYOrgin + breakPosition;
it = bounds.keySet().iterator(); // restart search for components that intersect the new considered break position
if ((firstConsideredBreak == -1) && (breakPosition > 0))
{
firstConsideredBreak = breakPosition;
}
lastComponent = component;
} // else the current break is OK from this component's point of view
if (breakPosition < 0)
{
break; // the components that we would find below 0 from this part are already printed - so we did not find a convenient
// break point in the given vertical space
}
}
}
if (breakPosition <= 0)
{
// no satisfactory normal break position; returns the first found break position (that can be < 0 too if not found)
return firstConsideredBreak;
}
else
{
// found a good break position
return breakPosition;
}
}
private int searchDesiredBreakThroughViews(final int pos, int returnValue, final JTextComponent tcomp, int parentY, Rectangle allocation)
{
// see if last kid view passes the desired break position or not (if not, then the desired position is fine)
final boolean[] viewsPassDesiredBreak = new boolean[1];
application.invokeAndWait(new Runnable()
{
public void run()
{
Rectangle r = null;
int lastPosition = tcomp.getDocument().getLength() - 1;
if (lastPosition >= 0)
{
try
{
r = tcomp.modelToView(tcomp.getDocument().getLength() - 1);
}
catch (BadLocationException e)
{
viewsPassDesiredBreak[0] = true;
}
}
else
{
r = new Rectangle(0, 0, 0, 0);
}
viewsPassDesiredBreak[0] = (r.y + r.height >= pos);
}
});
if (viewsPassDesiredBreak[0])
{
return walkView(pos, returnValue, tcomp.getUI().getRootView(tcomp), parentY, allocation);
}
else
{
return pos;
}
}
// Recursively walks a view hierarchy
private int walkView(int pos, int returnValue, View view, int parentY, Rectangle allocation)
{
//Get number of children views
int n = view.getViewCount();
// Visit the children of this view
for (int i = 0; i < n; i++)
{
View kid = view.getView(i);
Shape kidshape = view.getChildAllocation(i, allocation);
if (kidshape == null) continue;
Rectangle kidbox = kidshape.getBounds();
int kidY = ((int)kidbox.getY()) + parentY;
// //check for pagebreak <br id="pagebreak">
// int pagebreak = 0;
// if (kid != null)
// {
// Element e = kid.getElement();
// if (e != null && "br".equals(e.getName()))
// {
// AttributeSet as = e.getAttributes();
// if (as != null)
// {
// Object val = as.getAttribute(HTML.Attribute.ID);
// if (val != null && "pagebreak".equalsIgnoreCase(val.toString()))
// {
// if ( (kidY+kidbox.height) <= pos)
// {
// Debug.error("pagebreak "+(kidY+kidbox.height));
// pagebreak = (kidY+kidbox.height);
// }
// }
// }
// }
// }
if (kidY > returnValue && kidY <= pos)
{
// Debug.trace("Found Y "+Y+" (diff "+(Y-returnValue)+" )");
returnValue = kidY;
}
returnValue = walkView(pos, returnValue, kid, kidY, allocation);
}
return returnValue;
}
Part getPart()
{
return part;
}
}