blob: d319950ae4929d465fe86f90dc95713844fae292 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2012, 2020 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
* Dirk Fauth <dirk.fauth@googlemail.com> - Bug 459246
******************************************************************************/
package org.eclipse.nebula.widgets.nattable.summaryrow;
import java.util.Collection;
import org.eclipse.nebula.widgets.nattable.command.DisposeResourcesCommand;
import org.eclipse.nebula.widgets.nattable.command.ILayerCommand;
import org.eclipse.nebula.widgets.nattable.config.IConfigRegistry;
import org.eclipse.nebula.widgets.nattable.layer.AbstractLayerTransform;
import org.eclipse.nebula.widgets.nattable.layer.DataLayer;
import org.eclipse.nebula.widgets.nattable.layer.IDpiConverter;
import org.eclipse.nebula.widgets.nattable.layer.ILayer;
import org.eclipse.nebula.widgets.nattable.layer.IUniqueIndexLayer;
import org.eclipse.nebula.widgets.nattable.layer.LabelStack;
import org.eclipse.nebula.widgets.nattable.layer.LayerUtil;
import org.eclipse.nebula.widgets.nattable.layer.cell.ILayerCell;
import org.eclipse.nebula.widgets.nattable.layer.cell.LayerCell;
import org.eclipse.nebula.widgets.nattable.layer.command.ConfigureScalingCommand;
import org.eclipse.nebula.widgets.nattable.layer.event.ILayerEvent;
import org.eclipse.nebula.widgets.nattable.layer.event.IVisualChangeEvent;
import org.eclipse.nebula.widgets.nattable.resize.command.MultiRowResizeCommand;
import org.eclipse.nebula.widgets.nattable.resize.command.RowResizeCommand;
import org.eclipse.nebula.widgets.nattable.resize.event.RowResizeEvent;
import org.eclipse.nebula.widgets.nattable.style.DisplayMode;
import org.eclipse.nebula.widgets.nattable.summaryrow.command.CalculateSummaryRowValuesCommand;
import org.eclipse.nebula.widgets.nattable.util.ArrayUtil;
import org.eclipse.nebula.widgets.nattable.util.CalculatedValueCache;
import org.eclipse.nebula.widgets.nattable.util.ICalculatedValueCache;
import org.eclipse.nebula.widgets.nattable.util.ICalculator;
/**
* Adds a summary row at the end. Uses {@link ISummaryProvider} to calculate the
* summaries for all columns.
* <p>
* This layer also adds the following labels:
* <ol>
* <li>{@link SummaryRowLayer#DEFAULT_SUMMARY_COLUMN_CONFIG_LABEL_PREFIX} +
* column index</li>
* <li>{@link SummaryRowLayer#DEFAULT_SUMMARY_ROW_CONFIG_LABEL} to all cells in
* the row</li>
* </ol>
*
* Example: column with index 1 will have the
* DEFAULT_SUMMARY_COLUMN_CONFIG_LABEL_PREFIX + 1 label applied. Styling and
* {@link ISummaryProvider} can be hooked up to these labels.
*
* @see DefaultSummaryRowConfiguration
*/
public class SummaryRowLayer extends AbstractLayerTransform implements IUniqueIndexLayer {
/**
* Label that gets attached to the LabelStack for every cell in the summary
* row.
*/
public static final String DEFAULT_SUMMARY_ROW_CONFIG_LABEL = "SummaryRow"; //$NON-NLS-1$
/**
* Prefix of the labels that get attached to cells in the summary row. The
* complete label will consist of this prefix and the column index at the
* end of the label. This way every cell in the summary row can be accessed
* directly via label mechanism.
*/
public static final String DEFAULT_SUMMARY_COLUMN_CONFIG_LABEL_PREFIX = "SummaryColumn_"; //$NON-NLS-1$
/**
* The ConfigRegistry for retrieving the ISummaryProvider per column.
*/
private final IConfigRegistry configRegistry;
/**
* The row height of the summary row. As the summary row itself is not
* connected to a DataLayer, this layer needs to store the height for
* itself.
* <p>
* Note: We are not using a SizeConfig here because the position of the
* summary row might change in case rows are added.
* </p>
*/
private int summaryRowHeight = DataLayer.DEFAULT_ROW_HEIGHT;
/**
* The value cache that contains the summary values and performs summary
* calculation in background processes if necessary.
*/
private ICalculatedValueCache valueCache;
/**
* Converter that is used to ensure the row height of the summary row is
* scaled correctly.
*/
private IDpiConverter dpiConverter;
/**
* Flag to configure whether the summary row should be rendered below an
* underlying layer or if it should be rendered standalone. Setting this
* value to <code>true</code> will enable rendering of the single summary
* row in a composite region.
*/
private boolean standalone = false;
/**
* Creates a SummaryRowLayer on top of the given underlying layer. It uses
* smooth value updates as default.
* <p>
* Note: This constructor will create the SummaryRowLayer by using the
* default configuration. The default configuration doesn't fit the needs so
* you usually will use your custom summary row configuration.
*
* @param underlyingDataLayer
* The underlying layer on which the SummaryRowLayer should be
* build.
* @param configRegistry
* The ConfigRegistry for retrieving the ISummaryProvider per
* column.
*
* @see DefaultSummaryRowConfiguration
*/
public SummaryRowLayer(IUniqueIndexLayer underlyingDataLayer, IConfigRegistry configRegistry) {
this(underlyingDataLayer, configRegistry, true, true);
}
/**
* Creates a SummaryRowLayer on top of the given underlying layer. It uses
* smooth value updates as default.
* <p>
* Note: This constructor will create the SummaryRowLayer by using the
* default configuration if the autoConfig parameter is set to
* <code>true</code>. The default configuration doesn't fit the needs so you
* usually will use your custom summary row configuration. When using a
* custom configuration you should use this constructor setting autoConfig
* to <code>false</code>. Otherwise you might get strange behaviour as the
* default configuration will be set additionally to your configuration.
*
* @param underlyingDataLayer
* The underlying layer on which the SummaryRowLayer should be
* build.
* @param configRegistry
* The ConfigRegistry for retrieving the ISummaryProvider per
* column.
* @param autoConfigure
* <code>true</code> to use the DefaultSummaryRowConfiguration,
* <code>false</code> if a custom configuration will be set after
* the creation.
*
* @see DefaultSummaryRowConfiguration
*/
public SummaryRowLayer(IUniqueIndexLayer underlyingDataLayer, IConfigRegistry configRegistry, boolean autoConfigure) {
this(underlyingDataLayer, configRegistry, true, autoConfigure);
}
/**
* Creates a SummaryRowLayer on top of the given underlying layer.
* <p>
* Note: This constructor will create the SummaryRowLayer by using the
* default configuration if the autoConfig parameter is set to
* <code>true</code>. The default configuration doesn't fit the needs so you
* usually will use your custom summary row configuration. When using a
* custom configuration you should use this constructor setting autoConfig
* to <code>false</code>. Otherwise you might get strange behaviour as the
* default configuration will be set additionally to your configuration.
*
* @param underlyingDataLayer
* The underlying layer on which the SummaryRowLayer should be
* build.
* @param configRegistry
* The ConfigRegistry for retrieving the ISummaryProvider per
* column.
* @param smoothUpdates
* <code>true</code> if the summary value updates should be
* performed smoothly, <code>false</code> if on re-calculation
* the value should be immediately shown as not calculated.
* @param autoConfigure
* <code>true</code> to use the DefaultSummaryRowConfiguration,
* <code>false</code> if a custom configuration will be set after
* the creation.
*
* @see DefaultSummaryRowConfiguration
*/
public SummaryRowLayer(IUniqueIndexLayer underlyingDataLayer, IConfigRegistry configRegistry,
boolean smoothUpdates, boolean autoConfigure) {
super(underlyingDataLayer);
this.configRegistry = configRegistry;
this.valueCache = new CalculatedValueCache(this, true, false, smoothUpdates);
if (autoConfigure) {
addConfiguration(new DefaultSummaryRowConfiguration());
}
}
/**
* Calculates the summary for the column using the {@link ISummaryProvider}
* from the {@link IConfigRegistry}. In order to prevent the table from
* freezing (for large data sets), the summary is calculated in a separate
* Thread. While summary is being calculated
* {@link ISummaryProvider#DEFAULT_SUMMARY_VALUE} is returned.
* <p>
* NOTE: Since this is a {@link IUniqueIndexLayer} sitting close to the
* {@link DataLayer}, columnPosition == columnIndex
*/
@Override
public Object getDataValueByPosition(final int columnPosition, final int rowPosition) {
if (isSummaryRowPosition(rowPosition)) {
return calculateNewSummaryValue(columnPosition, true);
}
return super.getDataValueByPosition(columnPosition, rowPosition);
}
/**
* Asks the local CalculatedValueCache for summary value. Is also used to
* trigger calculation prior printing or exporting to ensure that the values
* are calculated. This is necessary because usually the calculation will be
* done when the data value is requested the first time.
*
* @param columnPosition
* The column position of the requested summary value.
* @param calculateInBackground
* <code>true</code> if the summary value calculation should be
* processed in a background thread, <code>false</code> if the
* calculation should be processed in the current thread.
* @return The value that is calculated in the CalculatedValueCache or a
* temporary value if the calculation process is started in a
* background thread.
*/
private Object calculateNewSummaryValue(final int columnPosition, boolean calculateInBackground) {
// as we only care about one row in the value cache, the real row
// position doesn't matter in fact using the real summary row
// position would cause issues if rows are added or removed because
// the value cache takes into account column and row position for
// caching.
return this.valueCache.getCalculatedValue(
columnPosition,
getSummaryRowPosition(),
calculateInBackground,
new ICalculator() {
@Override
public Object executeCalculation() {
LabelStack labelStack = getConfigLabelsByPositionWithoutTransformation(columnPosition, getSummaryRowPosition());
String[] configLabels = labelStack.toArray(ArrayUtil.STRING_TYPE_ARRAY);
final ISummaryProvider summaryProvider = SummaryRowLayer.this.configRegistry.getConfigAttribute(
SummaryRowConfigAttributes.SUMMARY_PROVIDER,
DisplayMode.NORMAL,
configLabels);
// If there is no Summary provider - skip processing
if (summaryProvider == ISummaryProvider.NONE || summaryProvider == null) {
return null;
}
return summaryProvider.summarize(columnPosition);
}
});
}
/**
* Checks if the given row position is the position of the summary row.
*
* @param rowPosition
* The row position to check.
* @return <code>true</code> if the given row position is the summary row
* position.
* @since 1.6
*/
public boolean isSummaryRowPosition(int rowPosition) {
return rowPosition == getSummaryRowPosition();
}
/**
* Get the position of the summary row in this layer.
*
* @return The position of the summary row. Typically
* <code>rowCount - 1</code>.
* @since 1.6
*/
public int getSummaryRowPosition() {
return getRowCount() - 1;
}
@Override
public boolean doCommand(ILayerCommand command) {
if (command instanceof RowResizeCommand && command.convertToTargetLayer(this)) {
RowResizeCommand rowResizeCommand = (RowResizeCommand) command;
if (isSummaryRowPosition(rowResizeCommand.getRowPosition())) {
if (rowResizeCommand.downScaleValue() && this.dpiConverter != null) {
this.summaryRowHeight = this.dpiConverter.convertDpiToPixel(rowResizeCommand.getNewHeight());
} else {
this.summaryRowHeight = rowResizeCommand.getNewHeight();
}
fireLayerEvent(new RowResizeEvent(this, getSummaryRowPosition()));
return true;
}
} else if (command instanceof MultiRowResizeCommand && command.convertToTargetLayer(this)) {
MultiRowResizeCommand rowResizeCommand = (MultiRowResizeCommand) command;
for (int row : rowResizeCommand.getRowPositionsArray()) {
if (isSummaryRowPosition(row)) {
if (rowResizeCommand.downScaleValue() && this.dpiConverter != null) {
this.summaryRowHeight = this.dpiConverter.convertDpiToPixel(rowResizeCommand.getRowHeight(row));
} else {
this.summaryRowHeight = rowResizeCommand.getRowHeight(row);
}
fireLayerEvent(new RowResizeEvent(this, getSummaryRowPosition()));
// do not consume as additional rows might need to get
// updated too
}
}
} else if (command instanceof CalculateSummaryRowValuesCommand) {
for (int i = 0; i < getColumnCount(); i++) {
calculateNewSummaryValue(i, false);
}
// we do not return true here, as there might be other layers
// involved in the composition that also need to calculate
// the summary values immediately
} else if (command instanceof DisposeResourcesCommand) {
this.valueCache.dispose();
} else if (command instanceof ConfigureScalingCommand) {
this.dpiConverter = ((ConfigureScalingCommand) command).getVerticalDpiConverter();
}
return super.doCommand(command);
}
@Override
public void handleLayerEvent(ILayerEvent event) {
if (event instanceof IVisualChangeEvent) {
clearCache();
}
super.handleLayerEvent(event);
}
/**
* Clear the internal cache to trigger new calculations.
* <p>
* Usually it is not necessary to call this method manually. But for certain
* use cases it might be useful, e.g. changing the summary provider
* implementation at runtime.
*
* @see CalculatedValueCache#clearCache()
*/
public void clearCache() {
this.valueCache.clearCache();
}
/**
* Clears all values in the internal cache to trigger new calculations. This
* will also clear all values in the cache copy and will result in rendering
* like there was never a summary value calculated before.
* <p>
* Usually it is not necessary to call this method manually. But for certain
* use cases it might be useful, e.g. changing the summary provider
* implementation at runtime.
*
* @see CalculatedValueCache#killCache()
*/
public void killCache() {
this.valueCache.killCache();
}
/**
* This method is a wrapper for calling
* {@link #getConfigLabelsByPosition(int, int)}. It is needed because
* sub-classes of the {@link SummaryRowLayer} might perform column
* position-index transformations, which would be executed again if the call
* for config labels is nested, e.g. in
* {@link #calculateNewSummaryValue(int, boolean)}. By providing and using
* this implementation, sub-classes that perform position-index
* transformations are able to implement this method by directly accessing
* the super implementation.
*
* @param columnPosition
* The column position of the cell for which the config labels
* are requested. If transformations are necessary, this value
* should be already transformed.
* @param rowPosition
* The row position of the cell for which the config labels are
* requested. If transformations are necessary, this value should
* be already transformed.
* @return The {@link LabelStack} for the cell at the given coordinates.
*/
protected LabelStack getConfigLabelsByPositionWithoutTransformation(int columnPosition, int rowPosition) {
return this.getConfigLabelsByPosition(columnPosition, rowPosition);
}
@Override
public LabelStack getConfigLabelsByPosition(int columnPosition, int rowPosition) {
if (isSummaryRowPosition(rowPosition)) {
// create a new LabelStack that takes the config labels into account
LabelStack labelStack = new LabelStack();
if (getConfigLabelAccumulator() != null) {
getConfigLabelAccumulator().accumulateConfigLabels(labelStack, columnPosition, rowPosition);
}
labelStack.addLabelOnTop(DEFAULT_SUMMARY_ROW_CONFIG_LABEL);
labelStack.addLabelOnTop(DEFAULT_SUMMARY_COLUMN_CONFIG_LABEL_PREFIX + columnPosition);
return labelStack;
}
return super.getConfigLabelsByPosition(columnPosition, rowPosition);
}
@Override
public ILayerCell getCellByPosition(int columnPosition, int rowPosition) {
if (isSummaryRowPosition(rowPosition)) {
return new LayerCell(this, columnPosition, rowPosition);
}
return super.getCellByPosition(columnPosition, rowPosition);
}
@Override
public int getHeight() {
if (this.standalone) {
return getRowHeightByPosition(getSummaryRowPosition());
}
return super.getHeight() + getRowHeightByPosition(getSummaryRowPosition());
}
@Override
public int getPreferredHeight() {
if (this.standalone) {
return getRowHeightByPosition(getSummaryRowPosition());
}
return super.getPreferredHeight() + getRowHeightByPosition(getSummaryRowPosition());
}
@Override
public int getRowCount() {
if (this.standalone) {
return 1;
}
return super.getRowCount() + 1;
}
@Override
public int getPreferredRowCount() {
return getRowCount();
}
@Override
public int getRowIndexByPosition(int rowPosition) {
if (isSummaryRowPosition(rowPosition)) {
return rowPosition;
}
return super.getRowIndexByPosition(rowPosition);
}
@Override
public int getRowPositionByY(int y) {
return LayerUtil.getRowPositionByY(this, y);
}
@Override
public int getRowHeightByPosition(int rowPosition) {
if (isSummaryRowPosition(rowPosition)) {
if (this.dpiConverter != null) {
return this.dpiConverter.convertPixelToDpi(this.summaryRowHeight);
}
return this.summaryRowHeight;
}
return super.getRowHeightByPosition(rowPosition);
}
@Override
public int getRowPositionByIndex(int rowIndex) {
if (rowIndex >= 0 && rowIndex < getRowCount()) {
return rowIndex;
} else {
return -1;
}
}
@Override
public int getColumnPositionByIndex(int columnIndex) {
if (columnIndex >= 0 && columnIndex < getColumnCount()) {
return columnIndex;
} else {
return -1;
}
}
/**
*
* @return <code>true</code> if the summary row is rendered standalone,
* <code>false</code> if it is rendered below the underlying layer.
*/
public boolean isStandalone() {
return this.standalone;
}
/**
* Configure whether the summary row should be rendered below the underlying
* layer (<code>false</code>) or if it should be rendered standalone in a
* separate region of a composite (<code>true</code>).
*
* @param standalone
* Whether the summary row should be rendered standalone or below
* the underlying layer.
*/
public void setStandalone(boolean standalone) {
this.standalone = standalone;
}
/**
* @return The {@link ICalculatedValueCache} that contains the summary
* values and performs summary calculation in background processes
* if necessary.
* @since 1.3
*/
public ICalculatedValueCache getValueCache() {
return this.valueCache;
}
/**
* Set the {@link ICalculatedValueCache} that should be used internally to
* calculate the summary values in a background thread and cache the
* results.
* <p>
* <b><u>Note:</u></b> By default the {@link CalculatedValueCache} is used.
* Be sure you know what you are doing when you are trying to exchange the
* implementation.
* </p>
*
* @param valueCache
* The {@link ICalculatedValueCache} that contains the summary
* values and performs summary calculation in background
* processes if necessary.
* @since 1.3
*/
public void setValueCache(ICalculatedValueCache valueCache) {
this.valueCache = valueCache;
}
@Override
public Collection<ILayer> getUnderlyingLayersByColumnPosition(int columnPosition) {
// Bug 475766
// if this SummaryRowLayer is configured to be standalone, return null
// to ensure that possible transformations are not skipped due to layer
// composition
return this.standalone ? null : super.getUnderlyingLayersByColumnPosition(columnPosition);
}
@Override
public Collection<ILayer> getUnderlyingLayersByRowPosition(int rowPosition) {
// Bug 475766
// if this SummaryRowLayer is configured to be standalone, return null
// to ensure that possible transformations are not skipped due to layer
// composition
return this.standalone ? null : super.getUnderlyingLayersByRowPosition(rowPosition);
}
@Override
public ILayer getUnderlyingLayerByPosition(int columnPosition, int rowPosition) {
// Bug 475766
// if this SummaryRowLayer is configured to be standalone, return null
// to ensure that possible transformations are not skipped due to layer
// composition
return this.standalone ? null : super.getUnderlyingLayerByPosition(columnPosition, rowPosition);
}
/**
* @since 1.4
*/
@Override
public Collection<String> getProvidedLabels() {
Collection<String> labels = super.getProvidedLabels();
labels.add(DEFAULT_SUMMARY_ROW_CONFIG_LABEL);
for (int i = 0; i < getColumnCount(); i++) {
labels.add(DEFAULT_SUMMARY_COLUMN_CONFIG_LABEL_PREFIX + i);
}
return labels;
}
}