blob: f08f9e7f9675557097444e1991b7b5bbf94d1e42 [file] [log] [blame]
/*=============================================================================#
# 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());
}
}
}