blob: ac322be547ac0680a01e4846f7074c9750f403ea [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2019, 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.group.performance;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import org.eclipse.collections.api.list.primitive.MutableIntList;
import org.eclipse.collections.impl.factory.primitive.IntLists;
import org.eclipse.nebula.widgets.nattable.NatTable;
import org.eclipse.nebula.widgets.nattable.command.ILayerCommand;
import org.eclipse.nebula.widgets.nattable.coordinate.PositionUtil;
import org.eclipse.nebula.widgets.nattable.freeze.CompositeFreezeLayer;
import org.eclipse.nebula.widgets.nattable.grid.GridRegion;
import org.eclipse.nebula.widgets.nattable.grid.layer.DimensionallyDependentIndexLayer;
import org.eclipse.nebula.widgets.nattable.grid.layer.DimensionallyDependentLayer;
import org.eclipse.nebula.widgets.nattable.group.ColumnGroupUtils;
import org.eclipse.nebula.widgets.nattable.group.command.ColumnGroupExpandCollapseCommand;
import org.eclipse.nebula.widgets.nattable.group.performance.GroupModel.Group;
import org.eclipse.nebula.widgets.nattable.group.performance.GroupModel.IndexPositionConverter;
import org.eclipse.nebula.widgets.nattable.group.performance.command.ColumnGroupCollapseCommand;
import org.eclipse.nebula.widgets.nattable.group.performance.command.ColumnGroupExpandCommand;
import org.eclipse.nebula.widgets.nattable.group.performance.command.ColumnGroupReorderCommandHandler;
import org.eclipse.nebula.widgets.nattable.group.performance.command.ColumnGroupReorderEndCommandHandler;
import org.eclipse.nebula.widgets.nattable.group.performance.command.ColumnGroupReorderStartCommandHandler;
import org.eclipse.nebula.widgets.nattable.group.performance.command.ColumnGroupsCommandHandler;
import org.eclipse.nebula.widgets.nattable.group.performance.command.GroupColumnReorderCommandHandler;
import org.eclipse.nebula.widgets.nattable.group.performance.command.GroupColumnReorderEndCommandHandler;
import org.eclipse.nebula.widgets.nattable.group.performance.command.GroupColumnReorderStartCommandHandler;
import org.eclipse.nebula.widgets.nattable.group.performance.command.GroupMultiColumnReorderCommand;
import org.eclipse.nebula.widgets.nattable.group.performance.command.GroupMultiColumnReorderCommandHandler;
import org.eclipse.nebula.widgets.nattable.group.performance.command.UpdateColumnGroupCollapseCommand;
import org.eclipse.nebula.widgets.nattable.group.performance.config.DefaultColumnGroupHeaderLayerConfiguration;
import org.eclipse.nebula.widgets.nattable.group.performance.config.GroupHeaderConfigLabels;
import org.eclipse.nebula.widgets.nattable.group.performance.painter.ColumnGroupHeaderGridLineCellLayerPainter;
import org.eclipse.nebula.widgets.nattable.layer.AbstractLayerTransform;
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.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.SizeConfig;
import org.eclipse.nebula.widgets.nattable.layer.cell.IConfigLabelProvider;
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.cell.TransformedLayerCell;
import org.eclipse.nebula.widgets.nattable.layer.command.ConfigureScalingCommandHandler;
import org.eclipse.nebula.widgets.nattable.layer.event.ColumnStructuralChangeEvent;
import org.eclipse.nebula.widgets.nattable.layer.event.ColumnStructuralRefreshEvent;
import org.eclipse.nebula.widgets.nattable.layer.event.ILayerEvent;
import org.eclipse.nebula.widgets.nattable.layer.event.IStructuralChangeEvent;
import org.eclipse.nebula.widgets.nattable.layer.event.RowStructuralRefreshEvent;
import org.eclipse.nebula.widgets.nattable.layer.event.StructuralDiff;
import org.eclipse.nebula.widgets.nattable.layer.event.StructuralDiff.DiffTypeEnum;
import org.eclipse.nebula.widgets.nattable.painter.layer.ILayerPainter;
import org.eclipse.nebula.widgets.nattable.reorder.event.ColumnReorderEvent;
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.selection.SelectionLayer;
import org.eclipse.nebula.widgets.nattable.selection.SelectionLayer.MoveDirectionEnum;
import org.eclipse.nebula.widgets.nattable.style.DisplayMode;
import org.eclipse.nebula.widgets.nattable.util.ArrayUtil;
import org.eclipse.swt.graphics.Rectangle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Adds the column grouping functionality to the column header. Also persists
* the state of the column groups when
* {@link NatTable#saveState(String, Properties)} is invoked.
* <p>
* Internally uses a collection of {@link GroupModel} to track the column groups
* on multiple levels.
* </p>
* <p>
* It supports multiple column grouping levels. The levels are 0 based and
* configured bottom up. That means if 3 levels of column groups are defined,
* the first level==0 is the bottom most rowPosition==2, and the top most
* level==2 is on rowPosition==0.
* </p>
*
* @since 1.6
*/
public class ColumnGroupHeaderLayer extends AbstractLayerTransform {
private static final Logger LOG = LoggerFactory.getLogger(ColumnGroupHeaderLayer.class);
private static final String PERSISTENCE_KEY_COLUMN_GROUPS = ".columnGroups"; //$NON-NLS-1$
private final List<GroupModel> model;
/**
* {@link SizeConfig} instance for the row height configuration.
*/
private final SizeConfig rowHeightConfig = new SizeConfig(DataLayer.DEFAULT_ROW_HEIGHT);
/**
* Flag which is used to tell the {@link ColumnGroupHeaderLayer} whether to
* calculate the height of the layer dependent on column group configuration
* or not. If it is set to <code>true</code> the column header will check if
* column groups are configured and if not, the height of the column header
* will not show the double height for showing column groups.
*/
private boolean calculateHeight = false;
/**
* Flag to configure whether group names should be always visible on
* rendering, e.g. on scrolling, or if the group names should scroll with
* the cell. Default is <code>false</code>.
*/
private boolean showAlwaysGroupNames = false;
/**
* The layer to which the positions in the group should match. Typically
* this is the {@link SelectionLayer}.
*/
private IUniqueIndexLayer positionLayer;
/**
* The converter that is used to perform the index-position conversion in a
* {@link GroupModel}.
*/
private IndexPositionConverter indexPositionConverter;
/**
* The path of {@link ILayer} from {@link #positionLayer} to this layer.
* Needed to be able to convert the column position based on the
* {@link #positionLayer} to a position in this layer.
*/
private List<ILayer> layerPath;
/**
* Position that is tracked on column group reorder via dragging. Needed
* because on drag operations the viewport could scroll and therefore the
* from position is not the original one anymore.
*/
private int reorderFromColumnPosition;
/**
* Map in which it is stored if reordering is supported per level.
*/
private Map<Integer, Boolean> reorderSupportedOnLevel = new HashMap<>();
/**
* The {@link CompositeFreezeLayer} in case it is part of the layer
* composition. Needed to deal with groups in frozen state as column
* positions could get ambiguous on scrolling.
*/
private CompositeFreezeLayer compositeFreezeLayer;
/**
* Creates a {@link ColumnGroupHeaderLayer} with the specified
* configurations and one grouping level. Uses the SelectionLayer as
* positionLayer and the default configuration.
*
* @param underlyingHeaderLayer
* The underlying layer on whose top this layer should be
* created, typically the ColumnHeaderLayer.
* @param selectionLayer
* The SelectionLayer needed for command handlers that inspect
* the selection on handling.
*/
public ColumnGroupHeaderLayer(
ILayer underlyingHeaderLayer,
SelectionLayer selectionLayer) {
this(underlyingHeaderLayer, selectionLayer, selectionLayer, 1, true);
}
/**
* Creates a {@link ColumnGroupHeaderLayer} with the specified
* configurations. Uses the SelectionLayer as positionLayer and the default
* configuration.
*
* @param underlyingHeaderLayer
* The underlying layer on whose top this layer should be
* created, typically the ColumnHeaderLayer.
* @param selectionLayer
* The SelectionLayer needed for command handlers that inspect
* the selection on handling.
* @param numberOfGroupLevels
* The number of group levels that should be supported.
* Additional levels can also be added via
* {@link #addGroupingLevel()}.
*/
public ColumnGroupHeaderLayer(
ILayer underlyingHeaderLayer,
SelectionLayer selectionLayer,
int numberOfGroupLevels) {
this(underlyingHeaderLayer, selectionLayer, selectionLayer, numberOfGroupLevels, true);
}
/**
* Creates a {@link ColumnGroupHeaderLayer} with the specified
* configurations and one grouping level. Uses the default configuration.
*
* @param underlyingHeaderLayer
* The underlying layer on whose top this layer should be
* created, typically the ColumnHeaderLayer.
* @param positionLayer
* The positionLayer to which this layer should be mapped to,
* needed to handle column position transformations without
* taking the viewport into account. Typically the
* SelectionLayer.
* @param selectionLayer
* The SelectionLayer needed for command handlers that inspect
* the selection on handling.
*/
public ColumnGroupHeaderLayer(
ILayer underlyingHeaderLayer,
IUniqueIndexLayer positionLayer,
SelectionLayer selectionLayer) {
this(underlyingHeaderLayer, positionLayer, selectionLayer, 1, true);
}
/**
* Creates a {@link ColumnGroupHeaderLayer} with the specified
* configurations. Uses the default configuration.
*
* @param underlyingHeaderLayer
* The underlying layer on whose top this layer should be
* created, typically the ColumnHeaderLayer.
* @param positionLayer
* The positionLayer to which this layer should be mapped to,
* needed to handle column position transformations without
* taking the viewport into account. Typically the
* SelectionLayer.
* @param selectionLayer
* The SelectionLayer needed for command handlers that inspect
* the selection on handling.
* @param numberOfGroupLevels
* The number of group levels that should be supported.
* Additional levels can also be added via
* {@link #addGroupingLevel()}.
*/
public ColumnGroupHeaderLayer(
ILayer underlyingHeaderLayer,
IUniqueIndexLayer positionLayer,
SelectionLayer selectionLayer,
int numberOfGroupLevels) {
this(underlyingHeaderLayer, positionLayer, selectionLayer, numberOfGroupLevels, true);
}
/**
* Creates a {@link ColumnGroupHeaderLayer} with the specified
* configurations. Takes the {@link SelectionLayer} as positionLayer.
*
* @param underlyingHeaderLayer
* The underlying layer on whose top this layer should be
* created, typically the ColumnHeaderLayer.
* @param selectionLayer
* The SelectionLayer needed for command handlers that inspect
* the selection on handling.
* @param numberOfGroupLevels
* The number of group levels that should be supported.
* Additional levels can also be added via
* {@link #addGroupingLevel()}.
* @param useDefaultConfiguration
* <code>true</code> if the default configuration should be
* applied, <code>false</code> if a custom configuration will be
* applied afterwards.
*/
public ColumnGroupHeaderLayer(
ILayer underlyingHeaderLayer,
SelectionLayer selectionLayer,
int numberOfGroupLevels,
boolean useDefaultConfiguration) {
this(underlyingHeaderLayer, selectionLayer, selectionLayer, numberOfGroupLevels, useDefaultConfiguration);
}
/**
* Creates a {@link ColumnGroupHeaderLayer} with one grouping level and the
* specified configurations. Takes the {@link SelectionLayer} as
* positionLayer.
*
* @param underlyingHeaderLayer
* The underlying layer on whose top this layer should be
* created, typically the ColumnHeaderLayer.
* @param selectionLayer
* The SelectionLayer needed for command handlers that inspect
* the selection on handling.
* @param useDefaultConfiguration
* <code>true</code> if the default configuration should be
* applied, <code>false</code> if a custom configuration will be
* applied afterwards.
*/
public ColumnGroupHeaderLayer(
ILayer underlyingHeaderLayer,
SelectionLayer selectionLayer,
boolean useDefaultConfiguration) {
this(underlyingHeaderLayer, selectionLayer, selectionLayer, 1, useDefaultConfiguration);
}
/**
* Creates a {@link ColumnGroupHeaderLayer} with one grouping level and the
* specified configurations.
*
* @param underlyingHeaderLayer
* The underlying layer on whose top this layer should be
* created, typically the ColumnHeaderLayer.
* @param positionLayer
* The positionLayer to which this layer should be mapped to,
* needed to handle column position transformations without
* taking the viewport into account. Typically the
* SelectionLayer.
* @param selectionLayer
* The SelectionLayer needed for command handlers that inspect
* the selection on handling.
* @param useDefaultConfiguration
* <code>true</code> if the default configuration should be
* applied, <code>false</code> if a custom configuration will be
* applied afterwards.
*/
public ColumnGroupHeaderLayer(
ILayer underlyingHeaderLayer,
IUniqueIndexLayer positionLayer,
SelectionLayer selectionLayer,
boolean useDefaultConfiguration) {
this(underlyingHeaderLayer, selectionLayer, selectionLayer, 1, useDefaultConfiguration);
}
/**
* Creates a {@link ColumnGroupHeaderLayer} with the specified
* configurations.
*
* @param underlyingHeaderLayer
* The underlying layer on whose top this layer should be
* created, typically the ColumnHeaderLayer.
* @param positionLayer
* The positionLayer to which this layer should be mapped to,
* needed to handle column position transformations without
* taking the viewport into account. Typically the
* SelectionLayer.
* @param selectionLayer
* The SelectionLayer needed for command handlers that inspect
* the selection on handling.
* @param numberOfGroupLevels
* The number of group levels that should be supported.
* Additional levels can also be added via
* {@link #addGroupingLevel()}.
* @param useDefaultConfiguration
* <code>true</code> if the default configuration should be
* applied, <code>false</code> if a custom configuration will be
* applied afterwards.
*/
public ColumnGroupHeaderLayer(
ILayer underlyingHeaderLayer,
IUniqueIndexLayer positionLayer,
SelectionLayer selectionLayer,
int numberOfGroupLevels,
boolean useDefaultConfiguration) {
super(underlyingHeaderLayer);
this.positionLayer = positionLayer;
this.indexPositionConverter = new GroupModel.IndexPositionConverter() {
@Override
public int convertPositionToIndex(int position) {
return positionLayer.getColumnIndexByPosition(position);
}
@Override
public int convertIndexToPosition(int index) {
return positionLayer.getColumnPositionByIndex(index);
}
};
this.model = new ArrayList<>(numberOfGroupLevels);
for (int i = 0; i < numberOfGroupLevels; i++) {
GroupModel groupModel = new GroupModel();
groupModel.setIndexPositionConverter(this.indexPositionConverter);
this.model.add(groupModel);
this.reorderSupportedOnLevel.put(i, Boolean.TRUE);
}
this.layerPath = findLayerPath(this, 0);
this.layerPainter = new ColumnGroupHeaderGridLineCellLayerPainter(this);
// add listener on dependent layer to be notified about structural
// changes
positionLayer.addLayerListener(new StructuralChangeLayerListener());
registerCommandHandlers(selectionLayer);
if (useDefaultConfiguration) {
addConfiguration(new DefaultColumnGroupHeaderLayerConfiguration(false));
}
}
/**
* @return The {@link ILayerPainter} that is used by this layer. Typically
* the {@link ColumnGroupHeaderGridLineCellLayerPainter} to support
* rendering of huge column group cells by inspecting the
* {@link #showAlwaysGroupNames} attribute.
*/
@Override
public ILayerPainter getLayerPainter() {
// return the ILayerPainter set to this layer, not the ILayerPainter
// from the underlying layer as specified in AbstractLayerTransform
return this.layerPainter;
}
@Override
public void setLayerPainter(ILayerPainter layerPainter) {
this.layerPainter = layerPainter;
}
/**
* Register command handlers for this layer.
*
* @param selectionLayer
* The {@link SelectionLayer} needed for handling selections on
* grouping/ungrouping.
*/
protected void registerCommandHandlers(SelectionLayer selectionLayer) {
registerCommandHandler(new ColumnGroupsCommandHandler(this, selectionLayer));
registerCommandHandler(new ConfigureScalingCommandHandler(null, this.rowHeightConfig));
// group reordering
registerCommandHandler(new ColumnGroupReorderCommandHandler(this));
registerCommandHandler(new ColumnGroupReorderStartCommandHandler(this));
registerCommandHandler(new ColumnGroupReorderEndCommandHandler(this));
// register command handlers to add checks if a reordering is valid in
// case of unbreakable groups
getPositionLayer().registerCommandHandler(new GroupColumnReorderCommandHandler(this));
getPositionLayer().registerCommandHandler(new GroupColumnReorderStartCommandHandler(this));
getPositionLayer().registerCommandHandler(new GroupColumnReorderEndCommandHandler(this));
getPositionLayer().registerCommandHandler(new GroupMultiColumnReorderCommandHandler(this));
}
/**
* Convenience method to get the {@link GroupModel} on level 0. Useful for
* single level column grouping.
*
* @return The {@link GroupModel} for level 0.
*/
public GroupModel getGroupModel() {
return getGroupModel(0);
}
/**
* Return the {@link GroupModel} for the given grouping level. Note that the
* levels are bottom up, so level 0 is the bottom most grouping level.
*
* @param level
* The grouping level. Value is bottom up.
* @return The {@link GroupModel} for the corresponding level.
*/
public GroupModel getGroupModel(int level) {
if (level >= this.model.size()) {
LOG.warn("tried to add a group on a non-existent level"); //$NON-NLS-1$
return null;
}
return this.model.get(level);
}
/**
* Adds a new grouping level on top.
*/
public void addGroupingLevel() {
GroupModel groupModel = new GroupModel();
groupModel.setIndexPositionConverter(this.indexPositionConverter);
this.model.add(groupModel);
this.reorderSupportedOnLevel.put(this.model.size() - 1, Boolean.TRUE);
}
/**
*
* @return The number of grouping levels configured in this layer.
*/
public int getLevelCount() {
return this.model.size();
}
/**
*
* @return The layer to which the positions in the group should match.
* Typically this is the {@link SelectionLayer}.
*/
public IUniqueIndexLayer getPositionLayer() {
return this.positionLayer;
}
/**
* Calculates the path of {@link ILayer} from {@link #positionLayer} to the
* given layer.
*
* @param layer
* The {@link ILayer} for which the path is requested.
* @param columnPosition
* The column position for which the layer path should be
* calculated.
* @return The path of {@link ILayer} from the {@link #positionLayer} to the
* given {@link ILayer} or <code>null</code> if a direct path is not
* available.
*/
List<ILayer> findLayerPath(ILayer layer, int columnPosition) {
if (layer == getPositionLayer()) {
List<ILayer> result = new ArrayList<>();
result.add(layer);
return result;
}
if (this.compositeFreezeLayer == null && layer instanceof CompositeFreezeLayer) {
this.compositeFreezeLayer = (CompositeFreezeLayer) layer;
}
// handle collection
List<ILayer> result = null;
Collection<ILayer> underlyingLayers = layer.getUnderlyingLayersByColumnPosition(columnPosition);
if (underlyingLayers != null) {
for (ILayer underlyingLayer : underlyingLayers) {
if (underlyingLayer != null) {
result = findLayerPath(underlyingLayer, columnPosition);
}
}
}
// handle horizontal dependency
if (result == null && layer instanceof DimensionallyDependentLayer) {
result = findLayerPath(((DimensionallyDependentLayer) layer).getHorizontalLayerDependency(), columnPosition);
}
if (result == null && this.underlyingLayer instanceof DimensionallyDependentIndexLayer) {
result = findLayerPath(((DimensionallyDependentIndexLayer) layer).getHorizontalLayerDependency(), columnPosition);
}
// in case of the CompositeFreezeLayer it can happen that for the last
// columns in scrolled state the path cannot be determined as
// getUnderlyingLayersByPosition() returns an empty collection because
// it is above the ViewportLayer. We therefore need a special handling
// to check additionally below the ViewportLayer.
if (result == null && layer instanceof CompositeFreezeLayer) {
result = findLayerPath(((CompositeFreezeLayer) layer).getChildLayerByLayoutCoordinate(1, 1), columnPosition);
}
if (result != null) {
result.add(layer);
}
return result;
}
/**
* Converts the given column position the {@link #layerPath} upwards.
*
* @param columnPosition
* The column position to convert.
* @return The upwards converted column position.
*/
protected int convertColumnPositionUpwards(int columnPosition) {
int converted = columnPosition;
// This could be for example when the CompositeFreezeLayer is in the
// composition. At creation time the underlying layers would be empty
// because the width is not yet calculated.
List<ILayer> path = this.layerPath;
if (path == null) {
path = findLayerPath(this, columnPosition);
}
if (path != null) {
for (int i = 0; i < path.size() - 1; i++) {
ILayer underlying = path.get(i);
ILayer upper = path.get(i + 1);
converted = upper.underlyingToLocalColumnPosition(underlying, converted);
}
}
return converted;
}
@Override
public boolean doCommand(ILayerCommand command) {
if (command instanceof ColumnGroupExpandCollapseCommand
&& command.convertToTargetLayer(getPositionLayer())) {
// only ColumnGroupExpandCollapseCommand needs to be converted to
// positionLayer so also currently not visible column groups can be
// expanded/collapsed, e.g. via ColumnChooser
ColumnGroupExpandCollapseCommand cmd = (ColumnGroupExpandCollapseCommand) command;
int rowPosition = cmd.getLocalRowPosition(this);
int columnPosition = cmd.getColumnPosition();
Object[] found = findGroupForCoordinates(columnPosition, rowPosition);
if (found != null) {
GroupModel groupModel = (GroupModel) found[0];
Group group = (Group) found[1];
if (group.isCollapsed()) {
expandGroup(groupModel, group);
} else {
collapseGroup(groupModel, group);
}
}
return true;
} else if (command instanceof RowResizeCommand
&& command.convertToTargetLayer(this)
&& ((RowResizeCommand) command).getRowPosition() < getRowCount() - 1) {
RowResizeCommand rowResizeCommand = (RowResizeCommand) command;
int newRowHeight = rowResizeCommand.downScaleValue()
? this.rowHeightConfig.downScale(rowResizeCommand.getNewHeight())
: rowResizeCommand.getNewHeight();
setRowHeight(rowResizeCommand.getRowPosition(), newRowHeight);
fireLayerEvent(new RowResizeEvent(this, rowResizeCommand.getRowPosition()));
return true;
} else if (command instanceof MultiRowResizeCommand && command.convertToTargetLayer(this)) {
MultiRowResizeCommand rowResizeCommand = (MultiRowResizeCommand) command;
for (int row : rowResizeCommand.getRowPositionsArray()) {
int newRowHeight = rowResizeCommand.downScaleValue()
? this.rowHeightConfig.downScale(rowResizeCommand.getRowHeight(row))
: rowResizeCommand.getRowHeight(row);
setRowHeight(row, newRowHeight);
fireLayerEvent(new RowResizeEvent(this, row));
// do not consume as additional rows might need to get updated
// too
}
}
return super.doCommand(command);
}
// Persistence
@Override
public void saveState(String prefix, Properties properties) {
super.saveState(prefix, properties);
int level = 0;
for (GroupModel groupModel : this.model) {
groupModel.saveState(prefix + PERSISTENCE_KEY_COLUMN_GROUPS + "_" + level, properties); //$NON-NLS-1$
level++;
}
}
@Override
public void loadState(String prefix, Properties properties) {
super.loadState(prefix, properties);
// expand all currently collapsed groups
expandAllGroups();
int level = 0;
for (GroupModel groupModel : this.model) {
// load the group model
groupModel.loadState(prefix + PERSISTENCE_KEY_COLUMN_GROUPS + "_" + level, properties); //$NON-NLS-1$
// trigger real collapse of collapsed groups in model
List<Group> collapsedGroups = new ArrayList<>();
for (Group group : groupModel.getGroups()) {
if (group.isCollapsed()) {
collapsedGroups.add(group);
}
}
if (!collapsedGroups.isEmpty()) {
doCommand(new ColumnGroupCollapseCommand(groupModel, collapsedGroups));
}
level++;
}
fireLayerEvent(new ColumnStructuralRefreshEvent(this));
}
// Vertical features
// Rows
@Override
public int getRowCount() {
return this.underlyingLayer.getRowCount() + this.model.size();
}
@Override
public int getPreferredRowCount() {
return this.underlyingLayer.getPreferredRowCount() + this.model.size();
}
@Override
public int getRowIndexByPosition(int rowPosition) {
int rowCount = this.model.size();
if (rowPosition < rowCount) {
return rowPosition;
} else {
return this.underlyingLayer.getRowIndexByPosition(rowPosition - rowCount);
}
}
@Override
public int localToUnderlyingRowPosition(int localRowPosition) {
int rowCount = this.model.size();
if (localRowPosition < rowCount) {
return localRowPosition;
} else {
return localRowPosition - rowCount;
}
}
// Height
private int getGroupingHeight() {
if (!this.calculateHeight) {
return this.rowHeightConfig.getAggregateSize(this.model.size());
}
int height = 0;
for (int i = 0; i < this.model.size(); i++) {
GroupModel groupModel = this.model.get(i);
if (!groupModel.isEmpty()) {
height += this.rowHeightConfig.getSize(getRowPositionForLevel(i));
}
}
return height;
}
@Override
public int getHeight() {
return getGroupingHeight() + this.underlyingLayer.getHeight();
}
@Override
public int getPreferredHeight() {
return getGroupingHeight() + this.underlyingLayer.getPreferredHeight();
}
@Override
public int getRowHeightByPosition(int rowPosition) {
int rowCount = this.model.size();
if (rowPosition < rowCount) {
if (!this.calculateHeight) {
return this.rowHeightConfig.getSize(rowPosition);
} else {
int level = getLevelForRowPosition(rowPosition);
return getGroupModel(level).isEmpty() ? 0 : this.rowHeightConfig.getSize(rowPosition);
}
} else {
return this.underlyingLayer.getRowHeightByPosition(rowPosition - rowCount);
}
}
/**
* Set the row height for grouping level 0.
*
* @param rowHeight
* The height to set for grouping level 0.
*/
public void setRowHeight(int rowHeight) {
setRowHeight(getRowPositionForLevel(0), rowHeight);
}
/**
* Set the row height for the given row in this layer.
* <p>
* <b>Note: </b> Use {@link #getLevelForRowPosition(int)} if the row
* position for a level needs to be determined.
* </p>
*
* @param row
* The row whose height should be set.
* @param rowHeight
* The height to set for the given row position.
*/
public void setRowHeight(int row, int rowHeight) {
this.rowHeightConfig.setSize(row, rowHeight);
}
/**
*
* @param level
* The level for which the row position is requested.
* @return The row positions for the given grouping level.
*/
public int getRowPositionForLevel(int level) {
return this.model.size() - level - 1;
}
/**
*
* @param rowPosition
* The row position for which the level is requested.
* @return The level for the given row position.
*/
public int getLevelForRowPosition(int rowPosition) {
return this.model.size() - rowPosition - 1;
}
// Row resize
@Override
public boolean isRowPositionResizable(int rowPosition) {
int rowCount = this.model.size();
if (rowPosition < rowCount) {
return this.rowHeightConfig.isPositionResizable(rowPosition);
} else {
return this.underlyingLayer.isRowPositionResizable(rowPosition - rowCount);
}
}
/**
* Set the row resizable configuration for the given row position.
*
* @param rowPosition
* The row for which the resizable flag should be set.
* @param resizable
* <code>true</code> if the row should be resizable,
* <code>false</code> if not.
*/
public void setRowPositionResizable(int rowPosition, boolean resizable) {
this.rowHeightConfig.setPositionResizable(rowPosition, resizable);
}
// Y
@Override
public int getRowPositionByY(int y) {
int groupHeight = getGroupingHeight();
if (y <= groupHeight) {
return LayerUtil.getRowPositionByY(this, y);
} else {
return this.model.size() + this.underlyingLayer.getRowPositionByY(y - groupHeight);
}
}
@Override
public int getStartYOfRowPosition(int rowPosition) {
int rowCount = this.model.size();
if (rowPosition < rowCount) {
if (!this.calculateHeight) {
return this.rowHeightConfig.getAggregateSize(rowPosition);
} else {
int startY = 0;
for (int i = 0; i < rowPosition; i++) {
GroupModel groupModel = this.model.get(i);
if (!groupModel.isEmpty()) {
startY += this.rowHeightConfig.getSize(getRowPositionForLevel(i));
}
}
return startY;
}
} else {
return getGroupingHeight()
+ this.underlyingLayer.getStartYOfRowPosition(rowPosition - this.model.size());
}
}
// Cell features
@Override
public ILayerCell getCellByPosition(final int columnPosition, final int rowPosition) {
// Column group header cell
if (rowPosition < this.model.size()) {
int level = getLevelForRowPosition(rowPosition);
Group group = getGroupByPosition(level, columnPosition);
if (group != null) {
int start = convertColumnPositionUpwards(getPositionLayer().getColumnPositionByIndex(group.getVisibleStartIndex()));
// check if there is a level above that does not have a group
int row = rowPosition;
int rowSpan = 1;
while (level < (this.model.size() - 1)) {
level++;
Group upperGroup = getGroupByPosition(level, columnPosition);
if (upperGroup == null) {
row--;
rowSpan++;
} else {
break;
}
}
// if the header should be shown always, e.g. because of
// huge column groups, the start will not below 0 and the
// end not below column count
int columnSpan = getColumnSpan(group);
if (this.showAlwaysGroupNames) {
if (start < 0) {
columnSpan += start;
start = 0;
}
if (start + columnSpan > getColumnCount()) {
columnSpan = getColumnCount() - start;
}
}
return new LayerCell(
this,
start,
row,
columnPosition,
rowPosition,
columnSpan,
rowSpan);
} else {
// for the level there is no group, check if the level below has
// a group to calculate the row spanning
int rowSpan = 2;
Group subGroup = null;
while (level > 0) {
level--;
group = getGroupByPosition(level, columnPosition);
if (group == null) {
rowSpan++;
} else {
subGroup = group;
}
}
if (subGroup != null) {
int start = convertColumnPositionUpwards(getPositionLayer().getColumnPositionByIndex(subGroup.getVisibleStartIndex()));
int columnSpan = getColumnSpan(subGroup);
// if the header should be shown always, e.g. because of
// huge column groups, the start will not below 0 and the
// end not below column count
if (this.showAlwaysGroupNames) {
if (start < 0) {
columnSpan += start;
start = 0;
}
if (start + columnSpan > getColumnCount()) {
columnSpan = getColumnCount() - start;
}
}
return new LayerCell(
this,
start,
rowPosition,
columnPosition,
rowPosition,
columnSpan,
rowSpan);
} else {
// get the cell from the underlying layer
final int span = rowSpan;
ILayerCell cell = this.underlyingLayer.getCellByPosition(columnPosition, 0);
if (cell != null) {
cell = new TransformedLayerCell(cell) {
@Override
public ILayer getLayer() {
return ColumnGroupHeaderLayer.this;
}
@Override
public int getRowSpan() {
return span;
}
@Override
public int getRowPosition() {
return rowPosition;
}
@Override
public int getOriginRowPosition() {
return rowPosition;
}
};
}
return cell;
}
}
} else {
int rowSpan = 1;
// check for special case if a column header data provider supports
// multiple rows
if (rowPosition - 1 < this.model.size()) {
// check if one row above has a group
int level = getLevelForRowPosition(rowPosition - 1);
Group group = null;
while (level < this.model.size()) {
group = getGroupByPosition(level, columnPosition);
if (group == null) {
rowSpan++;
} else {
break;
}
level++;
}
}
final int span = rowSpan;
ILayerCell cell = this.underlyingLayer.getCellByPosition(columnPosition, 0);
if (cell != null) {
cell = new TransformedLayerCell(cell) {
@Override
public ILayer getLayer() {
return ColumnGroupHeaderLayer.this;
}
@Override
public int getRowSpan() {
return span;
}
@Override
public int getRowPosition() {
return rowPosition;
}
@Override
public int getOriginRowPosition() {
return rowPosition - (span - 1);
}
};
}
return cell;
}
}
@Override
public Rectangle getBoundsByPosition(int columnPosition, int rowPosition) {
Rectangle bounds = super.getBoundsByPosition(columnPosition, rowPosition);
if (this.compositeFreezeLayer != null && this.compositeFreezeLayer.isFrozen()) {
// if we are have a composition with freeze and there is a freeze
// region active, we need to perform some special bound calculation
// because the origin column positions of a spanned cell could be
// ambiguous on scrolling
ILayerCell cell = getCellByPosition(columnPosition, rowPosition);
int[] columnBounds = this.compositeFreezeLayer.getColumnBounds(
columnPosition,
cell.getOriginColumnPosition(),
cell.getOriginColumnPosition() + cell.getColumnSpan() - 1);
bounds.x = columnBounds[0];
bounds.width = columnBounds[1];
}
return bounds;
}
/**
* Get the {@link Group} for the column at the given column position for
* level 0. Will transform the given column position to a position matching
* the position layer for correct resolution.
*
* @param columnPosition
* The column position related to this layer.
* @return The {@link Group} at the given column position or
* <code>null</code> if there is no {@link Group} at this position.
*/
public Group getGroupByPosition(int columnPosition) {
return getGroupByPosition(0, columnPosition);
}
/**
* Get the {@link Group} for the column at the given column position for the
* given grouping level. Will transform the given column position to a
* position matching the position layer for correct resolution.
*
* @param level
* The grouping level for which the group is requested. The level
* is zero based bottom-up.
* @param columnPosition
* The column position related to this layer.
* @return The {@link Group} at the given column position or
* <code>null</code> if there is no {@link Group} at this position.
*/
public Group getGroupByPosition(int level, int columnPosition) {
// calculate the position matching the position layer
int posColumn = LayerUtil.convertColumnPosition(this, columnPosition, getPositionLayer());
if (posColumn > -1) {
GroupModel groupModel = getGroupModel(level);
if (groupModel != null) {
return groupModel.getGroupByPosition(posColumn);
}
}
return null;
}
/**
* Finds a {@link Group} and its parent {@link GroupModel} based on the
* coordinates.
*
* @param columnPosition
* The column position based on the position layer.
* @param rowPosition
* The row position based on this layer.
* @return Object array where the first item is the {@link GroupModel} and
* the second item is the found {@link Group}. Returns
* <code>null</code> if either no {@link GroupModel} or no
* {@link Group} was found.
*/
protected Object[] findGroupForCoordinates(int columnPosition, int rowPosition) {
int level = getLevelForRowPosition(rowPosition);
GroupModel groupModel = null;
Group group = null;
for (; level >= 0; level--) {
groupModel = getGroupModel(level);
if (groupModel != null) {
group = groupModel.getGroupByPosition(columnPosition);
if (group != null) {
return new Object[] { groupModel, group };
}
}
}
return null;
}
/**
* Checks if there is a {@link Group} configured for the given column
* position at any level.
*
* @param columnPosition
* The column position related to this layer.
* @return <code>true</code> if there is a {@link Group} at the given column
* position, <code>false</code> if not.
*/
public boolean isPartOfAGroup(int columnPosition) {
for (int level = 0; level < this.model.size(); level++) {
if (isPartOfAGroup(level, columnPosition)) {
return true;
}
}
return false;
}
/**
* Checks if there is a {@link Group} configured for the given column
* position at the given level.
*
* @param level
* The grouping level for which the group is requested. The level
* is zero based bottom-up.
* @param columnPosition
* The column position related to this layer.
* @return <code>true</code> if there is a {@link Group} at the given column
* position, <code>false</code> if not.
*/
public boolean isPartOfAGroup(int level, int columnPosition) {
Group group = getGroupByPosition(level, columnPosition);
return group != null;
}
/**
* Check if the specified position belongs to a {@link Group} and if this
* {@link Group} is unbreakable. Convenience method for checks on level 0.
*
* @param columnPosition
* The position used to retrieve the corresponding group related
* to this layer.
* @return <code>true</code> if the specified position belongs to a
* {@link Group} and this {@link Group} is unbreakable,
* <code>false</code> if not.
*/
public boolean isPartOfAnUnbreakableGroup(int columnPosition) {
return isPartOfAnUnbreakableGroup(0, columnPosition);
}
/**
* Check if the specified position belongs to a {@link Group} at the
* specified level and if this {@link Group} is unbreakable.
*
* @param level
* The level for which the check should be performed.
* @param columnPosition
* The position used to retrieve the corresponding group related
* to this layer.
* @return <code>true</code> if the specified position belongs to a
* {@link Group} at the specified level and this {@link Group} is
* unbreakable, <code>false</code> if not.
*/
public boolean isPartOfAnUnbreakableGroup(int level, int columnPosition) {
Group group = getGroupByPosition(level, columnPosition);
if (group != null) {
return group.isUnbreakable();
}
return false;
}
/**
* Calculates the span of a cell in a group. Takes into account collapsed
* and hidden columns in the group.
*
* @param group
* the group for which the span should be calculated.
*/
public int getColumnSpan(Group group) {
int sizeOfGroup = group.getVisibleSpan();
if (group.isCollapsed()) {
int sizeOfStaticColumns = group.getStaticIndexes().length;
if (sizeOfStaticColumns == 0) {
return 1;
} else {
int staticSize = 0;
for (int index : group.getStaticIndexes()) {
if (getPositionLayer().getColumnPositionByIndex(index) >= 0) {
staticSize++;
}
}
sizeOfGroup = staticSize;
}
}
return sizeOfGroup;
}
@Override
public String getDisplayModeByPosition(int columnPosition, int rowPosition) {
if (rowPosition < this.model.size() && isPartOfAGroup(getLevelForRowPosition(rowPosition), columnPosition)) {
return DisplayMode.NORMAL;
} else {
return this.underlyingLayer.getDisplayModeByPosition(columnPosition, rowPosition);
}
}
@Override
public LabelStack getConfigLabelsByPosition(int columnPosition, int rowPosition) {
int posColumn = LayerUtil.convertColumnPosition(this, columnPosition, getPositionLayer());
Object[] found = findGroupForCoordinates(posColumn, rowPosition);
Group group = found != null ? (Group) found[1] : null;
if (rowPosition < this.model.size() && group != null) {
LabelStack stack = new LabelStack();
if (getConfigLabelAccumulator() != null) {
getConfigLabelAccumulator().accumulateConfigLabels(stack, columnPosition, rowPosition);
}
stack.addLabel(GridRegion.COLUMN_GROUP_HEADER);
if (group != null && group.isCollapseable()) {
if (group.isCollapsed()) {
stack.addLabelOnTop(GroupHeaderConfigLabels.GROUP_COLLAPSED_CONFIG_TYPE);
} else {
stack.addLabelOnTop(GroupHeaderConfigLabels.GROUP_EXPANDED_CONFIG_TYPE);
}
}
return stack;
} else {
return this.underlyingLayer.getConfigLabelsByPosition(columnPosition, rowPosition);
}
}
@Override
public Object getDataValueByPosition(int columnPosition, int rowPosition) {
if (rowPosition < this.model.size()) {
int level = getLevelForRowPosition(rowPosition);
Group group = getGroupByPosition(level, columnPosition);
while (group == null && level > 0) {
level--;
group = getGroupByPosition(level, columnPosition);
}
if (group != null) {
return group.getName();
}
}
return this.underlyingLayer.getDataValueByPosition(columnPosition, 0);
}
@Override
public LabelStack getRegionLabelsByXY(int x, int y) {
if (y < getGroupingHeight()) {
for (int i = 0; i < this.model.size(); i++) {
if (isPartOfAGroup(i, getColumnPositionByX(x))) {
return new LabelStack(GridRegion.COLUMN_GROUP_HEADER);
}
}
}
return this.underlyingLayer.getRegionLabelsByXY(x, y - getGroupingHeight());
}
// GroupModel delegates
/**
* Returns the {@link Group} for the given name at level 0.
*
* @param name
* The name of the requested group.
* @return The group with the given group name or <code>null</code> if there
* is no group with such a name.
*/
public Group getGroupByName(String name) {
return getGroupByName(0, name);
}
/**
* Returns the {@link Group} for the given name.
*
* @param level
* The grouping level for which the group is requested. The level
* is zero based bottom-up.
* @param name
* The name of the requested group.
* @return The group with the given group name or <code>null</code> if there
* is no group with such a name.
*/
public Group getGroupByName(int level, String name) {
GroupModel groupModel = getGroupModel(level);
if (groupModel != null) {
return groupModel.getGroupByName(name);
}
return null;
}
/**
* Adds the given positions to the group with the given name.
*
* @param groupName
* The name of the group to which the given positions should be
* added.
* @param positions
* The positions to add corresponding to this layer.
*/
public void addPositionsToGroup(String groupName, int... positions) {
addPositionsToGroup(0, groupName, positions);
}
/**
* Adds the given positions to the group with the given name.
*
* @param level
* The grouping level for which the group is requested. The level
* is zero based bottom-up.
* @param groupName
* The name of the group to which the given positions should be
* added.
* @param positions
* The positions to add corresponding to this layer.
*/
public void addPositionsToGroup(int level, String groupName, int... positions) {
GroupModel groupModel = getGroupModel(level);
if (groupModel != null) {
Group group = groupModel.getGroupByName(groupName);
if (group != null) {
addPositionsToGroup(group, positions);
}
}
}
/**
* Adds the given positions to the group to which the given column position
* belongs to.
*
* @param level
* The grouping level for which the group is requested. The level
* is zero based bottom-up.
* @param columnPosition
* The column position related to this layer to get the
* corresponding group to which the given positions should be
* added.
* @param positions
* The positions to add corresponding to this layer.
*/
public void addPositionsToGroup(int level, int columnPosition, int... positions) {
Group group = getGroupByPosition(level, columnPosition);
if (group != null) {
addPositionsToGroup(group, positions);
}
}
/**
* Adds the given positions to the given {@link Group}.
*
* @param group
* The {@link Group} to which the positions should be added.
* @param positions
* The positions to add corresponding to this layer.
*/
protected void addPositionsToGroup(Group group, int... positions) {
addPositionsToGroup(0, group, positions);
}
/**
* Adds the given positions to the given {@link Group}.
*
* @param level
* The grouping level for which the group is requested. The level
* is zero based bottom-up.
* @param group
* The {@link Group} to which the positions should be added.
* @param positions
* The positions to add corresponding to this layer.
*/
protected void addPositionsToGroup(int level, Group group, int... positions) {
int[] converted = new int[positions.length];
for (int pos = 0; pos < positions.length; pos++) {
// calculate the position matching the position layer
converted[pos] = LayerUtil.convertColumnPosition(this, positions[pos], getPositionLayer());
}
if (group.isCollapsed()) {
getPositionLayer().doCommand(new ColumnGroupExpandCommand(getGroupModel(level), group));
}
GroupModel groupModel = getGroupModel(level);
if (groupModel != null) {
groupModel.addPositionsToGroup(group, converted);
}
fireLayerEvent(new RowStructuralRefreshEvent(ColumnGroupHeaderLayer.this.underlyingLayer));
}
/**
* Removes the given positions from corresponding groups. Only performs an
* action if the position is part of the group.
* <p>
* <b>Note:</b><br>
* A removal will only happen for columns at the beginning or the end of a
* group. Removing a position in the middle will cause removal of columns at
* the end of the group to avoid splitting a group.
* </p>
* <p>
* <b>Note:</b><br>
* A removal does only work for visible positions. That means removing
* something from a collapsed group does not work.
* </p>
*
* @param level
* The grouping level for which the group is requested. The level
* is zero based bottom-up.
* @param positions
* The positions to remove corresponding to this layer.
*/
public void removePositionsFromGroup(int level, int... positions) {
int[] converted = new int[positions.length];
for (int pos = 0; pos < positions.length; pos++) {
// calculate the position matching the position layer
converted[pos] = LayerUtil.convertColumnPosition(this, positions[pos], getPositionLayer());
}
GroupModel groupModel = getGroupModel(level);
if (groupModel != null) {
for (int i = converted.length - 1; i >= 0; i--) {
int pos = converted[i];
Group group = groupModel.getGroupByPosition(pos);
if (group.isCollapsed()) {
getPositionLayer().doCommand(new ColumnGroupExpandCommand(groupModel, group));
}
groupModel.removePositionsFromGroup(group, pos);
}
}
// fire the event to update column group rendering in case calculate
// height is enabled
if (this.calculateHeight) {
fireLayerEvent(new RowStructuralRefreshEvent(ColumnGroupHeaderLayer.this.underlyingLayer));
}
}
/**
* Creates and adds a group.
*
* @param groupName
* The name of the group. Typically used as value in the cell.
* @param startIndex
* The index of the first item in the group.
* @param span
* The configured number of items that belong to this group.
*/
public void addGroup(String groupName, int startIndex, int span) {
addGroup(0, groupName, startIndex, span);
}
/**
* Creates and adds a group.
*
* @param level
* The grouping level for which the group is requested. The level
* is zero based bottom-up.
* @param groupName
* The name of the group. Typically used as value in the cell.
* @param startIndex
* The index of the first item in the group.
* @param span
* The configured number of items that belong to this group.
*/
public void addGroup(int level, String groupName, int startIndex, int span) {
GroupModel groupModel = getGroupModel(level);
if (groupModel != null) {
groupModel.addGroup(groupName, startIndex, span);
}
}
/**
* Removes the group identified by the given name.
*
* @param groupName
* The name of the group to remove.
*/
public void removeGroup(String groupName) {
removeGroup(0, groupName);
}
/**
* Removes the group identified by the given name.
*
* @param level
* The grouping level for which the group is requested. The level
* is zero based bottom-up.
* @param groupName
* The name of the group to remove.
*/
public void removeGroup(int level, String groupName) {
GroupModel groupModel = getGroupModel(level);
if (groupModel != null) {
Group group = groupModel.removeGroup(groupName);
if (group != null && group.isCollapsed()) {
getPositionLayer().doCommand(new ColumnGroupExpandCommand(getGroupModel(level), group));
}
}
}
/**
* Removes the group identified by the given column position.
*
* @param columnPosition
* The group that contains the given column position.
*/
public void removeGroup(int columnPosition) {
removeGroup(0, columnPosition);
}
/**
* Removes the group identified by the given column position.
*
* @param level
* The grouping level for which the group is requested. The level
* is zero based bottom-up.
* @param columnPosition
* The group that contains the given column position.
*/
public void removeGroup(int level, int columnPosition) {
GroupModel groupModel = getGroupModel(level);
if (groupModel != null) {
Group group = groupModel.removeGroup(columnPosition);
if (group != null && group.isCollapsed()) {
getPositionLayer().doCommand(new ColumnGroupExpandCommand(getGroupModel(level), group));
}
}
}
/**
* Removes the given group.
*
* @param group
* The group to remove.
*/
public void removeGroup(Group group) {
removeGroup(0, group);
}
/**
* Removes the given group.
*
* @param level
* The grouping level for which the group is requested. The level
* is zero based bottom-up.
* @param group
* The group to remove.
*/
public void removeGroup(int level, Group group) {
GroupModel groupModel = getGroupModel(level);
if (groupModel != null) {
groupModel.removeGroup(group);
if (group != null && group.isCollapsed()) {
getPositionLayer().doCommand(new ColumnGroupExpandCommand(getGroupModel(level), group));
}
}
}
/**
* Removes all groups in all levels.
*/
public void clearAllGroups() {
for (GroupModel groupModel : this.model) {
if (!groupModel.isEmpty()) {
getPositionLayer().doCommand(new ColumnGroupExpandCommand(groupModel, groupModel.getGroups()));
}
groupModel.clear();
}
}
/**
* Removes all groups in the given level.
*
* @param level
* The grouping level that should be cleared. The level is zero
* based bottom-up.
*/
public void clearAllGroups(int level) {
GroupModel groupModel = getGroupModel(level);
if (groupModel != null && !groupModel.isEmpty()) {
getPositionLayer().doCommand(new ColumnGroupExpandCommand(groupModel, groupModel.getGroups()));
groupModel.clear();
}
}
/**
* Adds the given indexes as static indexes to the group that is identified
* by the given group name. Static indexes are the indexes that stay visible
* when the group is collapsed.
*
* @param groupName
* The name of a group to which the the static indexes should be
* added to.
* @param staticIndexes
* The static indexes to add.
*/
public void addStaticColumnIndexesToGroup(String groupName, int... staticIndexes) {
addStaticColumnIndexesToGroup(0, groupName, staticIndexes);
}
/**
* Adds the given indexes as static indexes to the group that is identified
* by the given group name. Static indexes are the indexes that stay visible
* when the group is collapsed.
*
* @param level
* The grouping level for which the group is requested. The level
* is zero based bottom-up.
* @param groupName
* The name of a group to which the the static indexes should be
* added to.
* @param staticIndexes
* The static indexes to add.
*/
public void addStaticColumnIndexesToGroup(int level, String groupName, int... staticIndexes) {
GroupModel groupModel = getGroupModel(level);
if (groupModel != null) {
groupModel.addStaticIndexesToGroup(groupName, staticIndexes);
}
}
/**
* Adds the given indexes as static indexes to the group that is identified
* by the given column position. Static indexes are the indexes that stay
* visible when the group is collapsed.
*
* @param level
* The grouping level for which the group is requested. The level
* is zero based bottom-up.
* @param columnPosition
* The position of a group to which the the static indexes should
* be added to.
* @param staticIndexes
* The static indexes to add.
*/
public void addStaticColumnIndexesToGroup(int level, int columnPosition, int... staticIndexes) {
GroupModel groupModel = getGroupModel(level);
if (groupModel != null) {
groupModel.addStaticIndexesToGroup(columnPosition, staticIndexes);
}
}
/**
* Collapses the group with the given name.
*
* @param groupName
* The name of the group that should be collapsed.
*/
public void collapseGroup(String groupName) {
collapseGroup(0, groupName);
}
/**
* Collapses the group with the given name.
*
* @param level
* The grouping level for which the group is requested. The level
* is zero based bottom-up.
* @param groupName
* The name of the group that should be collapsed.
*/
public void collapseGroup(int level, String groupName) {
collapseGroup(getGroupModel(level), getGroupByName(groupName));
}
/**
* Collapses the group for the given position, if the column at the
* specified position belongs to a group.
*
* @param position
* The position corresponding to this layer whose corresponding
* group should be collapsed.
*/
public void collapseGroup(int position) {
collapseGroup(0, position);
}
/**
* Collapses the group for the given position, if the column at the
* specified position belongs to a group.
*
* @param level
* The grouping level for which the group is requested. The level
* is zero based bottom-up.
* @param position
* The position corresponding to this layer whose corresponding
* group should be collapsed.
*/
public void collapseGroup(int level, int position) {
collapseGroup(getGroupModel(level), getGroupByPosition(level, position));
}
/**
* Collapses the given group of the given group model.
*
* @param groupModel
* The group model to which the given group belongs to.
* @param group
* The group to collapse.
*/
public void collapseGroup(GroupModel groupModel, Group group) {
if (groupModel != null && group != null) {
getPositionLayer().doCommand(new ColumnGroupCollapseCommand(groupModel, group));
}
}
/**
* Collapses all groups in all levels.
*/
public void collapseAllGroups() {
for (GroupModel groupModel : this.model) {
if (!groupModel.isEmpty()) {
getPositionLayer().doCommand(new ColumnGroupCollapseCommand(groupModel, groupModel.getGroups()));
}
}
}
/**
* Collapses all groups in the given level.
*
* @param level
* The grouping level for which the group is requested. The level
* is zero based bottom-up.
*/
public void collapseAllGroups(int level) {
GroupModel groupModel = getGroupModel(level);
if (groupModel != null && !groupModel.isEmpty()) {
getPositionLayer().doCommand(new ColumnGroupCollapseCommand(groupModel, groupModel.getGroups()));
}
}
/**
* Expands the group with the given name.
*
* @param groupName
* The name of the group that should be expanded.
*/
public void expandGroup(String groupName) {
expandGroup(0, groupName);
}
/**
* Expands the group with the given name.
*
* @param level
* The grouping level for which the group is requested. The level
* is zero based bottom-up.
* @param groupName
* The name of the group that should be expanded.
*/
public void expandGroup(int level, String groupName) {
expandGroup(getGroupModel(level), getGroupByName(groupName));
}
/**
* Expands the group for the given position, if the column at the specified
* position belongs to a group.
*
* @param position
* The position corresponding to this layer whose corresponding
* group should be expanded.
*/
public void expandGroup(int position) {
expandGroup(0, position);
}
/**
* Expands the group for the given position, if the column at the specified
* position belongs to a group.
*
* @param level
* The grouping level for which the group is requested. The level
* is zero based bottom-up.
* @param position
* The position corresponding to this layer whose corresponding
* group should be expanded.
*/
public void expandGroup(int level, int position) {
expandGroup(getGroupModel(level), getGroupByPosition(level, position));
}
/**
* Expands the given group of the given group model.
*
* @param groupModel
* The group model to which the given group belongs to.
* @param group
* The group to expand.
*/
public void expandGroup(GroupModel groupModel, Group group) {
if (groupModel != null && group != null) {
getPositionLayer().doCommand(new ColumnGroupExpandCommand(groupModel, group));
}
}
/**
* Expands all groups in all levels.
*/
public void expandAllGroups() {
for (GroupModel groupModel : this.model) {
if (!groupModel.isEmpty()) {
getPositionLayer().doCommand(new ColumnGroupExpandCommand(groupModel, groupModel.getGroups()));
}
}
}
/**
* Expands all groups in the given level.
*
* @param level
* The grouping level that should be expanded. The level is zero
* based bottom-up.
*/
public void expandAllGroups(int level) {
GroupModel groupModel = getGroupModel(level);
if (groupModel != null && !groupModel.isEmpty()) {
getPositionLayer().doCommand(new ColumnGroupExpandCommand(groupModel, groupModel.getGroups()));
}
}
/**
* Sets the default value for the collapseable flag when creating group
* objects for all group levels.
*
* @param defaultCollapseable
* the default value for the collapseable flag that should be set
* on creating group.
*/
public void setDefaultCollapseable(boolean defaultCollapseable) {
for (GroupModel groupModel : this.model) {
groupModel.setDefaultCollapseable(defaultCollapseable);
}
}
/**
*
* @param level
* The grouping level for which the group is requested. The level
* is zero based bottom-up.
* @return The default value for the collapseable flag of newly created
* group objects.
*/
public boolean isDefaultCollapseable(int level) {
GroupModel groupModel = getGroupModel(level);
if (groupModel != null) {
return groupModel.isDefaultCollapseable();
}
return false;
}
/**
* Sets the default value for the collapseable flag when creating group
* objects.
*
* @param level
* The grouping level for which the group is requested. The level
* is zero based bottom-up.
* @param defaultCollapseable
* the default value for the collapseable flag that should be set
* on creating group.
*/
public void setDefaultCollapseable(int level, boolean defaultCollapseable) {
GroupModel groupModel = getGroupModel(level);
if (groupModel != null) {
groupModel.setDefaultCollapseable(defaultCollapseable);
}
}
/**
* Set the group with the given group name to be collapseable or not.
*
* @param groupName
* The name of the group that should be modified.
* @param collabseable
* <code>true</code> to set the group collapseable,
* <code>false</code> to set it not to be collapseable.
*/
public void setGroupCollapseable(String groupName, boolean collabseable) {
setGroupCollapseable(0, groupName, collabseable);
}
/**
* Set the group with the given group name to be collapseable or not.
*
* @param level
* The grouping level for which the group is requested. The level
* is zero based bottom-up.
* @param groupName
* The name of the group that should be modified.
* @param collabseable
* <code>true</code> to set the group collapseable,
* <code>false</code> to set it not to be collapseable.
*/
public void setGroupCollapseable(int level, String groupName, boolean collabseable) {
GroupModel groupModel = getGroupModel(level);
if (groupModel != null) {
groupModel.setGroupCollapseable(groupName, collabseable);
}
}
/**
* Set the group to which the specified position belongs to, to be
* collapseable or not.
*
* @param position
* The position used to retrieve the corresponding group.
* @param collabseable
* <code>true</code> to set the group collapseable,
* <code>false</code> to set it not to be collapseable.
*/
public void setGroupCollapseable(int position, boolean collabseable) {
setGroupCollapseable(0, position, collabseable);
}
/**
* Set the group to which the specified position belongs to, to be
* collapseable or not.
*
* @param level
* The grouping level for which the group is requested. The level
* is zero based bottom-up.
* @param position
* The position used to retrieve the corresponding group.
* @param collabseable
* <code>true</code> to set the group collapseable,
* <code>false</code> to set it not to be collapseable.
*/
public void setGroupCollapseable(int level, int position, boolean collabseable) {
GroupModel groupModel = getGroupModel(level);
if (groupModel != null) {
groupModel.setGroupCollapseable(position, collabseable);
}
}
/**
* Sets the default value for the unbreakable flag when creating group
* objects for all grouping levels.
*
* @param defaultUnbreakable
* the default value for the unbreakable flag that should be set
* on creating group.
*/
public void setDefaultUnbreakable(boolean defaultUnbreakable) {
for (GroupModel groupModel : this.model) {
groupModel.setDefaultUnbreakable(defaultUnbreakable);
}
}
/**
*
* @param level
* The grouping level for which the group is requested. The level
* is zero based bottom-up.
* @return The default value for the unbreakable flag of newly created group
* objects.
*/
public boolean isDefaultUnbreakable(int level) {
GroupModel groupModel = getGroupModel(level);
if (groupModel != null) {
return groupModel.isDefaultUnbreakable();
}
return false;
}
/**
* Sets the default value for the unbreakable flag when creating group
* objects.
*
* @param level
* The grouping level for which the group is requested. The level
* is zero based bottom-up.
* @param defaultUnbreakable
* the default value for the unbreakable flag that should be set
* on creating group.
*/
public void setDefaultUnbreakable(int level, boolean defaultUnbreakable) {
GroupModel groupModel = getGroupModel(level);
if (groupModel != null) {
groupModel.setDefaultUnbreakable(defaultUnbreakable);
}
}
/**
* Set the group with the given name to unbreakable/breakable.
*
* @param groupName
* The name of the group that should be modified.
* @param unbreakable
* <code>true</code> to set the group unbreakable,
* <code>false</code> to remove the unbreakable state.
*/
public void setGroupUnbreakable(String groupName, boolean unbreakable) {
setGroupUnbreakable(0, groupName, unbreakable);
}
/**
* Set the group with the given name to unbreakable/breakable.
*
* @param level
* The grouping level for which the group is requested. The level
* is zero based bottom-up.
* @param groupName
* The name of the group that should be modified.
* @param unbreakable
* <code>true</code> to set the group unbreakable,
* <code>false</code> to remove the unbreakable state.
*/
public void setGroupUnbreakable(int level, String groupName, boolean unbreakable) {
GroupModel groupModel = getGroupModel(level);
if (groupModel != null) {
groupModel.setGroupUnbreakable(groupName, unbreakable);
}
}
/**
* Set the group to which the position belongs to unbreakable/breakable.
*
* @param position
* The position used to retrieve the corresponding group.
* @param unbreakable
* <code>true</code> to set the group unbreakable,
* <code>false</code> to remove the unbreakable state.
*/
public void setGroupUnbreakable(int position, boolean unbreakable) {
setGroupUnbreakable(0, position, unbreakable);
}
/**
* Set the group to which the position belongs to unbreakable/breakable.
*
* @param level
* The grouping level for which the group is requested. The level
* is zero based bottom-up.
* @param position
* The position used to retrieve the corresponding group.
* @param unbreakable
* <code>true</code> to set the group unbreakable,
* <code>false</code> to remove the unbreakable state.
*/
public void setGroupUnbreakable(int level, int position, boolean unbreakable) {
GroupModel groupModel = getGroupModel(level);
if (groupModel != null) {
groupModel.setGroupUnbreakable(position, unbreakable);
}
}
// height
/**
*
* @return <code>true</code> if the a check is performed whether column
* groups are configured or not. If not the height of the layer will
* not show additional height for showing column groups.
* <code>false</code> if the height should be fixed regardless of
* existing column group.
*/
public boolean isCalculateHeight() {
return this.calculateHeight;
}
/**
* Configure whether the {@link ColumnGroupHeaderLayer} should calculate the
* height of the layer dependent on column group configuration or not.
*
* @param calculateHeight
* <code>true</code> if the layer should check if column groups
* are configured and if not, the height of the column header
* will not show the double height for showing column groups.
* <code>false</code> if the height should be fixed regardless of
* existing column group.
*/
public void setCalculateHeight(boolean calculateHeight) {
boolean changed = calculateHeight != this.calculateHeight;
this.calculateHeight = calculateHeight;
if (changed) {
this.fireLayerEvent(new RowStructuralRefreshEvent(this));
}
}
// group cell/name rendering
/**
*
* @return <code>true</code> if the group names are always visible on
* rendering, e.g. on scrolling, <code>false</code> if the group
* names stay at the fixed position in the cell and scroll with the
* cell. Default is <code>false</code>.
*/
public boolean isShowAlwaysGroupNames() {
return this.showAlwaysGroupNames;
}
/**
* Configure whether group names should be always visible on rendering, e.g.
* on scrolling, or if the group names should scroll with the cell. Setting
* this value to <code>true</code> is recommended for huge column groups to
* ensure that the group name is always visible. This also increases the
* rendering performance as the spanned grouping cells are limited to the
* visible area.
*
* @param showAlwaysGroupNames
* <code>true</code> if the group names should be always visible
* on rendering, e.g. on scrolling, <code>false</code> if the
* group names should stay at the fixed position in the cell and
* scroll with the cell. Default is <code>false</code>.
*/
public void setShowAlwaysGroupNames(boolean showAlwaysGroupNames) {
this.showAlwaysGroupNames = showAlwaysGroupNames;
}
// reorder
/**
* Used to support column reordering via drag and drop. Needed because on
* drag the viewport could scroll and therefore on drag end the initial
* position could not be determined anymore.
*
* @return The position from which a column reorder operation was started.
* Position is based on the configured {@link #positionLayer}.
*/
public int getReorderFromColumnPosition() {
return this.reorderFromColumnPosition;
}
/**
* Set the position from which a column group drag operation was started.
* <p>
* Used to support column reordering via drag and drop. Needed because on
* drag the viewport could scroll and therefore on drag end the initial
* position could not be determined anymore.
*
* @param fromColumnPosition
* The position from which a column reorder operation was
* started. Position needs to be based on the configured
* {@link #positionLayer}.
*/
public void setReorderFromColumnPosition(int fromColumnPosition) {
this.reorderFromColumnPosition = fromColumnPosition;
}
/**
* Check if reordering for the given grouping level is supported or not. By
* default reordering is supported on all grouping levels.
*
* @param level
* The level to check.
* @return <code>true</code> if the given grouping level does support
* reordering, <code>false</code> if group reordering is not
* supported for the given level.
*/
public boolean isReorderSupportedOnLevel(int level) {
Boolean supported = this.reorderSupportedOnLevel.get(level);
if (supported != null) {
return supported;
}
return true;
}
/**
* Configure whether reordering for a grouping level should be supported or
* not. By default reordering is enabled for all grouping levels.
*
* @param level
* The level for which the reorder support should be configured.
* @param supported
* <code>true</code> if the given grouping level should support
* reordering, <code>false</code> if group reordering should not
* be supported for the given level.
*/
public void setReorderSupportedOnLevel(int level, boolean supported) {
if (level < this.model.size()) {
this.reorderSupportedOnLevel.put(level, supported);
}
}
/**
* Reorder a column group for the fromColumnPosition at the given level to
* the specified toColumnPosition.
*
* @param level
* The group level on which the group reorder should be
* performed.
* @param fromColumnPosition
* The column position of a column in the column group that
* should be reordered. Based on the configured
* {@link #positionLayer}.
* @param toColumnPosition
* The column position to which a column group should be
* reordered to. Based on the configured {@link #positionLayer}.
* @return <code>true</code> if the reorder command was executed and
* consumed successfully
*/
public boolean reorderColumnGroup(int level, int fromColumnPosition, int toColumnPosition) {
if (!isReorderSupportedOnLevel(level)
|| !ColumnGroupUtils.isBetweenTwoGroups(
this,
level,
toColumnPosition,
toColumnPosition < getPositionLayer().getColumnCount(),
PositionUtil.getHorizontalMoveDirection(fromColumnPosition, toColumnPosition))) {
// consume the command and avoid reordering a group into another
// group
return true;
}
// additional check if the group itself is part of an unbreakable higher
// level group
if (level < getLevelCount() - 1
&& !ColumnGroupUtils.isReorderValid(
this,
level + 1,
fromColumnPosition,
toColumnPosition,
toColumnPosition < getPositionLayer().getColumnCount())) {
// consume the command and avoid reordering that breaks an
// unbreakable group
return true;
}
GroupModel groupModel = getGroupModel(level);
if (groupModel != null) {
Group group = groupModel.getGroupByPosition(fromColumnPosition);
if (group != null) {
int toPosition = toColumnPosition;
// we need to convert and fire the command on the underlying
// layer of the positionLayer as otherwise the special command
// handler is activated
int underlyingTo = getPositionLayer().localToUnderlyingColumnPosition(toPosition);
// we reorder by index so even hidden columns in a group are
// reordered
GroupMultiColumnReorderCommand command =
new GroupMultiColumnReorderCommand(
getPositionLayer(),
group.getMembers(),
underlyingTo);
Group toRight = groupModel.getGroupByPosition(toPosition);
if (toRight != null) {
command.setGroupToRight(toRight);
} else {
// check if there is a group to the left
Group toLeft = groupModel.getGroupByPosition(toPosition - 1);
if (toLeft != null) {
command.setGroupToLeft(toLeft);
}
}
command.setReorderByIndex(true);
return getPositionLayer().getUnderlyingLayerByPosition(0, 0).doCommand(command);
}
}
return false;
}
@Override
public Collection<String> getProvidedLabels() {
Collection<String> labels = super.getProvidedLabels();
labels.add(GridRegion.COLUMN_GROUP_HEADER);
labels.add(GroupHeaderConfigLabels.GROUP_COLLAPSED_CONFIG_TYPE);
labels.add(GroupHeaderConfigLabels.GROUP_EXPANDED_CONFIG_TYPE);
// add the labels configured via IConfigLabelAccumulator
if (getConfigLabelAccumulator() instanceof IConfigLabelProvider) {
labels.addAll(((IConfigLabelProvider) getConfigLabelAccumulator()).getProvidedLabels());
}
return labels;
}
/**
* {@link ILayerListener} implementation to update {@link Group}s according
* to structural changes. Is registered on the positionLayer, therefore a
* position conversion is not necessary.
*/
private final class StructuralChangeLayerListener implements ILayerListener {
@Override
public void handleLayerEvent(ILayerEvent event) {
// special handling of ColumnReorderEvent because that events
// removes and adds a column at the same time and we need special
// handling in terms of left edge to ungroup if possible
if (event instanceof ColumnReorderEvent) {
// handle reorder event and update model
updateColumnGroupModel((ColumnReorderEvent) event);
} else if (event instanceof IStructuralChangeEvent &&
((IStructuralChangeEvent) event).isHorizontalStructureChanged()) {
IStructuralChangeEvent changeEvent = (IStructuralChangeEvent) event;
Collection<StructuralDiff> columnDiffs = changeEvent.getColumnDiffs();
if (columnDiffs != null && !columnDiffs.isEmpty()) {
int[] deletedPositions = getDeletedPositions(columnDiffs);
if (deletedPositions != null) {
// check if the number of positions are the same as the
// number of indexes, otherwise trigger a consistency
// check. reason is that the ranges are modified to be
// always in a valid range, and that could cause a loss
// of hidden positions on conversion
if (event instanceof ColumnStructuralChangeEvent
&& ((ColumnStructuralChangeEvent) event).getColumnIndexes().length > deletedPositions.length) {
// this triggers a consistency check
handleDeleteDiffs(new int[0]);
} else {
handleDeleteDiffs(deletedPositions);
}
}
for (StructuralDiff diff : columnDiffs) {
if (diff.getDiffType() == DiffTypeEnum.ADD) {
// update visible start positions of all groups
updateVisibleStartPositions();
for (GroupModel groupModel : ColumnGroupHeaderLayer.this.model) {
Map<Group, UpdateColumnGroupCollapseCommand> collapseUpdates = new HashMap<>();
// find group and update visible span
for (int i = diff.getAfterPositionRange().start; i < diff.getAfterPositionRange().end; i++) {
Group group = groupModel.getGroupByPosition(i);
int newStartIndex = getPositionLayer().getColumnIndexByPosition(i);
if (group != null && group.getVisibleStartPosition() <= i) {
if (!group.isCollapsed()) {
group.setVisibleSpan(group.getVisibleSpan() + 1);
} else {
// update collapsed state
UpdateColumnGroupCollapseCommand cmd = collapseUpdates.get(group);
if (cmd == null) {
cmd = new UpdateColumnGroupCollapseCommand(groupModel, group);
collapseUpdates.put(group, cmd);
}
if (!group.containsStaticIndex(newStartIndex)) {
cmd.addIndexesToShow(newStartIndex);
cmd.addIndexesToHide(getPositionLayer().getColumnIndexByPosition(i) + 1);
}
if (group.getVisibleSpan() == 0) {
group.setVisibleSpan(1);
}
}
} else {
// if we have not found a group or the
// found group starts at the new visible
// position, check to the left to ensure
// correct handling at the end of a
// group
Group leftGroup = groupModel.getGroupByPosition(i - 1);
if (leftGroup != null
&& leftGroup.getVisibleSpan() < leftGroup.getOriginalSpan()
&& leftGroup.hasMember(newStartIndex)) {
if (!leftGroup.isCollapsed()) {
leftGroup.setVisibleSpan(leftGroup.getVisibleSpan() + 1);
} else {
if (!leftGroup.containsStaticIndex(newStartIndex)) {
// update collapsed state
UpdateColumnGroupCollapseCommand cmd = collapseUpdates.get(leftGroup);
if (cmd == null) {
cmd = new UpdateColumnGroupCollapseCommand(groupModel, leftGroup);
collapseUpdates.put(leftGroup, cmd);
}
cmd.addIndexesToHide(getPositionLayer().getColumnIndexByPosition(i));
} else {
leftGroup.setVisibleSpan(leftGroup.getVisibleSpan() + 1);
}
}
} else {
// check if there is a group with
// static indexes where the current
// index would belong to
Group g = groupModel.getGroupByStaticIndex(newStartIndex);
if (g != null) {
g.setVisibleStartIndex(newStartIndex);
g.setVisibleSpan(g.getVisibleSpan() + 1);
g.updateVisibleStartPosition();
} else {
// check if there is a group to
// the right and if that group
// has the newStartIndex as
// member
for (int e = diff.getAfterPositionRange().end; e > diff.getAfterPositionRange().start; e--) {
g = groupModel.getGroupByPosition(e);
if (g != null && g.hasMember(newStartIndex)) {
g.setStartIndex(newStartIndex);
g.setVisibleStartIndex(newStartIndex);
g.setVisibleSpan(g.getVisibleSpan() + 1);
g.updateVisibleStartPosition();
break;
}
}
}
}
}
}
for (UpdateColumnGroupCollapseCommand cmd : collapseUpdates.values()) {
doCommand(cmd);
}
}
}
}
// update visible start positions of all groups
updateVisibleStartPositions();
} else {
// trigger a consistency check as the details of the event
// probably where removed on converting the layer stack
// upwards (e.g. if the hidden position was at the end of
// the table) or a complete refresh was triggered which
// means there are no diffs
performConsistencyCheck(columnDiffs == null);
}
}
}
private int[] getDeletedPositions(Collection<StructuralDiff> columnDiffs) {
MutableIntList result = IntLists.mutable.empty();
boolean deleteDiffFound = false;
for (StructuralDiff diff : columnDiffs) {
if (diff.getDiffType() == DiffTypeEnum.DELETE) {
deleteDiffFound = true;
int[] positions = PositionUtil.getPositions(diff.getBeforePositionRange());
result.addAll(positions);
}
}
if (deleteDiffFound) {
// we need to handle the deleted positions backwards
return result.sortThis().reverseThis().toArray();
} else {
// there was no DELETE diff, so we return null
return null;
}
}
private void handleDeleteDiffs(int[] positionList) {
if (positionList.length > 0) {
for (GroupModel groupModel : ColumnGroupHeaderLayer.this.model) {
// we need to create the MutableIntList on a copy of the
// positionList array, because modifications on the
// MutableIntList will be traversed to the the underlying
// array
MutableIntList groupPositionList = IntLists.mutable.of(Arrays.copyOf(positionList, positionList.length));
while (!groupPositionList.isEmpty()) {
// find group and update visible span
// we need to iterate because one could hide the
// last column in one group and the first of
// another group at once
Integer pos = groupPositionList.get(0);
Group group = groupModel.getGroupByPosition(pos);
if (group != null) {
// find all positions belonging to
// the group
MutableIntList groupPositions = IntLists.mutable.of(group.getVisiblePositions());
// find the positions in the group
// that are hidden
MutableIntList hiddenGroupPositions = groupPositions.select(groupPositionList::contains);
// update the positionList as we
// handled the hidden columns
groupPositionList.removeAll(hiddenGroupPositions);
groupPositions.removeAll(hiddenGroupPositions);
if (groupPositions.size() > 0) {
group.setVisibleSpan(groupPositions.size());
hiddenGroupPositions.forEach(i -> {
if (group.getVisibleStartPosition() == i) {
group.setVisibleStartIndex(getPositionLayer().getColumnIndexByPosition(i - groupPositionList.size()));
}
});
} else {
group.setVisibleStartIndex(-1);
group.setVisibleSpan(0);
}
} else {
groupPositionList.remove(pos);
}
}
}
} else {
// trigger a consistency check as the details of
// the event probably where removed on
// converting the layer stack upwards (e.g. if
// the hidden position was at the end of the
// table)
performConsistencyCheck(false);
}
}
private void updateVisibleStartPositions() {
for (GroupModel groupModel : ColumnGroupHeaderLayer.this.model) {
groupModel.updateVisibleStartPositions();
}
}
private void performConsistencyCheck(boolean updateStartIndex) {
for (GroupModel groupModel : ColumnGroupHeaderLayer.this.model) {
groupModel.performConsistencyCheck(updateStartIndex);
}
}
private void updateColumnGroupModel(ColumnReorderEvent reorderEvent) {
int[] fromColumnPositions = PositionUtil.getPositions(reorderEvent.getBeforeFromColumnPositionRanges());
int toColumnPosition = reorderEvent.getBeforeToColumnPosition();
boolean reorderToLeftEdge = reorderEvent.isReorderToLeftEdge();
int fromColumnPosition = toColumnPosition;
if (toColumnPosition > fromColumnPositions[fromColumnPositions.length - 1]) {
// Moving from left to right
fromColumnPosition = fromColumnPositions[fromColumnPositions.length - 1];
} else if (toColumnPosition < fromColumnPositions[fromColumnPositions.length - 1]) {
// Moving from right to left
fromColumnPosition = fromColumnPositions[0];
}
MoveDirectionEnum moveDirection = PositionUtil.getHorizontalMoveDirection(fromColumnPosition, toColumnPosition);
if (reorderToLeftEdge
&& toColumnPosition > 0
&& MoveDirectionEnum.RIGHT == moveDirection) {
toColumnPosition--;
}
if (fromColumnPosition == -1 || toColumnPosition == -1) {
LOG.error("Invalid reorder positions, fromPosition: {}, toPosition: {}", fromColumnPosition, toColumnPosition); //$NON-NLS-1$
return;
}
for (GroupModel groupModel : ColumnGroupHeaderLayer.this.model) {
Group toColumnGroup = groupModel.getGroupByPosition(toColumnPosition);
Group fromColumnGroup = groupModel.getGroupByPosition(fromColumnPosition);
if (fromColumnGroup != null
&& toColumnGroup != null
&& fromColumnGroup.equals(toColumnGroup)) {
// movement inside a column group
// if from and to position are the same and it is a
// column at the edge of a group, remove the
// position from the group
if (fromColumnPosition == toColumnPosition
&& (!ColumnGroupUtils.isGroupReordered(fromColumnGroup, fromColumnPositions) || (fromColumnGroup.isCollapsed() && fromColumnGroup.getMembers().length > 1))
&& (fromColumnGroup.isGroupStart(fromColumnPosition)
|| fromColumnGroup.isGroupEnd(fromColumnPosition))) {
if (MoveDirectionEnum.RIGHT == moveDirection) {
int pos[] = new int[fromColumnPositions.length];
int index = 0;
for (int from = fromColumnPositions.length - 1; from >= 0; from--) {
pos[index] = fromColumnPositions[from];
index++;
}
removePositionsFromGroup(
groupModel,
fromColumnGroup,
pos,
reorderEvent.getBeforeFromColumnIndexesArray(),
fromColumnPosition,
moveDirection);
} else {
removePositionsFromGroup(
groupModel,
fromColumnGroup,
fromColumnPositions,
reorderEvent.getBeforeFromColumnIndexesArray(),
fromColumnPosition,
moveDirection);
}
break;
}
// if we moved the first visible start position or
// moved another column to the visible start
// position, we need to update the start index
if (fromColumnPosition != toColumnPosition
&& (fromColumnGroup.getVisibleStartPosition() == fromColumnPositions[0]
|| toColumnGroup.getVisibleStartPosition() == toColumnPosition)) {
int newStartIndex = getPositionLayer().getColumnIndexByPosition(fromColumnGroup.getVisibleStartPosition());
fromColumnGroup.setVisibleStartIndex(newStartIndex);
if (!fromColumnGroup.isCollapsed()
|| fromColumnGroup.containsStaticIndex(fromColumnGroup.getStartIndex())) {
// if we move inside a collapsed group, there are
// static indexes, and therefore we do not update
// the start index, as the start index should be the
// same on expand
fromColumnGroup.setStartIndex(newStartIndex);
}
}
} else if (fromColumnGroup == null
&& toColumnGroup != null) {
addPositionsToGroup(
groupModel,
toColumnGroup,
fromColumnPositions,
reorderEvent.getBeforeFromColumnIndexesArray(),
toColumnPosition,
moveDirection);
} else if (fromColumnGroup != null
&& toColumnGroup == null
&& !ColumnGroupUtils.isGroupReordered(fromColumnGroup, fromColumnPositions)) {
removePositionsFromGroup(
groupModel,
fromColumnGroup,
fromColumnPositions,
reorderEvent.getBeforeFromColumnIndexesArray(),
fromColumnPosition,
moveDirection);
} else if (fromColumnGroup == null
&& toColumnGroup == null
&& fromColumnPosition == toColumnPosition
&& fromColumnPositions.length == 1) {
// this might happen on drag and drop operations when
// trying to add a column back into an adjacent column
// group
int adjacentPos = (moveDirection == MoveDirectionEnum.RIGHT) ? fromColumnPosition + 1 : fromColumnPosition - 1;
// check if there is an adjacent column group
Group adjacentColumnGroup = groupModel.getGroupByPosition(adjacentPos);
if (adjacentColumnGroup != null && !adjacentColumnGroup.isUnbreakable()) {
addPositionsToGroup(
groupModel,
adjacentColumnGroup,
fromColumnPositions,
reorderEvent.getBeforeFromColumnIndexesArray(),
(adjacentColumnGroup.isCollapsed() && moveDirection != MoveDirectionEnum.RIGHT) ? toColumnPosition : adjacentPos,
moveDirection);
}
} else if (fromColumnGroup != null
&& toColumnGroup != null
&& !fromColumnGroup.equals(toColumnGroup)
&& (!ColumnGroupUtils.isGroupReordered(fromColumnGroup, fromColumnPositions) || (!fromColumnGroup.isCollapsed() && fromColumnGroup.getVisiblePositions().length == 1))) {
removePositionsFromGroup(
groupModel,
fromColumnGroup,
fromColumnPositions,
reorderEvent.getBeforeFromColumnIndexesArray(),
fromColumnPositions[0],
moveDirection);
addPositionsToGroup(
groupModel,
toColumnGroup,
fromColumnPositions,
reorderEvent.getBeforeFromColumnIndexesArray(),
toColumnPosition,
moveDirection);
}
groupModel.updateVisibleStartPositions();
}
}
private void addPositionsToGroup(
GroupModel groupModel,
Group group,
int[] fromColumnPositions,
int[] fromColumnIndexes,
int toPosition,
MoveDirectionEnum moveDirection) {
if (!group.isUnbreakable()) {
// increase the span as column is moved inside group
group.setOriginalSpan(group.getOriginalSpan() + fromColumnPositions.length);
// update the start index
if (group.isGroupStart(toPosition)) {
int newStartIndex = (moveDirection == MoveDirectionEnum.RIGHT)
? getPositionLayer().getColumnIndexByPosition(group.getVisibleStartPosition() - fromColumnPositions.length)
: getPositionLayer().getColumnIndexByPosition(group.getVisibleStartPosition());
if (group.getVisibleStartIndex() == group.getStartIndex()) {
group.setStartIndex(newStartIndex);
}
group.setVisibleStartIndex(newStartIndex);
}
// add the member indexes
group.addMembers(fromColumnIndexes);
if (group.isCollapsed()) {
// update collapsed state
UpdateColumnGroupCollapseCommand cmd = new UpdateColumnGroupCollapseCommand(groupModel, group);
if (group.isGroupStart(toPosition)) {
cmd.addIndexesToHide(group.getMembers());
if (group.getStaticIndexes().length > 0) {
group.setVisibleSpan(group.getVisibleSpan() + fromColumnPositions.length);
}
// the update command will trigger a hide event to hide
// the previous visible first column, therefore we need
// to update the visible start position before firing
// the command to ensure the hide handling is operating
// on the correct positions
group.updateVisibleStartPosition();
} else {
cmd.addIndexesToHide(fromColumnIndexes);
group.setVisibleSpan(group.getVisibleSpan() + fromColumnPositions.length);
}
getPositionLayer().doCommand(cmd);
} else {
// we update the visible span only in case the group is not
// collapsed
group.setVisibleSpan(group.getVisibleSpan() + fromColumnPositions.length);
}
}
}
private void removePositionsFromGroup(
GroupModel groupModel,
Group group,
int[] fromColumnPositions,
int[] fromColumnIndexes,
int fromColumnPosition,
MoveDirectionEnum moveDirection) {
if (!group.isUnbreakable()) {
boolean collapsed = group.isCollapsed();
if (collapsed) {
int fromColumnIndex = getPositionLayer().getColumnIndexByPosition(fromColumnPosition);
// we need to expand to make the next column visible again
expandGroup(groupModel, group);
fromColumnPosition = getPositionLayer().getColumnPositionByIndex(fromColumnIndex);
}
// decrease the span as column is moved out of group
group.setOriginalSpan(group.getOriginalSpan() - fromColumnPositions.length);
group.setVisibleSpan(group.getVisibleSpan() - fromColumnPositions.length);
// update the start index
if (group.isGroupStart(fromColumnPosition)) {
int newStartIndex = (moveDirection == MoveDirectionEnum.RIGHT)
? getPositionLayer().getColumnIndexByPosition(group.getVisibleStartPosition())
: getPositionLayer().getColumnIndexByPosition(group.getVisibleStartPosition() + fromColumnPositions.length);
group.setStartIndex(newStartIndex);
group.setVisibleStartIndex(newStartIndex);
}
// remove the member indexes
group.removeMembers(fromColumnIndexes);
// remove static index if removed position was a static index
group.removeStaticIndexes(ArrayUtil.asIntArray(fromColumnIndexes));
if (group.getOriginalSpan() > 0) {
group.updateVisibleStartPosition();
if (collapsed) {
// collapse again
collapseGroup(groupModel, group);
}
} else {
// all members where removed from the group, so we remove
// the group itself
groupModel.removeGroup(group);
}
}
}
}
}