| /******************************************************************************* |
| * 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); |
| } |
| } |
| } |