blob: a53e1cd2ed9a5938f885b2ddc2c565f43790546f [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2012, 2020 Original authors and others.
*
* 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:
* Original authors and others - initial API and implementation
******************************************************************************/
package org.eclipse.nebula.widgets.nattable.group;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.eclipse.collections.api.list.primitive.MutableIntList;
import org.eclipse.collections.impl.factory.primitive.IntLists;
import org.eclipse.nebula.widgets.nattable.coordinate.PositionUtil;
import org.eclipse.nebula.widgets.nattable.group.ColumnGroupModel.ColumnGroup;
import org.eclipse.nebula.widgets.nattable.group.performance.ColumnGroupHeaderLayer;
import org.eclipse.nebula.widgets.nattable.group.performance.GroupModel;
import org.eclipse.nebula.widgets.nattable.group.performance.GroupModel.Group;
import org.eclipse.nebula.widgets.nattable.layer.ILayer;
import org.eclipse.nebula.widgets.nattable.layer.IUniqueIndexLayer;
import org.eclipse.nebula.widgets.nattable.layer.LayerUtil;
import org.eclipse.nebula.widgets.nattable.selection.SelectionLayer.MoveDirectionEnum;
public final class ColumnGroupUtils {
private ColumnGroupUtils() {
// private default constructor for helper class
}
/**
* Calculates the move direction based on the from and to position.
*
* @param fromColumnPosition
* The column position from which a move is triggered.
* @param toColumnPosition
* The column position to which a move is triggered.
* @return The direction of the triggered move operation.
*/
public static MoveDirectionEnum getMoveDirection(int fromColumnPosition, int toColumnPosition) {
if (fromColumnPosition > toColumnPosition) {
return MoveDirectionEnum.LEFT;
} else if (fromColumnPosition < toColumnPosition) {
return MoveDirectionEnum.RIGHT;
} else {
return MoveDirectionEnum.NONE;
}
}
public static boolean isInTheSameGroup(int fromColumnIndex, int toColumnIndex, ColumnGroupModel model) {
ColumnGroup fromColumnGroup = model.getColumnGroupByIndex(fromColumnIndex);
ColumnGroup toColumnGroup = model.getColumnGroupByIndex(toColumnIndex);
return fromColumnGroup != null
&& toColumnGroup != null
&& fromColumnGroup == toColumnGroup;
}
/**
* Checks whether <code>columnIndex</code> is either a defined static column
* or (if not) the first visible column in the group containing group. This
* method provides downward compatibility for all group definitions without
* static columns. When no static columns are defined the first visible
* column will be used.
*
* @param columnIndex
* The column index to check.
* @param layer
* unused
* @param underlyingLayer
* The underlying layer.
* @param model
* The column group model.
*
* @return <code>TRUE</code> if the given <code>columnIndex</code> is either
* a defined static column or (if not) the first visible column the
* it's group
*
* @deprecated Use
* {@link #isStaticOrFirstVisibleColumn(int, IUniqueIndexLayer, ColumnGroupModel)}
*/
@Deprecated
public static boolean isStaticOrFirstVisibleColumn(
int columnIndex, ILayer layer, IUniqueIndexLayer underlyingLayer, ColumnGroupModel model) {
return isStaticOrFirstVisibleColumn(columnIndex, underlyingLayer, model);
}
/**
* Checks whether <code>columnIndex</code> is either a defined static column
* or (if not) the first visible column in the group containing group. This
* method provides downward compatibility for all group definitions without
* static columns. When no static columns are defined the first visible
* column will be used.
*
* @param columnIndex
* The column index to check.
* @param underlyingLayer
* The underlying layer.
* @param model
* The column group model.
*
* @return <code>TRUE</code> if the given <code>columnIndex</code> is either
* a defined static column or (if not) the first visible column the
* it's group
*
* @since 2.0
*/
public static boolean isStaticOrFirstVisibleColumn(
int columnIndex, IUniqueIndexLayer underlyingLayer, ColumnGroupModel model) {
ColumnGroup columnGroup = model.getColumnGroupByIndex(columnIndex);
if (columnGroup.getStaticColumnIndexes().isEmpty()) {
return isFirstVisibleColumnIndexInGroup(columnIndex, underlyingLayer, model);
} else {
return model.isStaticColumn(columnIndex);
}
}
/**
*
* @param columnIndex
* The column index to check.
* @param layer
* unused
* @param underlyingLayer
* The underlying layer.
* @param model
* The column group model.
* @return <code>true</code> if the given column index is the first column
* in a column group.
*
* @deprecated Use
* {@link #isFirstVisibleColumnIndexInGroup(int, IUniqueIndexLayer, ColumnGroupModel)}
*/
@Deprecated
public static boolean isFirstVisibleColumnIndexInGroup(
int columnIndex, ILayer layer, IUniqueIndexLayer underlyingLayer, ColumnGroupModel model) {
return isFirstVisibleColumnIndexInGroup(columnIndex, underlyingLayer, model);
}
/**
*
* @param columnIndex
* The column index to check.
* @param underlyingLayer
* The underlying layer.
* @param model
* The column group model.
* @return <code>true</code> if the given column index is the first column
* in a column group.
*
* @since 2.0
*/
public static boolean isFirstVisibleColumnIndexInGroup(
int columnIndex, IUniqueIndexLayer underlyingLayer, ColumnGroupModel model) {
if (isColumnIndexHiddenInUnderLyingLayer(columnIndex, underlyingLayer)) {
return false;
}
int columnPosition = underlyingLayer.getColumnPositionByIndex(columnIndex);
List<Integer> columnIndexesInGroup = model.getColumnGroupByIndex(columnIndex).getMembers();
List<Integer> previousVisibleColumnIndexes = new ArrayList<>();
// All other indexes in the column group which are visible and
// are positioned before me
for (Integer currentIndex : columnIndexesInGroup) {
int currentPosition = underlyingLayer.getColumnPositionByIndex(currentIndex.intValue());
if (!isColumnIndexHiddenInUnderLyingLayer(currentIndex.intValue(), underlyingLayer)
&& currentPosition < columnPosition) {
previousVisibleColumnIndexes.add(currentIndex);
}
}
return previousVisibleColumnIndexes.isEmpty();
}
/**
*
* @param columnIndex
* The column index to check.
* @param layer
* unused
* @param underlyingLayer
* The underlying layer.
* @param model
* The column group model.
* @return <code>true</code> if the given column index is the last column in
* a column group.
*
* @deprecated Use
* {@link #isLastVisibleColumnIndexInGroup(int, IUniqueIndexLayer, ColumnGroupModel)}
*/
@Deprecated
public static boolean isLastVisibleColumnIndexInGroup(
int columnIndex, ILayer layer, IUniqueIndexLayer underlyingLayer, ColumnGroupModel model) {
return isLastVisibleColumnIndexInGroup(columnIndex, underlyingLayer, model);
}
/**
*
* @param columnIndex
* The column index to check.
* @param underlyingLayer
* The underlying layer.
* @param model
* The column group model.
* @return <code>true</code> if the given column index is the last column in
* a column group.
*
* @since 2.0
*/
public static boolean isLastVisibleColumnIndexInGroup(
int columnIndex, IUniqueIndexLayer underlyingLayer, ColumnGroupModel model) {
if (isColumnIndexHiddenInUnderLyingLayer(columnIndex, underlyingLayer)) {
return false;
}
List<Integer> visibleIndexesToTheRight = getVisibleIndexesToTheRight(columnIndex, underlyingLayer, model);
return visibleIndexesToTheRight.size() == 1
&& visibleIndexesToTheRight.get(0).intValue() == columnIndex;
}
/**
*
* @param columnIndex
* The index to check.
* @param layer
* unused
* @param underlyingLayer
* The underlying layer to retrieve the visible columns to the
* right.
* @param model
* The {@link ColumnGroupModel} to get the group from.
* @return The column indexes to the right of the given column index
* (inclusive)
*
* @deprecated Use
* {@link #getVisibleIndexesToTheRight(int, IUniqueIndexLayer, ColumnGroupModel)}
*/
@Deprecated
public static List<Integer> getVisibleIndexesToTheRight(
int columnIndex, ILayer layer, IUniqueIndexLayer underlyingLayer, ColumnGroupModel model) {
return getVisibleIndexesToTheRight(columnIndex, underlyingLayer, model);
}
/**
*
* @param columnIndex
* The index to check.
* @param underlyingLayer
* The underlying layer to retrieve the visible columns to the
* right.
* @param model
* The {@link ColumnGroupModel} to get the group from.
* @return The column indexes to the right of the given column index
* (inclusive)
*
* @since 2.0
*/
public static List<Integer> getVisibleIndexesToTheRight(
int columnIndex, IUniqueIndexLayer underlyingLayer, ColumnGroupModel model) {
ColumnGroup columnGroup = model.getColumnGroupByIndex(columnIndex);
if (columnGroup.isCollapsed()) {
return Collections.emptyList();
}
List<Integer> columnIndexesInGroup = columnGroup.getMembers();
int columnPosition = underlyingLayer.getColumnPositionByIndex(columnIndex);
List<Integer> visibleColumnIndexesOnRight = new ArrayList<>();
for (Integer currentIndex : columnIndexesInGroup) {
int currentPosition = underlyingLayer.getColumnPositionByIndex(currentIndex.intValue());
if (!isColumnIndexHiddenInUnderLyingLayer(currentIndex.intValue(), underlyingLayer)
&& currentPosition >= columnPosition) {
visibleColumnIndexesOnRight.add(currentIndex);
}
}
return visibleColumnIndexesOnRight;
}
/**
*
* @param columnIndex
* The column index to check.
* @param layer
* unused
* @param underlyingLayer
* The underlying layer to check against.
* @return <code>true</code> if the column is hidden in the underlying
* layer, <code>false</code> if not.
*
* @deprecated Use
* {@link #isColumnIndexHiddenInUnderLyingLayer(int, IUniqueIndexLayer)}
*/
@Deprecated
public static boolean isColumnIndexHiddenInUnderLyingLayer(int columnIndex, ILayer layer, IUniqueIndexLayer underlyingLayer) {
return isColumnIndexHiddenInUnderLyingLayer(columnIndex, underlyingLayer);
}
/**
*
* @param columnIndex
* The column index to check.
* @param underlyingLayer
* The underlying layer to check against.
* @return <code>true</code> if the column is hidden in the underlying
* layer, <code>false</code> if not.
*
* @since 2.0
*/
public static boolean isColumnIndexHiddenInUnderLyingLayer(int columnIndex, IUniqueIndexLayer underlyingLayer) {
return underlyingLayer.getColumnPositionByIndex(columnIndex) == -1;
}
/**
*
* @param columnPosition
* The column position matching the underlying layer.
* @param layer
* unused
* @param underlyingLayer
* The underlying layer to check against.
* @return <code>true</code> if the column position is hidden in the
* underlying layer, <code>false</code> if not.
*
* @deprecated Use
* {@link #isColumnPositionHiddenInUnderLyingLayer(int, IUniqueIndexLayer)}
*/
@Deprecated
public static boolean isColumnPositionHiddenInUnderLyingLayer(int columnPosition, ILayer layer, IUniqueIndexLayer underlyingLayer) {
return isColumnPositionHiddenInUnderLyingLayer(columnPosition, underlyingLayer);
}
/**
*
* @param columnPosition
* The column position matching the underlying layer.
* @param underlyingLayer
* The underlying layer to check against.
* @return <code>true</code> if the column position is hidden in the
* underlying layer, <code>false</code> if not.
*
* @since 2.0
*/
public static boolean isColumnPositionHiddenInUnderLyingLayer(int columnPosition, IUniqueIndexLayer underlyingLayer) {
if (columnPosition < underlyingLayer.getColumnCount() && columnPosition >= 0) {
int columnIndex = underlyingLayer.getColumnIndexByPosition(columnPosition);
return isColumnIndexHiddenInUnderLyingLayer(columnIndex, underlyingLayer);
}
return true;
}
/**
* See ColumnGroupUtilsTest
*
* @return TRUE if the given column is the <i>right</i> most column in a
* group
*/
public static boolean isRightEdgeOfAColumnGroup(ILayer natLayer, int columnPosition, int columnIndex, ColumnGroupModel model) {
int nextColumnPosition = columnPosition + 1;
if (nextColumnPosition < natLayer.getColumnCount()) {
int nextColumnIndex = natLayer.getColumnIndexByPosition(nextColumnPosition);
if ((model.isPartOfAGroup(columnIndex) && !model.isPartOfAGroup(nextColumnIndex))) {
return true;
}
if ((model.isPartOfAGroup(columnIndex)
&& model.isPartOfAGroup(nextColumnIndex))
&& !ColumnGroupUtils.isInTheSameGroup(columnIndex, nextColumnIndex, model)) {
return true;
}
}
return false;
}
/**
* See ColumnGroupUtilsTest
*
* @return TRUE if the given column is the <i>left</i> most column in a
* group
*/
public static boolean isLeftEdgeOfAColumnGroup(ILayer natLayer, int columnPosition, int columnIndex, ColumnGroupModel model) {
int previousColumnPosition = columnPosition - 1;
// First column && in a group
if (columnPosition == 0 && model.isPartOfAGroup(columnIndex)) {
return true;
}
if (previousColumnPosition >= 0) {
int previousColumnIndex = natLayer.getColumnIndexByPosition(previousColumnPosition);
if ((model.isPartOfAGroup(columnIndex) && !model.isPartOfAGroup(previousColumnIndex))) {
return true;
}
if ((model.isPartOfAGroup(columnIndex)
&& model.isPartOfAGroup(previousColumnIndex))
&& !ColumnGroupUtils.isInTheSameGroup(columnIndex, previousColumnIndex, model)) {
return true;
}
}
return false;
}
/**
* @return TRUE if there is a column group boundary between startX and endX
*/
public static boolean isBetweenTwoGroups(ILayer natLayer, int startX, int endX, ColumnGroupModel model) {
return !ColumnGroupUtils.isInTheSameGroup(
natLayer.getColumnIndexByPosition(natLayer.getColumnPositionByX(startX)),
natLayer.getColumnIndexByPosition(natLayer.getColumnPositionByX(endX)),
model);
}
/**
* Checks if the two given column positions on the given layer belong to the
* same group at the given level on the given
* {@link ColumnGroupHeaderLayer}.
*
* @param layer
* The {@link ColumnGroupHeaderLayer} which is needed to perform
* the check against.
* @param level
* The grouping level to check.
* @param fromPosition
* The column position to check based on the position layer of
* the given {@link ColumnGroupHeaderLayer}.
* @param toPosition
* The column position to check based on the position layer of
* the given {@link ColumnGroupHeaderLayer}.
* @return <code>true</code> if both given positions belong to the same
* group, <code>false</code> if not.
*
* @since 1.6
*/
public static boolean isInTheSameGroup(ColumnGroupHeaderLayer layer, int level, int fromPosition, int toPosition) {
Group fromColumnGroup = layer.getGroupModel(level).getGroupByPosition(fromPosition);
Group toColumnGroup = layer.getGroupModel(level).getGroupByPosition(toPosition);
return fromColumnGroup != null && toColumnGroup != null && fromColumnGroup == toColumnGroup;
}
/**
* Checks if the column positions at the given x coordinates belong to the
* same group or not.
*
* @param natLayer
* The layer to which the given positions match. Typically the
* NatTable itself.
* @param startX
* The x coordinate of the column that should be checked.
* Typically the drag start x coordinate.
* @param endX
* The x coordinate of the column that should be checked.
* Typically the drag end x coordinate.
* @param layer
* The {@link ColumnGroupHeaderLayer} which is needed to perform
* the check against.
* @param level
* The grouping level to check.
* @return <code>true</code> if there is a column group boundary between
* startX and endX, <code>false</code> if both positions are in the
* same group.
*
* @since 1.6
*/
public static boolean isBetweenTwoGroups(ILayer natLayer, int startX, int endX, ColumnGroupHeaderLayer layer, int level) {
int natFromPosition = natLayer.getColumnPositionByX(startX);
int natToPosition = natLayer.getColumnPositionByX(endX);
// convert grid position to layer position
int fromPosition = LayerUtil.convertColumnPosition(natLayer, natFromPosition, layer.getPositionLayer());
int toPosition = LayerUtil.convertColumnPosition(natLayer, natToPosition, layer.getPositionLayer());
boolean result = !ColumnGroupUtils.isInTheSameGroup(
layer,
level,
fromPosition,
toPosition);
// special check for reordering to position 0 left of an unbreakable
// group
if (!result && fromPosition == toPosition && natFromPosition < natToPosition) {
result = true;
}
return result;
}
/**
* Checks if the edge of a column position is the left-most or the right
* most column on any level of a column group.
*
* @param columnGroupHeaderLayer
* The {@link ColumnGroupHeaderLayer} to handle the checks.
* @param toPosition
* The position to check. Needs to be related to the
* positionLayer.
* @param reorderToLeftEdge
* <code>true</code> if the check should be performed to the left
* edge or the right edge of the toPosition.
* @param moveDirection
* The direction in which the reordering is performed.
* @return <code>true</code> if the destination would be between two groups,
* <code>false</code> if the destination would be inside a group.
*
* @since 1.6
*/
public static boolean isBetweenTwoGroups(
ColumnGroupHeaderLayer columnGroupHeaderLayer, int toPosition, boolean reorderToLeftEdge, MoveDirectionEnum moveDirection) {
if ((toPosition == 0 && reorderToLeftEdge)
|| (toPosition == (columnGroupHeaderLayer.getPositionLayer().getColumnCount() - 1) && !reorderToLeftEdge)) {
// start or end of the table
return true;
}
int toPositionToCheck = toPosition;
if (reorderToLeftEdge
&& MoveDirectionEnum.RIGHT == moveDirection) {
toPositionToCheck--;
}
boolean valid = true;
for (int level = 0; level < columnGroupHeaderLayer.getLevelCount(); level++) {
if (MoveDirectionEnum.RIGHT == moveDirection) {
valid = !isInTheSameGroup(columnGroupHeaderLayer, level, toPosition, toPositionToCheck);
} else {
valid = !isInTheSameGroup(columnGroupHeaderLayer, level, toPosition, toPositionToCheck + (reorderToLeftEdge ? -1 : 1));
}
if (!valid) {
break;
}
}
return valid;
}
/**
* Checks if the edge of a column position for a specific grouping level is
* the left-most or the right most column of a column group.
*
* @param columnGroupHeaderLayer
* The {@link ColumnGroupHeaderLayer} to handle the checks.
* @param level
* The grouping level on which the check should be performed.
* @param toPosition
* The position to check. Needs to be related to the
* positionLayer.
* @param reorderToLeftEdge
* <code>true</code> if the check should be performed to the left
* edge or the right edge of the toPosition.
* @param moveDirection
* The direction in which the reordering is performed.
* @return <code>true</code> if the destination would be between two groups,
* <code>false</code> if the destination would be inside a group.
*
* @since 1.6
*/
public static boolean isBetweenTwoGroups(
ColumnGroupHeaderLayer columnGroupHeaderLayer, int level, int toPosition, boolean reorderToLeftEdge, MoveDirectionEnum moveDirection) {
if ((toPosition == 0 && reorderToLeftEdge)
|| (toPosition == (columnGroupHeaderLayer.getPositionLayer().getColumnCount() - 1) && !reorderToLeftEdge)) {
// start or end of the table
return true;
}
int toPositionToCheck = toPosition;
if (MoveDirectionEnum.RIGHT == moveDirection) {
if (reorderToLeftEdge) {
toPositionToCheck--;
} else {
toPositionToCheck++;
}
}
boolean valid = true;
if (MoveDirectionEnum.RIGHT == moveDirection) {
valid = !isInTheSameGroup(columnGroupHeaderLayer, level, toPosition, toPositionToCheck);
} else {
valid = !isInTheSameGroup(columnGroupHeaderLayer, level, toPosition, toPositionToCheck + (reorderToLeftEdge ? -1 : 1));
}
if (valid && level > 0) {
// check on deeper levels
valid = isBetweenTwoGroups(columnGroupHeaderLayer, level - 1, toPosition, reorderToLeftEdge, moveDirection);
}
return valid;
}
/**
* Checks if a reorder operation is valid by checking the unbreakable states
* of the groups below the from and the to position.
*
* @param columnGroupHeaderLayer
* The {@link ColumnGroupHeaderLayer} to get the groups to check.
* @param fromPosition
* The position from which a column should be reordered.
* @param toPosition
* The position to which a column should be reordered.
* @param reorderToLeftEdge
* <code>true</code> if the reorder should be performed to the
* left edge of the toPosition.
* @return <code>true</code> if the reorder operation would be valid,
* <code>false</code> if the either the source or the target belongs
* to an unbreakable group.
*
* @since 1.6
*/
public static boolean isReorderValid(ColumnGroupHeaderLayer columnGroupHeaderLayer, int fromPosition, int toPosition, boolean reorderToLeftEdge) {
for (int level = 0; level < columnGroupHeaderLayer.getLevelCount(); level++) {
if (!isReorderValid(columnGroupHeaderLayer, level, fromPosition, toPosition, reorderToLeftEdge)) {
return false;
}
}
return true;
}
/**
* Checks if a reorder operation is valid by checking the unbreakable states
* of the groups below the from and the to position.
*
* @param columnGroupHeaderLayer
* The {@link ColumnGroupHeaderLayer} to get the groups to check.
* @param level
* The grouping level that should be checked.
* @param fromPosition
* The position from which a column should be reordered.
* @param toPosition
* The position to which a column should be reordered.
* @param reorderToLeftEdge
* <code>true</code> if the reorder should be performed to the
* left edge of the toPosition.
* @return <code>true</code> if the reorder operation would be valid,
* <code>false</code> if the either the source or the target belongs
* to an unbreakable group.
*
* @since 1.6
*/
public static boolean isReorderValid(
ColumnGroupHeaderLayer columnGroupHeaderLayer, int level, int fromPosition, int toPosition, boolean reorderToLeftEdge) {
MoveDirectionEnum moveDirection = PositionUtil.getHorizontalMoveDirection(fromPosition, toPosition);
int toPositionToCheck = toPosition;
if (MoveDirectionEnum.RIGHT == moveDirection && reorderToLeftEdge
|| toPosition == columnGroupHeaderLayer.getPositionLayer().getColumnCount() && !reorderToLeftEdge) {
toPositionToCheck--;
}
boolean fromUnbreakable = false;
boolean valid = true;
// check if the from position is unbreakable
GroupModel model = columnGroupHeaderLayer.getGroupModel(level);
Group group = model.getGroupByPosition(fromPosition);
if (group != null && group.isUnbreakable() && group.getVisibleSpan() > 1) {
fromUnbreakable = true;
valid = isInTheSameGroup(columnGroupHeaderLayer, level, fromPosition, toPositionToCheck);
}
// if the from position is part of an unbreakable group, we already know
// the result
if (!fromUnbreakable) {
if ((toPosition == 0 && reorderToLeftEdge)
|| (toPosition == (columnGroupHeaderLayer.getPositionLayer().getColumnCount() - 1) && !reorderToLeftEdge)) {
// start or end of the table
return true;
}
// check if the to position is unbreakable
if (model.isPartOfAnUnbreakableGroup(toPositionToCheck)) {
// check if the original toPosition is in another group to
// see if we might reorder between groups
if (MoveDirectionEnum.RIGHT == moveDirection) {
valid = !isInTheSameGroup(columnGroupHeaderLayer, level, toPosition, toPositionToCheck);
} else {
valid = !isInTheSameGroup(columnGroupHeaderLayer, level, toPosition, toPositionToCheck + (reorderToLeftEdge ? -1 : 1));
}
}
}
return valid;
}
/**
* Check if a complete group is reordered.
*
* @param fromGroup
* The group to check.
* @param fromColumnPositions
* The column positions to check.
* @return <code>true</code> if the fromColumnPositions are all part of the
* given group.
* @since 1.6
*/
public static boolean isGroupReordered(Group fromGroup, int[] fromColumnPositions) {
int[] visiblePositions = fromGroup.getVisiblePositions();
if (visiblePositions.length > fromColumnPositions.length) {
return false;
} else if (visiblePositions.length < fromColumnPositions.length) {
MutableIntList fromPositions = IntLists.mutable.of(fromColumnPositions);
return fromPositions.containsAll(visiblePositions);
} else {
return Arrays.equals(visiblePositions, fromColumnPositions);
}
}
}