| /*=============================================================================# |
| # Copyright (c) 2012, 2021 Original NatTable authors and others. |
| # |
| # This program and the accompanying materials are made available under the |
| # terms of the Eclipse Public License 2.0 which is available at |
| # https://www.eclipse.org/legal/epl-2.0. |
| # |
| # SPDX-License-Identifier: EPL-2.0 |
| # |
| # Contributors: |
| # Original NatTable authors and others - initial API and implementation |
| #=============================================================================*/ |
| |
| package org.eclipse.statet.ecommons.waltable.print; |
| |
| import java.text.SimpleDateFormat; |
| import java.util.Date; |
| |
| import org.eclipse.swt.SWT; |
| import org.eclipse.swt.graphics.GC; |
| import org.eclipse.swt.graphics.Point; |
| import org.eclipse.swt.graphics.Transform; |
| import org.eclipse.swt.printing.PrintDialog; |
| import org.eclipse.swt.printing.Printer; |
| import org.eclipse.swt.printing.PrinterData; |
| import org.eclipse.swt.widgets.Display; |
| import org.eclipse.swt.widgets.Shell; |
| |
| import org.eclipse.statet.ecommons.waltable.Messages; |
| import org.eclipse.statet.ecommons.waltable.config.IConfigRegistry; |
| import org.eclipse.statet.ecommons.waltable.coordinate.LRectangle; |
| import org.eclipse.statet.ecommons.waltable.layer.ILayer; |
| import org.eclipse.statet.ecommons.waltable.swt.SWTUtil; |
| import org.eclipse.statet.ecommons.waltable.ui.IClientAreaProvider; |
| |
| |
| /** |
| * This class is used to print a layer. |
| * Usually you create an instance by using the top most layer in the layer stack. |
| * For grids this is the GridLayer, otherwise the ViewportLayer is a good choice. |
| */ |
| public class LayerPrinter { |
| |
| private final IConfigRegistry configRegistry; |
| private final ILayer layer; |
| private final IClientAreaProvider originalClientAreaProvider; |
| public static final int FOOTER_HEIGHT_IN_PRINTER_DPI= 300; |
| |
| final SimpleDateFormat dateFormat= new SimpleDateFormat("EEE, d MMM yyyy HH:mm a"); //$NON-NLS-1$ |
| private final String footerDate; |
| |
| /** |
| * |
| * @param layer The layer to print. Usually the top most layer in the layer stack. |
| * For grids this should be the GridLayer, for custom CompositeLayer compositions |
| * the CompositeLayer, otherwise the ViewportLayer is a good choice. |
| * @param configRegistry The ConfigRegistry needed for rendering to the print GC. |
| */ |
| public LayerPrinter(final ILayer layer, final IConfigRegistry configRegistry) { |
| this.layer= layer; |
| this.configRegistry= configRegistry; |
| this.originalClientAreaProvider= layer.getClientAreaProvider(); |
| this.footerDate= this.dateFormat.format(new Date()); |
| } |
| |
| /** |
| * Computes the scale factor to match the printer resolution. |
| * @param printer The printer that will be used. |
| * @return The amount to scale the screen resolution by, to match the |
| * printer the resolution. |
| */ |
| private Point computeScaleFactor(final Printer printer) { |
| final Point screenDPI= Display.getDefault().getDPI(); |
| final Point printerDPI= printer.getDPI(); |
| |
| final int scaleFactorX= printerDPI.x / screenDPI.x; |
| final int scaleFactorY= printerDPI.y / screenDPI.y; |
| return new Point(scaleFactorX, scaleFactorY); |
| } |
| |
| /** |
| * @return The size of the layer to fit all the contents. |
| */ |
| private LRectangle getTotalArea() { |
| return new LRectangle(0, 0, this.layer.getWidth(), this.layer.getHeight()); |
| } |
| |
| /** |
| * Calculates number of horizontal and vertical pages needed |
| * to print the entire layer. |
| * @param printer The printer that will be used. |
| * @return The number of horizontal and vertical pages that are |
| * needed to print the layer. |
| */ |
| private Point getPageCount(final Printer printer){ |
| final LRectangle layerArea= getTotalArea(); |
| final LRectangle printArea= computePrintArea(printer); |
| final Point scaleFactor= computeScaleFactor(printer); |
| |
| final int numOfHorizontalPages= (int) (layerArea.width / (printArea.width / scaleFactor.x)); |
| final int numOfVerticalPages= (int) (layerArea.height / (printArea.height / scaleFactor.y)); |
| |
| // Adjusting for 0 index |
| return new Point(numOfHorizontalPages + 1, numOfVerticalPages + 1); |
| } |
| |
| /** |
| * Will first open the PrintDialog to let a user configure the print job |
| * and then starts the print job. |
| * @param shell The shell which should be the parent of the PrintDialog. |
| */ |
| public void print(final Shell shell) { |
| //turn viewport off to ensure calculation of the print pages for the whole table |
| this.layer.doCommand(new TurnViewportOffCommand()); |
| |
| final Printer printer= setupPrinter(shell); |
| if (printer == null) { |
| return; |
| } |
| |
| //turn viewport on |
| this.layer.doCommand(new TurnViewportOnCommand()); |
| |
| //Note: As we are operating on the same layer instance that is shown in the UI |
| // executing the print job asynchronously will not cause a real asynchronous |
| // execution. The UI will hang until the print job is done, because we access |
| // the information to print from the same instance. |
| // For further developments we need to ensure that for printing a deep copy |
| // of the layer needs to be performed instead of operating on the same instance. |
| Display.getDefault().asyncExec(new PrintJob(printer)); |
| } |
| |
| /** |
| * Checks if a given page number should be printed. |
| * Page is allowed to print if: |
| * User asked to print all pages or Page in a specified range |
| * @param printerData The printer settings made by the user. Needed to determine |
| * if a page should be printed dependent to the scope |
| * @param currentPage The page that should be checked |
| * @return <code>true</code> if the given page should be printed, |
| * <code>false</code> if not |
| */ |
| private boolean shouldPrint(final PrinterData printerData, final int totalPageCount) { |
| if (printerData.scope == PrinterData.PAGE_RANGE) { |
| return totalPageCount >= printerData.startPage && totalPageCount <= printerData.endPage; |
| } |
| return true; |
| } |
| |
| /** |
| * Opens the PrintDialog to let the user specify the printer and print configurations to use. |
| * @param shell The Shell which should be the parent for the PrintDialog |
| * @return The selected printer with the print configuration made by the user. |
| */ |
| private Printer setupPrinter(final Shell shell) { |
| final Printer defaultPrinter= new Printer(); |
| final Point pageCount= getPageCount(defaultPrinter); |
| defaultPrinter.dispose(); |
| |
| final PrintDialog printDialog= new PrintDialog(shell); |
| printDialog.setStartPage(1); |
| printDialog.setEndPage(pageCount.x * pageCount.y); |
| printDialog.setScope(PrinterData.ALL_PAGES); |
| |
| final PrinterData printerData= printDialog.open(); |
| if (printerData == null){ |
| return null; |
| } |
| return new Printer(printerData); |
| } |
| |
| /** |
| * Computes the print area, including margins |
| * @param printer The printer that will be used. |
| * @return The print area that will be used to render the table. |
| */ |
| private LRectangle computePrintArea(final Printer printer) { |
| // Get the printable area |
| final org.eclipse.swt.graphics.Rectangle rect= printer.getClientArea(); |
| |
| // Compute the trim |
| final org.eclipse.swt.graphics.Rectangle trim= printer.computeTrim(0, 0, 0, 0); |
| |
| // Get the printer's DPI |
| final Point dpi= printer.getDPI(); |
| dpi.x= dpi.x / 2; |
| dpi.y= dpi.y / 2; |
| |
| // Calculate the printable area, using 1 inch margins |
| int left= trim.x + dpi.x; |
| if (left < rect.x) { |
| left= rect.x; |
| } |
| |
| int right= (rect.width + trim.x + trim.width) - dpi.x; |
| if (right > rect.width) { |
| right= rect.width; |
| } |
| |
| int top= trim.y + dpi.y; |
| if (top < rect.y) { |
| top= rect.y; |
| } |
| |
| int bottom= (rect.height + trim.y + trim.height) - dpi.y; |
| if (bottom > rect.height) { |
| bottom= rect.height; |
| } |
| |
| return new LRectangle(left, top, right - left, bottom - top); |
| } |
| |
| |
| /** |
| * The job for printing the layer. |
| */ |
| private class PrintJob implements Runnable { |
| /** |
| * The printer that will be used. |
| */ |
| private final Printer printer; |
| |
| /** |
| * @param printer The printer that will be used. |
| */ |
| private PrintJob(final Printer printer) { |
| this.printer= printer; |
| } |
| |
| @Override |
| public void run() { |
| if (this.printer.startJob("NatTable")) { //$NON-NLS-1$ |
| //if a SummaryRowLayer is in the layer stack, we need to ensure that the values are calculated |
| // LayerPrinter.this.layer.doCommand(new CalculateSummaryRowValuesCommand()); |
| |
| //ensure that the viewport is turned off |
| LayerPrinter.this.layer.doCommand(new TurnViewportOffCommand()); |
| |
| //set the size of the layer according to the print setttings made by the user |
| setLayerSize(this.printer.getPrinterData()); |
| |
| final LRectangle printerClientArea= computePrintArea(this.printer); |
| final Point scaleFactor= computeScaleFactor(this.printer); |
| final Point pageCount= getPageCount(this.printer); |
| final GC gc= new GC(this.printer); |
| |
| // Print pages Left to Right and then Top to Down |
| int currentPage= 1; |
| for (int verticalPageNumber= 0; verticalPageNumber < pageCount.y; verticalPageNumber++) { |
| |
| for (int horizontalPageNumber= 0; horizontalPageNumber < pageCount.x; horizontalPageNumber++) { |
| |
| // Calculate bounds for the next page |
| final org.eclipse.swt.graphics.Rectangle printBounds= SWTUtil.toSWT(new LRectangle( |
| (printerClientArea.width / scaleFactor.x) * horizontalPageNumber, |
| ((printerClientArea.height - FOOTER_HEIGHT_IN_PRINTER_DPI) / scaleFactor.y) * verticalPageNumber, |
| printerClientArea.width / scaleFactor.x, |
| (printerClientArea.height - FOOTER_HEIGHT_IN_PRINTER_DPI) / scaleFactor.y)); |
| |
| if (shouldPrint(this.printer.getPrinterData(), currentPage)) { |
| this.printer.startPage(); |
| |
| final Transform printerTransform= new Transform(this.printer); |
| |
| // Adjust for DPI difference between display and printer |
| printerTransform.scale(scaleFactor.x, scaleFactor.y); |
| |
| // Adjust for margins |
| printerTransform.translate(printerClientArea.x / scaleFactor.x, printerClientArea.y / scaleFactor.y); |
| |
| // Grid will not automatically print the pages at the left margin. |
| // Example: page 1 will print at x= 0, page 2 at x= 100, page 3 at x= 300 |
| // Adjust to print from the left page margin. i.e x= 0 |
| printerTransform.translate(-1 * printBounds.x, -1 * printBounds.y); |
| gc.setTransform(printerTransform); |
| |
| printLayer(gc, printBounds); |
| |
| printFooter(gc, currentPage, printBounds); |
| |
| this.printer.endPage(); |
| printerTransform.dispose(); |
| } |
| currentPage++; |
| } |
| } |
| |
| this.printer.endJob(); |
| |
| gc.dispose(); |
| this.printer.dispose(); |
| } |
| restoreLayerState(); |
| } |
| |
| /** |
| * Set the client area of the layer so it matches the print settings made by the user. |
| * In case a user selected to print everything, the size needs to be extended so that |
| * all the contents fit in the viewport to ensure that we print the <i>entire</i> table. |
| * @param printerData The PrinterData that was configured by the user on the PrintDialog. |
| */ |
| private void setLayerSize(final PrinterData printerData) { |
| if (printerData.scope == PrinterData.SELECTION) { |
| LayerPrinter.this.layer.setClientAreaProvider(LayerPrinter.this.originalClientAreaProvider); |
| } |
| else { |
| final LRectangle fullLayerSize= getTotalArea(); |
| |
| LayerPrinter.this.layer.setClientAreaProvider(new IClientAreaProvider(){ |
| @Override |
| public LRectangle getClientArea() { |
| return fullLayerSize; |
| } |
| }); |
| |
| //in case the whole layer should be printed or only the selected pages, |
| //we need to ensure to set the starting point to 0/0 |
| LayerPrinter.this.layer.doCommand(new PrintEntireGridCommand()); |
| } |
| } |
| |
| /** |
| * Print the part of the layer that matches the given print bounds. |
| * @param gc The print GC to render the layer to. |
| * @param printBounds The bounds of the print page. |
| */ |
| private void printLayer(final GC gc, final org.eclipse.swt.graphics.Rectangle printBounds) { |
| LayerPrinter.this.layer.getLayerPainter().paintLayer(LayerPrinter.this.layer, gc, 0, 0, printBounds, LayerPrinter.this.configRegistry); |
| } |
| |
| /** |
| * Print the footer to the page. |
| * @param gc The print GC to render the footer to. |
| * @param totalPageCount The total number of pages that are printed. |
| * @param printBounds The bounds of the print page. |
| */ |
| private void printFooter(final GC gc, final int totalPageCount, final org.eclipse.swt.graphics.Rectangle printBounds) { |
| gc.setForeground(Display.getCurrent().getSystemColor(SWT.COLOR_BLACK)); |
| gc.setBackground(Display.getCurrent().getSystemColor(SWT.COLOR_WHITE)); |
| |
| gc.drawLine(printBounds.x, |
| printBounds.y + printBounds.height+10, |
| printBounds.x + printBounds.width, |
| printBounds.y + printBounds.height+10); |
| |
| gc.drawText(Messages.getString("Printer.page") + " " + totalPageCount, //$NON-NLS-1$ //$NON-NLS-2$ |
| printBounds.x, |
| printBounds.y + printBounds.height + 15); |
| |
| // Approximate width of the date string: 140 |
| gc.drawText(LayerPrinter.this.footerDate, |
| printBounds.x + printBounds.width - 140, |
| printBounds.y + printBounds.height + 15); |
| } |
| |
| /** |
| * Restores the layer state to match the display characteristics again. |
| * This is done by resetting the client area provider and turning the viewport |
| * on again. |
| */ |
| private void restoreLayerState() { |
| LayerPrinter.this.layer.setClientAreaProvider(LayerPrinter.this.originalClientAreaProvider); |
| LayerPrinter.this.layer.doCommand(new TurnViewportOnCommand()); |
| } |
| |
| } |
| |
| } |