Bug 505660 - [Print] Extended multi table print support

Change-Id: Ia17c27f9bc7d0782fd65d36fe0780cc6ee066caf
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 985ca4e..a2c80a0 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
@@ -88,6 +88,8 @@
 
     private ILayer repeatHeaderLayer;
 
+    private boolean join = false;
+
     /**
      *
      * @param layer
@@ -163,6 +165,21 @@
     }
 
     /**
+     * Configure whether multiple print targets should be joined for printing or
+     * not.
+     *
+     * @param join
+     *            <code>true</code> if the print targets should be printed
+     *            consecutively, <code>false</code> if every print target should
+     *            start with a new page. Default is <code>false</code>.
+     *
+     * @since 1.5
+     */
+    public void joinPrintTargets(boolean join) {
+        this.join = join;
+    }
+
+    /**
      * Configure printing to repeat the header layer on every print page. For
      * this it is necessary to specify the header layer that should be repeated
      * in order to calculate the necessary header height on printing.
@@ -187,6 +204,23 @@
     }
 
     /**
+     *
+     * @return The height of the print target that should be repeated on every
+     *         page. Typically only the first registered one in case of multiple
+     *         print targets. If there is only one print target or no print
+     *         target should be repeated it returns 0.
+     */
+    private int getRepeatPrintTargetHeight() {
+        int result = 0;
+        for (PrintTarget target : this.printTargets) {
+            if (target.repeat) {
+                result += target.layer.getHeight();
+            }
+        }
+        return result;
+    }
+
+    /**
      * Computes the scale factor to match the printer resolution.
      *
      * @param layer
@@ -210,13 +244,29 @@
 
         if (!dpi && (this.fittingMode != Direction.NONE)) {
             Rectangle total = getTotalArea(layer);
-            // TODO this is for the additional layer
-            // total.height += this.headerHeight;
+            if (this.join || this.printTargets.get(0).repeat) {
+                // calculate the total height of all targets
+                total.height = 0;
+                for (PrintTarget target : this.printTargets) {
+                    total.height += target.layer.getHeight();
+                }
+            }
             Rectangle print = computePrintArea(printer);
 
             float pixelX = Float.valueOf(print.width) / Float.valueOf(total.width);
             float pixelY = (Float.valueOf(print.height) - getFooterHeightInPrinterDPI()) / Float.valueOf(total.height);
 
+            // only support down-scaling, no stretching
+            // 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) {
+                pixelX = sfX;
+            }
+            if (pixelY > sfY) {
+                pixelY = sfY;
+            }
+
             switch (this.fittingMode) {
                 case HORIZONTAL:
                     return new float[] { pixelX, pixelX };
@@ -231,6 +281,38 @@
     }
 
     /**
+     * Computes the scale factor to match the printer resolution. In case of
+     * fitting mode configurations and multiple print targets, the common
+     * scaling factor will be calculated and returned.
+     *
+     * @param layer
+     *            The layer for which the scale factor should be calculated.
+     * @param printer
+     *            The printer that will be used.
+     * @return The amount to scale the screen resolution by, to match the
+     *         printer the resolution.
+     */
+    private float[] computeLayerScaleFactor(ILayer layer, Printer printer) {
+        float[] scaleFactor = null;
+        if (this.fittingMode == Direction.NONE
+                || (!this.join && !this.printTargets.get(0).repeat)) {
+            scaleFactor = computeScaleFactor(layer, printer, false);
+        } else {
+            // search for the common scaling factor
+            for (PrintTarget tempTarget : this.printTargets) {
+                float[] tempFactor = computeScaleFactor(tempTarget.layer, printer, false);
+                if (scaleFactor == null) {
+                    scaleFactor = tempFactor;
+                } else {
+                    scaleFactor[0] = Math.min(scaleFactor[0], tempFactor[0]);
+                    scaleFactor[1] = Math.min(scaleFactor[1], tempFactor[1]);
+                }
+            }
+        }
+        return scaleFactor;
+    }
+
+    /**
      * @param layer
      *            The layer for which the total area is requested.
      * @return The size of the layer to fit all the contents.
@@ -249,10 +331,22 @@
      */
     private int getPageCount(Printer printer) {
         int result = 0;
+        int available = -1;
         for (PrintTarget target : this.printTargets) {
-            Point layerResult = getPageCount(target, printer);
-            result += (layerResult.x * layerResult.y);
+            if (!target.repeat) {
+                int[] layerResult = getPageCount(target, printer, available);
+                result += (layerResult[0] * layerResult[1]);
+
+                // as the print targets should be joined and the print was
+                // started on an existing page, we need to reduce the page count
+                if (this.join && available > 0) {
+                    result--;
+                }
+
+                available = layerResult[2];
+            }
         }
+
         return result;
     }
 
@@ -264,12 +358,17 @@
      *            The print target to print.
      * @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.
      * @return The number of horizontal and vertical pages that are needed to
-     *         print the layer of the given print target.
+     *         print the layer of the given print target and the remaining space
+     *         on a page if the print targets should be glued.
      */
-    private Point getPageCount(PrintTarget target, Printer printer) {
+    private int[] getPageCount(PrintTarget target, Printer printer, int available) {
         Rectangle printArea = computePrintArea(printer);
-        float[] scaleFactor = computeScaleFactor(target.layer, printer, false);
+        float[] scaleFactor = computeLayerScaleFactor(target.layer, printer);
 
         Integer[] gridLineWidth = getGridLineWidth(target.configRegistry);
 
@@ -297,13 +396,23 @@
         }
 
         int numOfVerticalPages = 0;
-        // TODO
+        int repeatPrintTargetHeightInDpi = Math.round(Float.valueOf(getRepeatPrintTargetHeight() * scaleFactor[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 - headerHeightInDpi - getFooterHeightInPrinterDPI()) / scaleFactor[1]));
+        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;
         int endY = 0;
+        int added = 0;
+        int remaining = -1;
         while (endY < (target.layer.getHeight() - this.headerHeight)) {
-            endY += pageHeight;
+            // on the first page we don't need to take care of the repeat
+            // header height
+            added = (numOfVerticalPages == 0) ? firstPageHeight : pageHeight;
+            endY += added;
+
             int rowPos = target.layer.getRowPositionByY(endY);
             if (rowPos >= 0) {
                 ILayerCell cell = findRowCellForBounds(target.layer, rowPos);
@@ -315,6 +424,11 @@
                     }
                 }
             } else {
+                // in case the print targets should be glued we need to
+                // calculate the remaining space for the following print target
+                if (this.join) {
+                    remaining = ((numOfVerticalPages == 0) ? firstPageHeight : pageHeight) - (target.layer.getHeight() - (endY - added));
+                }
                 endY = target.layer.getHeight();
             }
 
@@ -325,7 +439,7 @@
             target.configRegistry.unregisterConfigAttribute(CellConfigAttributes.GRID_LINE_WIDTH);
         }
 
-        return new Point(numOfHorizontalPages, numOfVerticalPages);
+        return new int[] { numOfHorizontalPages, numOfVerticalPages, remaining };
     }
 
     private ILayerCell findColumnCellForBounds(ILayer layer, int colPos) {
@@ -547,30 +661,48 @@
         public void run() {
             if (this.printer.startJob("NatTable")) { //$NON-NLS-1$
                 GC gc = new GC(this.printer);
+
+                // turn the viewport for all targets off to ensure everything is
+                // taken into account for calculation and printing
+                for (PrintTarget target : LayerPrinter.this.printTargets) {
+                    target.layer.doCommand(new TurnViewportOffCommand());
+                }
+
                 try {
                     int currentPage = 1;
 
+                    Integer[] repeatHeaderGridLineWidth = null;
+
+                    int available = -1;
+                    boolean newPage = true;
+                    boolean pageStarted = false;
                     for (PrintTarget target : LayerPrinter.this.printTargets) {
+                        if (target.repeat) {
+                            // we do not render the repeat print target directly
+                            // as it is handled on every page while printing
+                            repeatHeaderGridLineWidth = getGridLineWidth(target.configRegistry);
+                            continue;
+                        }
+
                         // if pre-rendering is enabled, render in-memory to
                         // trigger content based auto-resizing
                         if (LayerPrinter.this.preRender) {
-                            AutoResizeHelper.autoResize(target.layer, target.configRegistry);
+                            try {
+                                AutoResizeHelper.autoResize(target.layer, target.configRegistry);
+                            } finally {
+                                // the AutoResizeHelper also toggles the
+                                // viewport state we need to ensure that it is
+                                // still turned off afterwards
+                                target.layer.doCommand(new TurnViewportOffCommand());
+                            }
 
-                            // TODO
                             if (LayerPrinter.this.repeatHeaderLayer != null) {
                                 LayerPrinter.this.headerHeight = LayerPrinter.this.repeatHeaderLayer.getHeight();
                             }
                         }
 
-                        target.layer.doCommand(new TurnViewportOffCommand());
-                        float[] scaleFactor = null;
-                        float[] dpiFactor = null;
-                        try {
-                            scaleFactor = computeScaleFactor(target.layer, this.printer, false);
-                            dpiFactor = computeScaleFactor(target.layer, this.printer, true);
-                        } finally {
-                            target.layer.doCommand(new TurnViewportOnCommand());
-                        }
+                        float[] scaleFactor = computeLayerScaleFactor(target.layer, this.printer);
+                        float[] dpiFactor = computeScaleFactor(target.layer, this.printer, true);
 
                         Integer[] gridLineWidth = getGridLineWidth(target.configRegistry);
 
@@ -579,9 +711,6 @@
                             // need to ensure that the values are calculated
                             target.layer.doCommand(new CalculateSummaryRowValuesCommand());
 
-                            // ensure that the viewport is turned off
-                            target.layer.doCommand(new TurnViewportOffCommand());
-
                             // ensure that formula processing is performed in
                             // the current thread
                             target.layer.doCommand(new DisableFormulaCachingCommand());
@@ -592,18 +721,26 @@
 
                             final Rectangle printerClientArea = computePrintArea(this.printer);
                             final int printBoundsWidth = Math.round(Float.valueOf(printerClientArea.width / scaleFactor[0]));
-                            // TODO
+                            int repeatPrintTargetHeight = getRepeatPrintTargetHeight();
+                            int repeatPrintTargetHeightInDpi = Math.round(Float.valueOf(repeatPrintTargetHeight * scaleFactor[1]));
                             int headerHeightDPI = Math.round(Float.valueOf((LayerPrinter.this.headerHeight * scaleFactor[1])));
-                            int printBoundsHeight = Math.round(Float.valueOf((printerClientArea.height - headerHeightDPI - getFooterHeightInPrinterDPI()) / scaleFactor[1]));
+                            int printBoundsHeight = Math.round(Float.valueOf((printerClientArea.height - repeatPrintTargetHeightInDpi - headerHeightDPI - getFooterHeightInPrinterDPI()) / scaleFactor[1]));
 
-                            final Point pageCount = getPageCount(target, this.printer);
+                            int firstPagePrintBoundsHeight = (available < 0)
+                                    ? Math.round(Float.valueOf((printerClientArea.height - repeatPrintTargetHeightInDpi - getFooterHeightInPrinterDPI()) / scaleFactor[1]))
+                                    : available;
+
+                            final int[] pageCount = getPageCount(target, this.printer, available);
 
                             // Print pages Left to Right and then Top to Down
-                            // TODO
-                            int startY = LayerPrinter.this.headerHeight;
-                            for (int verticalPageNumber = 0; verticalPageNumber < pageCount.y; verticalPageNumber++) {
+                            int startY = 0;
+                            for (int verticalPageNumber = 0; verticalPageNumber < pageCount[1]; verticalPageNumber++) {
 
-                                int endY = startY + printBoundsHeight;
+                                // on the first page we don't need to take care
+                                // of the repeat header height
+                                int pbh = (verticalPageNumber == 0 ? firstPagePrintBoundsHeight : printBoundsHeight);
+
+                                int endY = startY + pbh;
                                 // int rowPos =
                                 // target.layer.getRowPositionByY(endY);
                                 int rowPos = target.layer.getRowPositionByY(endY);
@@ -612,20 +749,20 @@
                                     if (cell != null) {
                                         Rectangle cellBounds = cell.getBounds();
                                         if (cellBounds.y < endY) {
-                                            printBoundsHeight -= (endY - cellBounds.y);
+                                            pbh -= (endY - cellBounds.y);
                                         }
                                     }
                                 }
 
                                 int startX = 0;
-                                for (int horizontalPageNumber = 0; horizontalPageNumber < pageCount.x; horizontalPageNumber++) {
+                                for (int horizontalPageNumber = 0; horizontalPageNumber < pageCount[0]; horizontalPageNumber++) {
 
                                     // Calculate bounds for the next page
                                     Rectangle printBounds = new Rectangle(
                                             startX,
                                             startY,
                                             printBoundsWidth,
-                                            printBoundsHeight);
+                                            pbh);
 
                                     int endX = startX + printBounds.width;
                                     int colPos = target.layer.getColumnPositionByX(endX);
@@ -646,9 +783,29 @@
                                             Math.round(Float.valueOf((printerClientArea.height - getFooterHeightInPrinterDPI()) / dpiFactor[1])));
 
                                     if (shouldPrint(this.printer.getPrinterData(), currentPage)) {
-                                        this.printer.startPage();
+                                        // end a page that was previously
+                                        // started
+                                        if (pageStarted) {
+                                            this.printer.endPage();
+                                            newPage = true;
+                                        }
+
+                                        // start a new page
+                                        if (newPage) {
+                                            this.printer.startPage();
+                                            pageStarted = true;
+                                            newPage = false;
+                                        }
+
+                                        // ensure there is a next page
+                                        // will be set to false afterwards again
+                                        // if multiple targets fit on one page
+                                        if (!pageStarted && !newPage) {
+                                            pageStarted = true;
+                                        }
 
                                         Transform printerTransform = new Transform(this.printer);
+                                        Transform repeatTransform = new Transform(this.printer);
                                         Transform headerTransform = new Transform(this.printer);
                                         Transform footerTransform = new Transform(this.printer);
 
@@ -661,16 +818,40 @@
                                         intersect = printBounds.intersection(intersect);
 
                                         configureScalingTransform(printerTransform, scaleFactor, printerClientArea, intersect);
+                                        configureScalingTransform(repeatTransform, scaleFactor, printerClientArea, intersect);
                                         configureScalingTransform(headerTransform, scaleFactor, printerClientArea, intersect);
 
-                                        // TODO
-                                        if (LayerPrinter.this.repeatHeaderLayer != null) {
-                                            headerTransform.translate(0, startY);
+                                        if (repeatPrintTargetHeight > 0) {
+                                            repeatTransform.translate(0, startY);
+                                            gc.setTransform(repeatTransform);
+
+                                            Rectangle repeatIntersect = new Rectangle(
+                                                    0,
+                                                    0,
+                                                    LayerPrinter.this.printTargets.get(0).layer.getWidth(),
+                                                    LayerPrinter.this.printTargets.get(0).layer.getHeight());
+
+                                            repeatIntersect = printBounds.intersection(repeatIntersect);
+
+                                            printLayer(LayerPrinter.this.printTargets.get(0), gc, new Rectangle(repeatIntersect.x, 0, repeatIntersect.width, repeatPrintTargetHeight));
+                                            printerTransform.translate(0, repeatPrintTargetHeight);
+                                        }
+
+                                        if (LayerPrinter.this.repeatHeaderLayer != null && verticalPageNumber != 0) {
+                                            headerTransform.translate(0, startY + repeatPrintTargetHeight);
                                             gc.setTransform(headerTransform);
-                                            printLayer(target, gc, new Rectangle(printBounds.x, 0, printBounds.width, LayerPrinter.this.headerHeight));
+                                            printLayer(target, gc, new Rectangle(printBounds.x, 0, intersect.width, LayerPrinter.this.headerHeight));
                                             printerTransform.translate(0, LayerPrinter.this.headerHeight);
                                         }
 
+                                        // on joining print targets we need to
+                                        // transform for rendering the first
+                                        // 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);
+                                        }
+
                                         gc.setTransform(printerTransform);
                                         printLayer(target, gc, intersect);
 
@@ -678,15 +859,33 @@
                                         gc.setTransform(footerTransform);
                                         printFooter(gc, currentPage, footerBounds, target.configRegistry);
 
-                                        this.printer.endPage();
                                         printerTransform.dispose();
+                                        repeatTransform.dispose();
+                                        headerTransform.dispose();
                                         footerTransform.dispose();
                                     }
                                     currentPage++;
 
                                     startX += printBounds.width;
                                 }
-                                startY += printBoundsHeight;
+                                startY += pbh;
+                                available = pageCount[2];
+                            }
+
+                            if (LayerPrinter.this.join && available > 0) {
+                                currentPage--;
+                            }
+
+                            // after last page is printed, check if the page
+                            // needs to end or if it should be used for the next
+                            // print target
+                            if (!LayerPrinter.this.join || available < 0) {
+                                this.printer.endPage();
+                                newPage = true;
+                                pageStarted = false;
+                            } else if (LayerPrinter.this.join && available > 0) {
+                                newPage = false;
+                                pageStarted = false;
                             }
 
                         } finally {
@@ -700,10 +899,19 @@
                             target.configRegistry.unregisterConfigAttribute(CellConfigAttributes.GRID_LINE_WIDTH);
                         }
                     }
+
+                    if (repeatHeaderGridLineWidth != null && repeatHeaderGridLineWidth[0] == null) {
+                        LayerPrinter.this.printTargets.get(0).configRegistry.unregisterConfigAttribute(CellConfigAttributes.GRID_LINE_WIDTH);
+                    }
                 } finally {
                     this.printer.endJob();
                     gc.dispose();
                     this.printer.dispose();
+
+                    // turn viewport on
+                    for (PrintTarget target : LayerPrinter.this.printTargets) {
+                        target.layer.doCommand(new TurnViewportOnCommand());
+                    }
                 }
             }
         }
@@ -853,7 +1061,6 @@
          */
         private void restoreLayerState(PrintTarget target) {
             target.layer.setClientAreaProvider(target.originalClientAreaProvider);
-            target.layer.doCommand(new TurnViewportOnCommand());
             target.layer.doCommand(new EnableFormulaCachingCommand());
         }