blob: df2ab174ccd814c8c21163ac954d421a16c4a041 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2015, 2020 Dirk Fauth.
*
* 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:
* Dirk Fauth <dirk.fauth@googlemail.com> - initial API and implementation
******************************************************************************/
package org.eclipse.nebula.widgets.nattable.resize;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.nebula.widgets.nattable.NatTable;
import org.eclipse.nebula.widgets.nattable.config.IConfigRegistry;
import org.eclipse.nebula.widgets.nattable.layer.DataLayer;
import org.eclipse.nebula.widgets.nattable.layer.ILayer;
import org.eclipse.nebula.widgets.nattable.layer.ILayerListener;
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.resize.command.MultiRowResizeCommand;
import org.eclipse.nebula.widgets.nattable.resize.event.ColumnResizeEvent;
import org.eclipse.nebula.widgets.nattable.resize.event.RowResizeEvent;
import org.eclipse.nebula.widgets.nattable.util.GCFactory;
import org.eclipse.nebula.widgets.nattable.util.IClientAreaProvider;
import org.eclipse.nebula.widgets.nattable.util.ObjectUtils;
import org.eclipse.nebula.widgets.nattable.viewport.ViewportLayer;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Display;
/**
* Helper class that renders a {@link ILayer} in-memory to trigger auto-resizing
* of rows and columns in case content painters are configured to calculate the
* necessary dimensions.
* <p>
* Note that this operation is expensive in terms of memory consumption and
* processing time. Be careful when using this helper for huge tables.
* </p>
*
* @since 1.4
*/
public class AutoResizeHelper {
/**
* The {@link ILayer} that should be used for in-memory rendering to trigger
* auto-resizing.
*/
protected final ILayer layer;
/**
* The {@link IConfigRegistry} needed for rendering.
*/
protected final IConfigRegistry configRegistry;
/**
* The total area needed to render the whole layer at once.
*/
protected Rectangle totalArea;
/**
* The total area of the previous in-memory rendering. Needed to reduce the
* rendering area on consecutive calls.
*
* @since 1.5
*/
protected Rectangle prevArea = null;
/**
* The original {@link IClientAreaProvider} needed to restore the original
* state after processing.
*/
protected IClientAreaProvider originalClientAreaProvider;
/**
* Flag to indicate that an automatic resize was triggered on rendering.
*/
protected volatile boolean resizedOnPrinting = true;
/**
* {@link ILayerListener} that is added to the {@link ILayer} to get
* informed about {@link RowResizeEvent} and {@link ColumnResizeEvent} to
* know if an automatic resize was triggered on rendering.
*/
protected ILayerListener resizeListener = event -> {
if (!AutoResizeHelper.this.resizedOnPrinting &&
(event instanceof RowResizeEvent || event instanceof ColumnResizeEvent)) {
AutoResizeHelper.this.resizedOnPrinting = true;
}
};
/**
* The {@link IClientAreaProvider} that is used for rendering the whole
* layer in-memory.
*/
protected IClientAreaProvider clientAreaProvider = () -> AutoResizeHelper.this.totalArea;
/**
*
* @param layer
* The {@link ILayer} that should be used for in-memory rendering
* to trigger auto-resizing.
* @param configRegistry
* The {@link IConfigRegistry} needed for rendering.
*/
private AutoResizeHelper(ILayer layer, IConfigRegistry configRegistry) {
this.layer = layer;
this.configRegistry = configRegistry;
this.originalClientAreaProvider = layer.getClientAreaProvider();
calculateTotalArea();
}
/**
* Executes in-memory rendering of the given {@link ILayer} to trigger
* content based auto-resizing.
*
* @param layer
* The {@link ILayer} that should be used for in-memory rendering
* to trigger auto-resizing.
* @param configRegistry
* The {@link IConfigRegistry} needed for rendering.
*/
public static void autoResize(ILayer layer, IConfigRegistry configRegistry) {
AutoResizeHelper helper = new AutoResizeHelper(layer, configRegistry);
helper.init();
try {
// as long as resize events were triggered on rendering, we paint
// in-memory again to ensure everything was at least rendered once
// and resized correctly
while (helper.resizedOnPrinting) {
helper.resizedOnPrinting = false;
helper.calculateTotalArea();
helper.paintInMemory();
helper.prevArea = helper.totalArea;
}
} finally {
helper.restore();
}
}
/**
* Paints the layer on a temporary image GC. If painters are configured for
* automatic size calculation, this painting will trigger the resize events.
*/
protected void paintInMemory() {
Image tmpImage = new Image(Display.getDefault(), 100, 100);
GC tempGC = new GC(tmpImage);
try {
if (this.prevArea != null) {
Rectangle bottom = new Rectangle(
0,
this.prevArea.height,
this.totalArea.width,
this.totalArea.height - this.prevArea.height);
if (bottom.height > 0) {
paintLayer(tempGC, bottom);
}
Rectangle right = new Rectangle(
this.prevArea.width,
0,
this.totalArea.width - this.prevArea.width,
this.totalArea.height);
if (right.width > 0) {
paintLayer(tempGC, right);
}
} else {
// render the layer on the temporary GC
paintLayer(tempGC, this.totalArea);
}
} finally {
// ensure the temporary created resources are disposed after
// processing
tempGC.dispose();
tmpImage.dispose();
}
}
/**
* 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.
*/
protected void paintLayer(GC gc, Rectangle printBounds) {
this.layer.getLayerPainter().paintLayer(
this.layer, gc, 0, 0, printBounds, this.configRegistry);
}
/**
* Calculate the total area needed to render the whole layer.
*/
protected void calculateTotalArea() {
this.totalArea = new Rectangle(0, 0, this.layer.getWidth(), this.layer.getHeight());
}
/**
* Prepare the layer for complete in-memory rendering.
*/
protected void init() {
this.layer.addLayerListener(this.resizeListener);
this.layer.setClientAreaProvider(this.clientAreaProvider);
this.layer.doCommand(new TurnViewportOffCommand());
this.layer.doCommand(new PrintEntireGridCommand());
}
/**
* Restore the original state of the layer before in-memory rendering
* preparations.
*/
protected void restore() {
this.layer.removeLayerListener(this.resizeListener);
this.layer.setClientAreaProvider(this.originalClientAreaProvider);
this.layer.doCommand(new TurnViewportOnCommand());
}
/**
* Reference to the currently active {@link AutoResizeRowRunnable} or
* <code>null</code> if no runnable is active.
*/
private static AutoResizeRowRunnable activeRunnable;
/**
* Trigger auto-resizing of rows based on the content of the whole row.
*
* @param natTable
* The NatTable on which the auto row resize should be performed.
* Needed to create a temporary {@link GC} and retrieve the
* {@link IConfigRegistry}.
* @param rowLayer
* The {@link ILayer} that should be used to determine the rows
* to auto-resize. Can be the {@link ViewportLayer} to ensure
* that the auto row resize is only triggered for visible rows or
* the {@link DataLayer} of the body region to auto-resize all
* rows.
* @param bodyDataLayer
* The {@link DataLayer} of the body region to inspect all
* columns in a row, even if not visible in the viewport. Can
* also be a higher level layer if it adds rows, e.g. the
* SummaryRowLayer.
*
* @since 1.6
*/
public static void autoResizeRows(final NatTable natTable, final ILayer rowLayer, final ILayer bodyDataLayer) {
cancelActiveRunnable();
setActiveRunnable(new AutoResizeRowRunnable(natTable, rowLayer, bodyDataLayer), natTable.getDisplay());
}
/**
* Cancel an active {@link AutoResizeRowRunnable} if the reference is set.
* Perform the check and the cancel as an atomic operation.
*/
private static synchronized void cancelActiveRunnable() {
if (activeRunnable != null) {
// if a runnable is currently active we stop it to avoid
// inconsistent execution, e.g. a previous started runnable could
// not have been finished although in the meanwhile the state of the
// table has changed
activeRunnable.cancelled = true;
}
}
/**
*
* @param runnable
* The {@link AutoResizeRowRunnable} to activate or
* <code>null</code> if there is no active
* {@link AutoResizeRowRunnable} to set.
* @param display
* The {@link Display} needed to execute the
* {@link AutoResizeRowRunnable} asynchronously, or
* <code>null</code> if there is no active
* {@link AutoResizeRowRunnable} to set.
*/
private static synchronized void setActiveRunnable(AutoResizeRowRunnable runnable, Display display) {
activeRunnable = runnable;
if (activeRunnable != null) {
display.asyncExec(activeRunnable);
}
}
/**
* {@link Runnable} that is executed asynchronously to calculate the
* preferred height of the visible rows.
*/
private static class AutoResizeRowRunnable implements Runnable {
private final NatTable natTable;
private final ILayer rowLayer;
private final ILayer bodyDataLayer;
private volatile boolean cancelled = false;
public AutoResizeRowRunnable(NatTable natTable, ILayer rowLayer, ILayer bodyDataLayer) {
this.natTable = natTable;
this.rowLayer = rowLayer;
this.bodyDataLayer = bodyDataLayer;
}
@Override
public void run() {
int rowCount = this.rowLayer.getRowCount();
if (rowCount > 0) {
int[] rowPos = new int[rowCount];
int[] rowHeights = new int[rowCount];
for (int i = 0; i < rowCount; i++) {
rowPos[i] = this.rowLayer.getRowIndexByPosition(i);
rowHeights[i] = this.rowLayer.getRowHeightByPosition(i);
}
if (this.cancelled) {
setActiveRunnable(null, null);
return;
}
int[] calculatedRowHeights = MaxCellBoundsHelper.getPreferredRowHeights(
this.natTable.getConfigRegistry(),
new GCFactory(this.natTable),
this.bodyDataLayer,
rowPos);
// only perform further actions if the heights could be
// calculated
// could fail and return null for example if the GCFactory fails
if (calculatedRowHeights != null && calculatedRowHeights.length > 0) {
// only perform row resize where necessary
// avoid unnecessary commands
final List<Integer> positions = new ArrayList<>(rowPos.length);
final List<Integer> heights = new ArrayList<>(rowPos.length);
for (int i = 0; i < rowPos.length; i++) {
if (this.cancelled) {
setActiveRunnable(null, null);
return;
}
// we ignore resizing of negative calculated heights
if (calculatedRowHeights[i] >= 0) {
// on scaling there could be a difference of 1
// pixel because of rounding issues.
// in that case we do not trigger a resize to
// avoid endless useless resizing
int diff = rowHeights[i] - calculatedRowHeights[i];
if (diff < -1 || diff > 1) {
positions.add(rowPos[i]);
heights.add(calculatedRowHeights[i]);
}
}
}
if (!positions.isEmpty() && !this.cancelled) {
this.bodyDataLayer.doCommand(
new MultiRowResizeCommand(
this.bodyDataLayer,
ObjectUtils.asIntArray(positions),
ObjectUtils.asIntArray(heights),
true));
}
}
}
setActiveRunnable(null, null);
}
}
}