blob: 8b0c548a0f1232cdf7af76a24aec7e1898508775 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2012, 2016 Original authors and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Original authors and others - initial API and implementation
******************************************************************************/
package org.eclipse.nebula.widgets.nattable.print;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import org.eclipse.nebula.widgets.nattable.Messages;
import org.eclipse.nebula.widgets.nattable.config.CellConfigAttributes;
import org.eclipse.nebula.widgets.nattable.config.Direction;
import org.eclipse.nebula.widgets.nattable.config.IConfigRegistry;
import org.eclipse.nebula.widgets.nattable.formula.command.DisableFormulaCachingCommand;
import org.eclipse.nebula.widgets.nattable.formula.command.EnableFormulaCachingCommand;
import org.eclipse.nebula.widgets.nattable.layer.ILayer;
import org.eclipse.nebula.widgets.nattable.layer.cell.ILayerCell;
import org.eclipse.nebula.widgets.nattable.print.command.PrintEntireGridCommand;
import org.eclipse.nebula.widgets.nattable.print.command.TurnViewportOffCommand;
import org.eclipse.nebula.widgets.nattable.print.command.TurnViewportOnCommand;
import org.eclipse.nebula.widgets.nattable.print.config.PrintConfigAttributes;
import org.eclipse.nebula.widgets.nattable.resize.AutoResizeHelper;
import org.eclipse.nebula.widgets.nattable.style.CellStyleAttributes;
import org.eclipse.nebula.widgets.nattable.style.DisplayMode;
import org.eclipse.nebula.widgets.nattable.style.IStyle;
import org.eclipse.nebula.widgets.nattable.summaryrow.command.CalculateSummaryRowValuesCommand;
import org.eclipse.nebula.widgets.nattable.util.GUIHelper;
import org.eclipse.nebula.widgets.nattable.util.IClientAreaProvider;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
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;
/**
* 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 class PrintTarget {
private final IConfigRegistry configRegistry;
private final ILayer layer;
private final IClientAreaProvider originalClientAreaProvider;
PrintTarget(ILayer layer, IConfigRegistry configRegistry) {
this.layer = layer;
this.configRegistry = configRegistry;
this.originalClientAreaProvider = layer.getClientAreaProvider();
}
}
private final List<PrintTarget> printTargets = new ArrayList<PrintTarget>();
public static final int FOOTER_HEIGHT_IN_PRINTER_DPI = 300;
final SimpleDateFormat dateFormat;
private final String footerDate;
private final int footerHeight;
/**
* @since 1.4
*/
protected boolean preRender = true;
private final Direction fittingMode;
/**
*
* @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(ILayer layer, IConfigRegistry configRegistry) {
this.printTargets.add(new PrintTarget(layer, configRegistry));
// configure the footer height
Integer fh = configRegistry.getConfigAttribute(
PrintConfigAttributes.FOOTER_HEIGHT,
DisplayMode.NORMAL);
this.footerHeight = (fh != null) ? fh : FOOTER_HEIGHT_IN_PRINTER_DPI;
// configure the footer date
String configuredFormat = configRegistry.getConfigAttribute(
PrintConfigAttributes.DATE_FORMAT,
DisplayMode.NORMAL);
if (configuredFormat != null) {
this.dateFormat = new SimpleDateFormat(configuredFormat);
} else {
this.dateFormat = new SimpleDateFormat("EEE, d MMM yyyy HH:mm a"); //$NON-NLS-1$
}
this.footerDate = this.dateFormat.format(new Date());
// configure the fitting mode
Direction configuredFittingMode = configRegistry.getConfigAttribute(
PrintConfigAttributes.FITTING_MODE,
DisplayMode.NORMAL);
this.fittingMode = (configuredFittingMode != null) ? configuredFittingMode : Direction.NONE;
}
/**
* Adds the given {@link ILayer} as print target. It can be used to register
* multiple NatTable or layer stacks in one print job.
*
* @param layer
* The {@link ILayer} that should be printed together with the
* main {@link ILayer} registered via constructor.
* @param configRegistry
* The {@link IConfigRegistry} that should be used to print the
* given {@link ILayer}.
*
* @since 1.5
*/
public void addPrintTarget(ILayer layer, IConfigRegistry configRegistry) {
this.printTargets.add(new PrintTarget(layer, configRegistry));
}
/**
* Computes the scale factor to match the printer resolution.
*
* @param layer
* The layer for which the scale factor should be calculated.
* @param printer
* The printer that will be used.
* @param dpi
* <code>true</code> if in any case the dpi scaling factor should
* be returned, <code>false</code> if the calculation properties
* need to be checked whether the scaling factor to match a page
* should be returned
* @return The amount to scale the screen resolution by, to match the
* printer the resolution.
*/
private float[] computeScaleFactor(ILayer layer, Printer printer, boolean dpi) {
Point screenDPI = Display.getDefault().getDPI();
Point printerDPI = printer.getDPI();
float sfX = Float.valueOf(printerDPI.x) / Float.valueOf(screenDPI.x);
float sfY = Float.valueOf(printerDPI.y) / Float.valueOf(screenDPI.y);
if (!dpi && (this.fittingMode != Direction.NONE)) {
Rectangle total = getTotalArea(layer);
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);
switch (this.fittingMode) {
case HORIZONTAL:
return new float[] { pixelX, pixelX };
case VERTICAL:
return new float[] { pixelY, pixelY };
case BOTH:
return new float[] { pixelX, pixelY };
}
}
return new float[] { sfX, sfY };
}
/**
* @param layer
* The layer for which the total area is requested.
* @return The size of the layer to fit all the contents.
*/
private Rectangle getTotalArea(ILayer layer) {
return new Rectangle(0, 0, layer.getWidth(), layer.getHeight());
}
/**
* Calculates number of horizontal and vertical pages needed to print all
* registered layers.
*
* @param printer
* The printer that will be used.
* @return The number of pages that are needed to print.
*/
private int getPageCount(Printer printer) {
int result = 0;
for (PrintTarget target : this.printTargets) {
Point layerResult = getPageCount(target, printer);
result += (layerResult.x * layerResult.y);
}
return result;
}
/**
* Calculates number of horizontal and vertical pages needed to print the
* entire layer of the given print target.
*
* @param target
* The print target to print.
* @param printer
* The printer that will be used.
* @return The number of horizontal and vertical pages that are needed to
* print the layer of the given print target.
*/
private Point getPageCount(PrintTarget target, Printer printer) {
Rectangle printArea = computePrintArea(printer);
float[] scaleFactor = computeScaleFactor(target.layer, printer, false);
Integer[] gridLineWidth = getGridLineWidth(target.configRegistry);
// calculate pages based on non cut off columns/rows
int numOfHorizontalPages = 0;
int pageWidth = Math.round(Float.valueOf(printArea.width / scaleFactor[0]));
int endX = 0;
while (endX < target.layer.getWidth()) {
endX += pageWidth;
int colPos = target.layer.getColumnPositionByX(endX);
if (colPos >= 0) {
ILayerCell cell = findColumnCellForBounds(target.layer, colPos);
if (cell != null) {
Rectangle cellBounds = cell.getBounds();
if (cellBounds.x < endX) {
endX -= (endX - cellBounds.x);
endX -= gridLineWidth[1];
}
}
} else {
endX = target.layer.getWidth();
}
numOfHorizontalPages++;
}
int numOfVerticalPages = 0;
int pageHeight = Math.round(Float.valueOf((printArea.height - getFooterHeightInPrinterDPI()) / scaleFactor[1]));
int endY = 0;
while (endY < target.layer.getHeight()) {
endY += pageHeight;
int rowPos = target.layer.getRowPositionByY(endY);
if (rowPos >= 0) {
ILayerCell cell = findRowCellForBounds(target.layer, rowPos);
if (cell != null) {
Rectangle cellBounds = cell.getBounds();
if (cellBounds.y < endY) {
endY -= (endY - cellBounds.y);
endY -= gridLineWidth[1];
}
}
} else {
endY = target.layer.getHeight();
}
numOfVerticalPages++;
}
if (gridLineWidth[0] == null) {
target.configRegistry.unregisterConfigAttribute(CellConfigAttributes.GRID_LINE_WIDTH);
}
return new Point(numOfHorizontalPages, numOfVerticalPages);
}
private ILayerCell findColumnCellForBounds(ILayer layer, int colPos) {
int rowPos = 0;
ILayerCell cell = layer.getCellByPosition(colPos, rowPos);
while (cell != null && cell.isSpannedCell()) {
// if the cell is spanned, check the cell at the next row
rowPos++;
cell = layer.getCellByPosition(colPos, rowPos);
}
return cell;
}
private ILayerCell findRowCellForBounds(ILayer layer, int rowPos) {
int colPos = 0;
ILayerCell cell = layer.getCellByPosition(colPos, rowPos);
while (cell != null && cell.isSpannedCell()) {
// if the cell is spanned, check the cell at the next column
colPos++;
cell = layer.getCellByPosition(colPos, rowPos);
}
return cell;
}
/**
* @return The footer height in printer DPI that should be used to render
* the footer.
* @since 1.5
*/
protected int getFooterHeightInPrinterDPI() {
return this.footerHeight;
}
/**
* @param configRegistry
* The {@link IConfigRegistry} to retrieve the grid line width
* from.
* @return Integer array that contains the original configured width at
* index 0 and the grid line width to use at index 1.
* @since 1.5
*/
protected Integer[] getGridLineWidth(IConfigRegistry configRegistry) {
// check if a grid line width is configured
Integer width = configRegistry.getConfigAttribute(
CellConfigAttributes.GRID_LINE_WIDTH,
DisplayMode.NORMAL);
Integer gridLineWidth = width;
// if no explicit width is set, we temporary specify a grid line
// width of 2 for optimized grid line printing
if (width == null) {
configRegistry.registerConfigAttribute(
CellConfigAttributes.GRID_LINE_WIDTH, 2);
gridLineWidth = 2;
}
return new Integer[] { width, gridLineWidth };
}
/**
* 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
for (PrintTarget target : this.printTargets) {
target.layer.doCommand(new TurnViewportOffCommand());
}
Printer printer = null;
try {
printer = setupPrinter(shell);
if (printer == null) {
return;
}
} finally {
// turn viewport on
for (PrintTarget target : this.printTargets) {
target.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(PrinterData printerData, 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) {
Printer defaultPrinter = new Printer();
int pageCount = getPageCount(defaultPrinter);
defaultPrinter.dispose();
final PrintDialog printDialog = new PrintDialog(shell);
printDialog.setStartPage(1);
printDialog.setEndPage(pageCount);
printDialog.setScope(PrinterData.ALL_PAGES);
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 Rectangle computePrintArea(Printer printer) {
// Get the printable area
Rectangle rect = printer.getClientArea();
// Compute the trim
Rectangle trim = printer.computeTrim(0, 0, 0, 0);
// Get the printer's DPI
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 Rectangle(left, top, right - left, bottom - top);
}
/**
* Enable in-memory pre-rendering. This is necessary in case content
* painters are used that are configured for content based auto-resizing.
*
* @since 1.4
*/
public void enablePreRendering() {
this.preRender = true;
}
/**
* Disable in-memory pre-rendering. You should consider to disable
* pre-rendering if no content painters are used that are configured for
* content based auto-resizing.
*
* @since 1.4
*/
public void disablePreRendering() {
this.preRender = false;
}
/**
* 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(Printer printer) {
this.printer = printer;
}
@Override
public void run() {
if (this.printer.startJob("NatTable")) { //$NON-NLS-1$
GC gc = new GC(this.printer);
try {
int currentPage = 1;
for (PrintTarget target : LayerPrinter.this.printTargets) {
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());
}
Integer[] gridLineWidth = getGridLineWidth(target.configRegistry);
// if pre-rendering is enabled, render in-memory to
// trigger content based auto-resizing
if (LayerPrinter.this.preRender) {
Transform tempTransform = new Transform(this.printer);
tempTransform.scale(scaleFactor[0], scaleFactor[1]);
AutoResizeHelper.autoResize(target.layer, target.configRegistry);
tempTransform.dispose();
}
try {
// if a SummaryRowLayer is in the layer stack, we
// 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());
// set the size of the layer according to the print
// settings made by the user
setLayerSize(target, this.printer.getPrinterData());
final Rectangle printerClientArea = computePrintArea(this.printer);
final int printBoundsWidth = Math.round(Float.valueOf(printerClientArea.width / scaleFactor[0]));
int printBoundsHeight = Math.round(Float.valueOf((printerClientArea.height - getFooterHeightInPrinterDPI()) / scaleFactor[1]));
final Point pageCount = getPageCount(target, this.printer);
// Print pages Left to Right and then Top to Down
int startY = 0;
for (int verticalPageNumber = 0; verticalPageNumber < pageCount.y; verticalPageNumber++) {
int endY = startY + printBoundsHeight;
// int rowPos =
// target.layer.getRowPositionByY(endY);
int rowPos = target.layer.getRowPositionByY(endY);
if (rowPos >= 0) {
ILayerCell cell = findRowCellForBounds(target.layer, rowPos);
if (cell != null) {
Rectangle cellBounds = cell.getBounds();
if (cellBounds.y < endY) {
printBoundsHeight -= (endY - cellBounds.y);
}
}
}
int startX = 0;
for (int horizontalPageNumber = 0; horizontalPageNumber < pageCount.x; horizontalPageNumber++) {
// Calculate bounds for the next page
Rectangle printBounds = new Rectangle(
startX,
startY,
printBoundsWidth,
printBoundsHeight);
int endX = startX + printBounds.width;
int colPos = target.layer.getColumnPositionByX(endX);
if (colPos >= 0) {
ILayerCell cell = findColumnCellForBounds(target.layer, colPos);
if (cell != null) {
Rectangle cellBounds = cell.getBounds();
if (cellBounds.x < endX) {
printBounds.width -= (endX - cellBounds.x);
}
}
}
Rectangle footerBounds = new Rectangle(
Math.round(Float.valueOf((printerClientArea.width / dpiFactor[0]) * horizontalPageNumber)),
Math.round(Float.valueOf(((printerClientArea.height - getFooterHeightInPrinterDPI()) / dpiFactor[1]) * verticalPageNumber)),
Math.round(Float.valueOf(printerClientArea.width / dpiFactor[0])),
Math.round(Float.valueOf((printerClientArea.height - getFooterHeightInPrinterDPI()) / dpiFactor[1])));
if (shouldPrint(this.printer.getPrinterData(), currentPage)) {
this.printer.startPage();
Transform printerTransform = new Transform(this.printer);
Transform footerTransform = new Transform(this.printer);
// FIXME
Rectangle intersect = new Rectangle(
0,
0,
target.layer.getWidth(),
target.layer.getHeight());
intersect = printBounds.intersection(intersect);
configureScalingTransform(printerTransform, scaleFactor, printerClientArea, intersect);
configureScalingTransform(footerTransform, dpiFactor, printerClientArea, footerBounds);
gc.setTransform(printerTransform);
printLayer(target, gc, intersect);
gc.setTransform(footerTransform);
printFooter(gc, currentPage, footerBounds, target.configRegistry);
this.printer.endPage();
printerTransform.dispose();
footerTransform.dispose();
}
currentPage++;
startX += printBounds.width;
}
startY += printBoundsHeight;
}
} finally {
restoreLayerState(target);
}
// there was no explicit width configured, so we
// configured a temporary one for grid line printing.
// this configuration needs to be removed again
if (gridLineWidth[0] == null) {
target.configRegistry.unregisterConfigAttribute(CellConfigAttributes.GRID_LINE_WIDTH);
}
}
} finally {
this.printer.endJob();
gc.dispose();
this.printer.dispose();
}
}
}
/**
* Configure the given {@link Transform} in order to support scaling
* correctly on printing.
*
* @param transform
* The {@link Transform} to configure
* @param scaleFactor
* The scale factor to set and to be used for translation
* @param printerClientArea
* The client area of the printer
* @param printBounds
* The print bounds
*/
private void configureScalingTransform(
Transform transform, float[] scaleFactor,
Rectangle printerClientArea, Rectangle printBounds) {
// Adjust for DPI difference between display and printer
transform.scale(scaleFactor[0], scaleFactor[1]);
// Adjust for margins
transform.translate(
printerClientArea.x / scaleFactor[0],
printerClientArea.y / scaleFactor[1]);
// 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
transform.translate(-1 * printBounds.x, -1 * printBounds.y);
}
/**
* 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 target
* The print target to print.
* @param printerData
* The PrinterData that was configured by the user on the
* PrintDialog.
*/
private void setLayerSize(PrintTarget target, PrinterData printerData) {
if (printerData != null && printerData.scope == PrinterData.SELECTION) {
target.layer.setClientAreaProvider(target.originalClientAreaProvider);
} else {
final Rectangle fullLayerSize = getTotalArea(target.layer);
target.layer.setClientAreaProvider(new IClientAreaProvider() {
@Override
public Rectangle 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
target.layer.doCommand(new PrintEntireGridCommand());
}
}
/**
* Print the part of the layer that matches the given print bounds.
*
* @param target
* The print target to print
* @param gc
* The print GC to render the layer to.
* @param printBounds
* The bounds of the print page.
*/
private void printLayer(PrintTarget target, GC gc, Rectangle printBounds) {
target.layer.getLayerPainter().paintLayer(
target.layer, gc, 0, 0, printBounds, target.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.
* @param configRegistry
* The {@link IConfigRegistry} needed to retrieve the footer
* style.
*/
private void printFooter(GC gc, int totalPageCount, Rectangle printBounds, IConfigRegistry configRegistry) {
Color oldForeground = gc.getForeground();
Color oldBackground = gc.getBackground();
Font oldFont = gc.getFont();
Color footerForeground = null;
Color footerBackground = null;
Font footerFont = null;
IStyle style = configRegistry.getConfigAttribute(
PrintConfigAttributes.FOOTER_STYLE,
DisplayMode.NORMAL);
if (style != null) {
footerForeground = style.getAttributeValue(CellStyleAttributes.FOREGROUND_COLOR);
footerBackground = style.getAttributeValue(CellStyleAttributes.BACKGROUND_COLOR);
footerFont = style.getAttributeValue(CellStyleAttributes.FONT);
}
gc.setForeground(footerForeground != null ? footerForeground : GUIHelper.COLOR_BLACK);
gc.setBackground(footerBackground != null ? footerBackground : GUIHelper.COLOR_WHITE);
gc.setFont(footerFont != null ? footerFont : GUIHelper.DEFAULT_FONT);
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);
gc.drawText(
LayerPrinter.this.footerDate,
printBounds.x + printBounds.width - gc.textExtent(LayerPrinter.this.footerDate).x,
printBounds.y + printBounds.height + 15);
gc.setForeground(oldForeground);
gc.setBackground(oldBackground);
gc.setFont(oldFont);
}
/**
* Restores the layer state to match the display characteristics again.
* This is done by resetting the client area provider, turning the
* viewport on and enabling formula result caching again.
*
* @param target
* The print target whose state should be restarted.
*/
private void restoreLayerState(PrintTarget target) {
target.layer.setClientAreaProvider(target.originalClientAreaProvider);
target.layer.doCommand(new TurnViewportOnCommand());
target.layer.doCommand(new EnableFormulaCachingCommand());
}
}
}