Bug 506392 - [Print] Support stretching with fit-to-page

Change-Id: Ib1d5875734e9aadf4da2d8108263a94d4bd394b6
Signed-off-by: Dirk Fauth <dirk.fauth@googlemail.com>
diff --git a/org.eclipse.nebula.widgets.nattable.core/src/org/eclipse/nebula/widgets/nattable/print/LayerPrinter.java b/org.eclipse.nebula.widgets.nattable.core/src/org/eclipse/nebula/widgets/nattable/print/LayerPrinter.java
index 08218bb..3d2f4b6 100644
--- a/org.eclipse.nebula.widgets.nattable.core/src/org/eclipse/nebula/widgets/nattable/print/LayerPrinter.java
+++ b/org.eclipse.nebula.widgets.nattable.core/src/org/eclipse/nebula/widgets/nattable/print/LayerPrinter.java
@@ -85,6 +85,7 @@
     protected boolean preRender = true;
 
     private final Direction fittingMode;
+    private final boolean stretch;
 
     private int headerHeight = 0;
 
@@ -152,6 +153,11 @@
                 PrintConfigAttributes.FITTING_MODE,
                 DisplayMode.NORMAL);
         this.fittingMode = (configuredFittingMode != null) ? configuredFittingMode : Direction.NONE;
+
+        Boolean configureStretching = configRegistry.getConfigAttribute(
+                PrintConfigAttributes.STRETCH,
+                DisplayMode.NORMAL);
+        this.stretch = (configureStretching != null) ? configureStretching : false;
     }
 
     /**
@@ -228,6 +234,22 @@
     }
 
     /**
+     * @param printer
+     *            The printer that will be used for printing.
+     * @return The scale factor used for the scaling of the repeat print target
+     *         or [0, 0] of there is no print target configured for repeating.
+     */
+    private float[] getRepeatPrintTargetScaleFactor(Printer printer) {
+        float[] result = new float[] { 0, 0 };
+        // currently we only support repeating the first of the configured multi
+        // print targets
+        if (this.printTargets.get(0).repeat) {
+            result = computeLayerScaleFactor(this.printTargets.get(0).layer, printer);
+        }
+        return result;
+    }
+
+    /**
      * Computes the scale factor to match the printer resolution.
      *
      * @param layer
@@ -267,7 +289,7 @@
             // stretching could cause serious issues, e.g. vertical
             // stretching for one row could cause a really long running
             // operation because the width gets really really big
-            if (pixelX > sfX) {
+            if (pixelX > sfX && !this.stretch) {
                 pixelX = sfX;
             }
             if (pixelY > sfY) {
@@ -302,7 +324,8 @@
     private float[] computeLayerScaleFactor(ILayer layer, Printer printer) {
         float[] scaleFactor = null;
         if (this.fittingMode == Direction.NONE
-                || (!this.join && !this.printTargets.get(0).repeat)) {
+                || (!this.join && !this.printTargets.get(0).repeat)
+                || this.stretch) {
             scaleFactor = computeScaleFactor(layer, printer, false);
         } else {
             // search for the common scaling factor
@@ -339,9 +362,10 @@
     private int getPageCount(Printer printer) {
         int result = 0;
         int available = -1;
+        float[] prevScaleFactor = null;
         for (PrintTarget target : this.printTargets) {
             if (!target.repeat) {
-                int[] layerResult = getPageCount(target, printer, available);
+                int[] layerResult = getPageCount(target, printer, available, prevScaleFactor);
                 result += (layerResult[0] * layerResult[1]);
 
                 // as the print targets should be joined and the print was
@@ -351,6 +375,9 @@
                 }
 
                 available = layerResult[2];
+                if (available >= 0) {
+                    prevScaleFactor = computeLayerScaleFactor(target.layer, printer);
+                }
             }
         }
 
@@ -366,14 +393,18 @@
      * @param printer
      *            The printer that will be used.
      * @param available
-     *            The remaining available space on a page after the target is
-     *            printed in case the print targets should be glued, -1
-     *            otherwise.
+     *            The remaining available space in pixel on a page after the
+     *            target is printed in case the print targets should be glued,
+     *            -1 otherwise.
+     * @param prevScaleFactor
+     *            The scale factor of the previous table in case the tables
+     *            should be joined. Needed to calculate the available space
+     *            correctly
      * @return The number of horizontal and vertical pages that are needed to
      *         print the layer of the given print target and the remaining space
      *         on a page if the print targets should be glued.
      */
-    private int[] getPageCount(PrintTarget target, Printer printer, int available) {
+    private int[] getPageCount(PrintTarget target, Printer printer, int available, float[] prevScaleFactor) {
         Rectangle printArea = computePrintArea(printer);
         float[] scaleFactor = computeLayerScaleFactor(target.layer, printer);
 
@@ -403,14 +434,14 @@
         }
 
         int numOfVerticalPages = 0;
-        int repeatPrintTargetHeightInDpi = Math.round(Float.valueOf(getRepeatPrintTargetHeight() * scaleFactor[1]));
+        int repeatPrintTargetHeightInDpi = Math.round(Float.valueOf(getRepeatPrintTargetHeight() * getRepeatPrintTargetScaleFactor(printer)[1]));
         int headerHeightInDpi = (this.headerHeight != 0 && this.repeatHeaderLayer != null)
                 ? Math.round(Float.valueOf((this.headerHeight * scaleFactor[1]))) : 0;
         int pageHeight = Math.round(Float.valueOf(
                 (printArea.height - repeatPrintTargetHeightInDpi - headerHeightInDpi - getFooterHeightInPrinterDPI()) / scaleFactor[1]));
         int firstPageHeight = (available < 0)
                 ? Math.round(Float.valueOf((printArea.height - repeatPrintTargetHeightInDpi - getFooterHeightInPrinterDPI()) / scaleFactor[1]))
-                : available;
+                : Math.round(Float.valueOf(Math.round(Float.valueOf(available * prevScaleFactor[1])) / scaleFactor[1]));
         int endY = 0;
         int added = 0;
         int remaining = -1;
@@ -680,8 +711,11 @@
                     int totalPageCount = getPageCount(this.printer);
 
                     Integer[] repeatHeaderGridLineWidth = null;
+                    float[] repeatScaleFactor = null;
 
                     int available = -1;
+                    float[] prevScaleFactor = null;
+
                     boolean newPage = true;
                     boolean pageStarted = false;
                     for (PrintTarget target : LayerPrinter.this.printTargets) {
@@ -689,6 +723,7 @@
                             // we do not render the repeat print target directly
                             // as it is handled on every page while printing
                             repeatHeaderGridLineWidth = getGridLineWidth(target.configRegistry);
+                            repeatScaleFactor = computeLayerScaleFactor(target.layer, this.printer);
                             continue;
                         }
 
@@ -712,6 +747,12 @@
                         float[] scaleFactor = computeLayerScaleFactor(target.layer, this.printer);
                         float[] dpiFactor = computeScaleFactor(target.layer, this.printer, true);
 
+                        int availablePixel = available;
+                        if (available > 0) {
+                            int prevDPI = Math.round(Float.valueOf(available * prevScaleFactor[1]));
+                            availablePixel = Math.round(Float.valueOf(prevDPI / scaleFactor[1]));
+                        }
+
                         Integer[] gridLineWidth = getGridLineWidth(target.configRegistry);
 
                         try {
@@ -730,15 +771,15 @@
                             final Rectangle printerClientArea = computePrintArea(this.printer);
                             final int printBoundsWidth = Math.round(Float.valueOf(printerClientArea.width / scaleFactor[0]));
                             int repeatPrintTargetHeight = getRepeatPrintTargetHeight();
-                            int repeatPrintTargetHeightInDpi = Math.round(Float.valueOf(repeatPrintTargetHeight * scaleFactor[1]));
+                            int repeatPrintTargetHeightInDpi = Math.round(Float.valueOf(repeatPrintTargetHeight * ((repeatScaleFactor != null) ? repeatScaleFactor[1] : 0)));
                             int headerHeightDPI = Math.round(Float.valueOf((LayerPrinter.this.headerHeight * scaleFactor[1])));
                             int printBoundsHeight = Math.round(Float.valueOf((printerClientArea.height - repeatPrintTargetHeightInDpi - headerHeightDPI - getFooterHeightInPrinterDPI()) / scaleFactor[1]));
 
                             int firstPagePrintBoundsHeight = (available < 0)
                                     ? Math.round(Float.valueOf((printerClientArea.height - repeatPrintTargetHeightInDpi - getFooterHeightInPrinterDPI()) / scaleFactor[1]))
-                                    : available;
+                                    : availablePixel;
 
-                            final int[] pageCount = getPageCount(target, this.printer, available);
+                            final int[] pageCount = getPageCount(target, this.printer, available, prevScaleFactor);
 
                             // Print pages Left to Right and then Top to Down
                             int startY = 0;
@@ -826,7 +867,7 @@
                                         intersect = printBounds.intersection(intersect);
 
                                         configureScalingTransform(printerTransform, scaleFactor, printerClientArea, intersect);
-                                        configureScalingTransform(repeatTransform, scaleFactor, printerClientArea, intersect);
+                                        configureScalingTransform(repeatTransform, (repeatScaleFactor != null ? repeatScaleFactor : scaleFactor), printerClientArea, intersect);
                                         configureScalingTransform(headerTransform, scaleFactor, printerClientArea, intersect);
 
                                         if (repeatPrintTargetHeight > 0) {
@@ -842,11 +883,11 @@
                                             repeatIntersect = printBounds.intersection(repeatIntersect);
 
                                             printLayer(LayerPrinter.this.printTargets.get(0), gc, new Rectangle(repeatIntersect.x, 0, repeatIntersect.width, repeatPrintTargetHeight));
-                                            printerTransform.translate(0, repeatPrintTargetHeight);
+                                            printerTransform.translate(0, Math.round(Float.valueOf(Math.round(Float.valueOf(repeatPrintTargetHeight * repeatScaleFactor[1])) / scaleFactor[1])));
                                         }
 
                                         if (LayerPrinter.this.repeatHeaderLayer != null && verticalPageNumber != 0) {
-                                            headerTransform.translate(0, startY + repeatPrintTargetHeight);
+                                            headerTransform.translate(0, startY + Math.round(Float.valueOf(Math.round(Float.valueOf(repeatPrintTargetHeight * ((repeatScaleFactor != null) ? repeatScaleFactor[1] : 0))) / scaleFactor[1])));
                                             gc.setTransform(headerTransform);
                                             printLayer(target, gc, new Rectangle(printBounds.x, 0, intersect.width, LayerPrinter.this.headerHeight));
                                             printerTransform.translate(0, LayerPrinter.this.headerHeight);
@@ -857,7 +898,7 @@
                                         // page on the same page as the previous
                                         // target
                                         if (LayerPrinter.this.join && available > 0 && verticalPageNumber == 0) {
-                                            printerTransform.translate(0, (printBoundsHeight + LayerPrinter.this.headerHeight) - available);
+                                            printerTransform.translate(0, (printBoundsHeight + LayerPrinter.this.headerHeight) - availablePixel);
                                         }
 
                                         gc.setTransform(printerTransform);
@@ -878,6 +919,7 @@
                                 }
                                 startY += pbh;
                                 available = pageCount[2];
+                                prevScaleFactor = scaleFactor;
                             }
 
                             if (LayerPrinter.this.join && available > 0) {
diff --git a/org.eclipse.nebula.widgets.nattable.core/src/org/eclipse/nebula/widgets/nattable/print/config/PrintConfigAttributes.java b/org.eclipse.nebula.widgets.nattable.core/src/org/eclipse/nebula/widgets/nattable/print/config/PrintConfigAttributes.java
index 2903334..5d3b21d 100644
--- a/org.eclipse.nebula.widgets.nattable.core/src/org/eclipse/nebula/widgets/nattable/print/config/PrintConfigAttributes.java
+++ b/org.eclipse.nebula.widgets.nattable.core/src/org/eclipse/nebula/widgets/nattable/print/config/PrintConfigAttributes.java
@@ -40,6 +40,19 @@
     ConfigAttribute<Direction> FITTING_MODE = new ConfigAttribute<Direction>();
 
     /**
+     * Configuration attribute to configure a scaling option in case
+     * {@link #FITTING_MODE} is set. By default only downscaling is supported on
+     * enabling the fit-to-page scaling. By setting {@link #STRETCH} to
+     * <code>true</code> also upscaling will be performed to make the most out
+     * of the available space.
+     * <p>
+     * <b>Note:</b> stretching is only supported for fitting mode
+     * {@link Direction#HORIZONTAL}.
+     * </p>
+     */
+    ConfigAttribute<Boolean> STRETCH = new ConfigAttribute<Boolean>();
+
+    /**
      * Configuration attribute to configure the date format that is used for
      * rendering the print date in the footer region. If not specified the
      * default value <i>EEE, d MMM yyyy HH:mm a</i> will be used.
diff --git a/org.eclipse.nebula.widgets.nattable.examples/src/org/eclipse/nebula/widgets/nattable/examples/_700_AdditionalFunctions/_762_MultiPrintExample.java b/org.eclipse.nebula.widgets.nattable.examples/src/org/eclipse/nebula/widgets/nattable/examples/_700_AdditionalFunctions/_762_MultiPrintExample.java
index 07f869a..488215e 100644
--- a/org.eclipse.nebula.widgets.nattable.examples/src/org/eclipse/nebula/widgets/nattable/examples/_700_AdditionalFunctions/_762_MultiPrintExample.java
+++ b/org.eclipse.nebula.widgets.nattable.examples/src/org/eclipse/nebula/widgets/nattable/examples/_700_AdditionalFunctions/_762_MultiPrintExample.java
@@ -17,6 +17,8 @@
 import org.eclipse.jface.layout.GridDataFactory;
 import org.eclipse.nebula.widgets.nattable.NatTable;
 import org.eclipse.nebula.widgets.nattable.config.DefaultNatTableStyleConfiguration;
+import org.eclipse.nebula.widgets.nattable.config.Direction;
+import org.eclipse.nebula.widgets.nattable.config.IConfigRegistry;
 import org.eclipse.nebula.widgets.nattable.data.ExtendedReflectiveColumnPropertyAccessor;
 import org.eclipse.nebula.widgets.nattable.data.IColumnPropertyAccessor;
 import org.eclipse.nebula.widgets.nattable.data.IDataProvider;
@@ -39,10 +41,12 @@
 import org.eclipse.nebula.widgets.nattable.hideshow.ColumnHideShowLayer;
 import org.eclipse.nebula.widgets.nattable.layer.DataLayer;
 import org.eclipse.nebula.widgets.nattable.layer.ILayer;
+import org.eclipse.nebula.widgets.nattable.painter.NatTableBorderOverlayPainter;
 import org.eclipse.nebula.widgets.nattable.print.LayerPrinter;
 import org.eclipse.nebula.widgets.nattable.print.command.PrintCommand;
 import org.eclipse.nebula.widgets.nattable.print.command.PrintCommandHandler;
 import org.eclipse.nebula.widgets.nattable.print.config.DefaultPrintBindings;
+import org.eclipse.nebula.widgets.nattable.print.config.PrintConfigAttributes;
 import org.eclipse.nebula.widgets.nattable.reorder.ColumnReorderLayer;
 import org.eclipse.nebula.widgets.nattable.selection.SelectionLayer;
 import org.eclipse.nebula.widgets.nattable.viewport.ViewportLayer;
@@ -50,12 +54,17 @@
 import org.eclipse.swt.events.SelectionAdapter;
 import org.eclipse.swt.events.SelectionEvent;
 import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.layout.RowLayout;
 import org.eclipse.swt.widgets.Button;
 import org.eclipse.swt.widgets.Composite;
 import org.eclipse.swt.widgets.Control;
 
 public class _762_MultiPrintExample extends AbstractNatExample {
 
+    Button joinTablesButton;
+    Button repeatHeaderTableButton;
+    Button repeatColumnHeaderButton;
+
     public static void main(String[] args) throws Exception {
         StandaloneNatExampleRunner.run(new _762_MultiPrintExample());
     }
@@ -79,47 +88,110 @@
         GridDataFactory.fillDefaults().grab(true, true).applyTo(gridPanel);
 
         Composite buttonPanel = new Composite(panel, SWT.NONE);
-        buttonPanel.setLayout(new GridLayout());
+        buttonPanel.setLayout(new GridLayout(4, false));
         GridDataFactory.fillDefaults().grab(true, false).applyTo(buttonPanel);
 
-        NatTable natTable = createSmallTable(gridPanel);
-        NatTable grid = createGrid(gridPanel);
+        NatTable headerTable = createSmallTable(gridPanel);
+        NatTable bodyTable = createGrid(gridPanel);
 
         // create a custom command handler for printing of multiple NatTable
         // instances
-        PrintCommandHandler handler = new PrintCommandHandler(natTable.getLayer()) {
+        PrintCommandHandler handler = new PrintCommandHandler(headerTable.getLayer()) {
             @Override
             public boolean doCommand(PrintCommand command) {
-                LayerPrinter printer = new LayerPrinter(natTable.getLayer(), natTable.getConfigRegistry());
-                printer.addPrintTarget(grid.getLayer(), grid.getConfigRegistry());
-                printer.print(natTable.getShell());
+                LayerPrinter printer = new LayerPrinter(headerTable, headerTable.getConfigRegistry(), _762_MultiPrintExample.this.repeatHeaderTableButton.getSelection());
+                printer.addPrintTarget(bodyTable, bodyTable.getConfigRegistry());
+                printer.joinPrintTargets(_762_MultiPrintExample.this.joinTablesButton.getSelection());
+                if (_762_MultiPrintExample.this.repeatColumnHeaderButton.getSelection()) {
+                    printer.repeatHeaderLayer(((GridLayer) bodyTable.getLayer()).getColumnHeaderLayer());
+                }
+                printer.print(headerTable.getShell());
                 return true;
             };
         };
 
         // register the handler to both NatTable instances
-        natTable.getLayer().registerCommandHandler(handler);
-        grid.getLayer().registerCommandHandler(handler);
+        headerTable.getLayer().registerCommandHandler(handler);
+        bodyTable.getLayer().registerCommandHandler(handler);
+
+        Composite multiTableConfigPanel = new Composite(buttonPanel, SWT.NONE);
+        multiTableConfigPanel.setLayout(new RowLayout(SWT.VERTICAL));
+        this.joinTablesButton = new Button(multiTableConfigPanel, SWT.CHECK);
+        this.joinTablesButton.setText("Join Tables");
+        this.repeatHeaderTableButton = new Button(multiTableConfigPanel, SWT.CHECK);
+        this.repeatHeaderTableButton.setText("Repeat Header Table");
+        this.repeatColumnHeaderButton = new Button(multiTableConfigPanel, SWT.CHECK);
+        this.repeatColumnHeaderButton.setText("Repeat Column Header");
+
+        Composite fittingConfigPanel = new Composite(buttonPanel, SWT.NONE);
+        fittingConfigPanel.setLayout(new RowLayout(SWT.VERTICAL));
+        Button fitHorizontalButton = new Button(fittingConfigPanel, SWT.CHECK);
+        fitHorizontalButton.setText("Fit Horizontally");
+        Button fitVerticalButton = new Button(fittingConfigPanel, SWT.CHECK);
+        fitVerticalButton.setText("Fit Vertically");
+        Button stretchButton = new Button(fittingConfigPanel, SWT.CHECK);
+        stretchButton.setText("Stretch");
+        stretchButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                headerTable.getConfigRegistry().registerConfigAttribute(
+                        PrintConfigAttributes.STRETCH,
+                        stretchButton.getSelection());
+            }
+        });
+
+        fitHorizontalButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                updateFittingConfig(
+                        headerTable.getConfigRegistry(),
+                        fitHorizontalButton.getSelection(),
+                        fitVerticalButton.getSelection());
+            }
+        });
+
+        fitVerticalButton.addSelectionListener(new SelectionAdapter() {
+            @Override
+            public void widgetSelected(SelectionEvent e) {
+                updateFittingConfig(
+                        headerTable.getConfigRegistry(),
+                        fitHorizontalButton.getSelection(),
+                        fitVerticalButton.getSelection());
+            }
+        });
 
         Button addColumnButton = new Button(buttonPanel, SWT.PUSH);
         addColumnButton.setText("Print");
         addColumnButton.addSelectionListener(new SelectionAdapter() {
             @Override
             public void widgetSelected(SelectionEvent e) {
-                natTable.doCommand(
+                headerTable.doCommand(
                         new PrintCommand(
-                                natTable.getConfigRegistry(),
-                                natTable.getShell()));
+                                headerTable.getConfigRegistry(),
+                                headerTable.getShell()));
             }
         });
 
         return panel;
     }
 
+    private void updateFittingConfig(IConfigRegistry configRegistry, boolean fitHorizontally, boolean fitVertically) {
+        Direction dir = Direction.NONE;
+        if (fitHorizontally && fitVertically) {
+            dir = Direction.BOTH;
+        } else if (fitHorizontally && !fitVertically) {
+            dir = Direction.HORIZONTAL;
+        } else if (!fitHorizontally && fitVertically) {
+            dir = Direction.VERTICAL;
+        }
+        configRegistry.registerConfigAttribute(
+                PrintConfigAttributes.FITTING_MODE,
+                dir);
+    }
+
     private NatTable createSmallTable(Composite parent) {
         // property names of the Person class
-        String[] propertyNames = { "firstName", "lastName", "gender",
-                "married", "birthday" };
+        String[] propertyNames = { "firstName", "lastName", "gender", "married", "birthday" };
 
         // mapping from property to label, needed for column header labels
         Map<String, String> propertyToLabelMap = new HashMap<String, String>();
@@ -131,7 +203,7 @@
 
         IDataProvider bodyDataProvider =
                 new DefaultBodyDataProvider<Person>(
-                        PersonService.getPersons(150), propertyNames);
+                        PersonService.getPersons(3), propertyNames);
         DataLayer bodyDataLayer = new DataLayer(bodyDataProvider);
         SelectionLayer selectionLayer = new SelectionLayer(bodyDataLayer);
         ViewportLayer viewportLayer = new ViewportLayer(selectionLayer);
@@ -140,10 +212,21 @@
 
         // adding this configuration adds the styles and the painters to use
         natTable.addConfiguration(new DefaultNatTableStyleConfiguration());
-        natTable.addConfiguration(new DefaultPrintBindings());
+        natTable.addConfiguration(new DefaultPrintBindings() {
+            @Override
+            public void configureRegistry(IConfigRegistry configRegistry) {
+                super.configureRegistry(configRegistry);
+
+                configRegistry.registerConfigAttribute(
+                        PrintConfigAttributes.FOOTER_PAGE_PATTERN,
+                        "Page {0} of {1}");
+            }
+        });
 
         natTable.configure();
 
+        natTable.addOverlayPainter(new NatTableBorderOverlayPainter());
+
         GridDataFactory.fillDefaults().grab(true, true).applyTo(natTable);
 
         return natTable;
@@ -215,6 +298,8 @@
 
         NatTable natTable = new NatTable(parent, gridLayer);
 
+        natTable.addOverlayPainter(new NatTableBorderOverlayPainter());
+
         GridDataFactory.fillDefaults().grab(true, true).applyTo(natTable);
 
         return natTable;