blob: d1dc7b561636495bf2a853c3488cb37b607410c3 [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.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
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.model.IRowGroup;
import org.eclipse.nebula.widgets.nattable.group.model.IRowGroupModel;
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.group.performance.RowGroupHeaderLayer;
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;
/**
* The utility methods in this class bridge the divide between the world of row
* indexes and positions and row objects used the model.
*
* @author Stefan Bolton
* @author Matt Biggs
*
*/
public final class RowGroupUtils {
private RowGroupUtils() {
// private default constructor for helper class
}
public static <T> IRowGroup<T> getRowGroupForRowIndex(final IRowGroupModel<T> model, final int rowIndex) {
final T row = model.getRowFromIndexCache(rowIndex);
return model.getRowGroupForRow(row);
}
public static <T> IRowGroup<T> getOwnRowGroupForRowIndex(final IRowGroupModel<T> model, final int rowIndex) {
final T row = model.getRowFromIndexCache(rowIndex);
IRowGroup<T> rowGroup = model.getRowGroupForRow(row);
// If this is a sub-group row, then rowGroup will currently point to the
// parent group.
// We need to find the real, sub-group this row belongs to.
if (rowGroup != null && !rowGroup.getOwnMemberRows(true).contains(row)) {
rowGroup = rowGroup.getRowGroupForRow(row);
}
return rowGroup;
}
public static <T> boolean isPartOfAGroup(final IRowGroupModel<T> model, final int rowIndex) {
final T row = model.getRowFromIndexCache(rowIndex);
if (row != null) {
return (model.getRowGroupForRow(row) != null);
}
return false;
}
public static <T> boolean isInTheSameGroup(final int fromRowIndex, final int toRowIndex, final IRowGroupModel<T> model) {
final T fromRow = model.getRowFromIndexCache(fromRowIndex);
final T toRow = model.getRowFromIndexCache(toRowIndex);
IRowGroup<T> rowGroupFrom = getTopMostParentGroup(model.getRowGroupForRow(fromRow));
IRowGroup<T> rowGroupTo = getTopMostParentGroup(model.getRowGroupForRow(toRow));
return rowGroupFrom != null
&& rowGroupTo != null
&& rowGroupFrom.equals(rowGroupTo);
}
/**
*
* @param model
* The {@link IRowGroupModel} the given {@link IRowGroup} belongs
* to.
* @param group
* The {@link IRowGroup} to check.
* @return <code>true</code> if the given row group or one of its parent
* groups is collapsed.
*/
public static <T> boolean isCollapsed(final IRowGroupModel<T> model, final IRowGroup<T> group) {
return group == null
|| group.isCollapsed()
|| isAnyParentCollapsed(group);
}
/**
*
* @param group
* The {@link IRowGroup} to check.
* @return <code>true</code> if any of the groups parent groups is
* collapsed.
*/
public static <T> boolean isAnyParentCollapsed(IRowGroup<T> group) {
boolean collapsed = false;
if (group != null) {
IRowGroup<T> topMostGroup = getTopMostParentGroup(group);
// Walk up the group hierarchy until we find a collapsed group.
while (!collapsed && group != topMostGroup) {
group = group.getParentGroup();
if (group == null) {
break;
}
collapsed = group.isCollapsed();
}
}
return collapsed;
}
/**
*
* @param model
* The {@link IRowGroupModel} to check.
* @param bodyRowIndex
* The index of a row whose row group should be inspected.
* @return The number of rows in the row group to which the given
* bodyRowIndex belongs to.
*/
public static <T> int sizeOfGroup(final IRowGroupModel<T> model, final int bodyRowIndex) {
IRowGroup<T> group = getRowGroupForRowIndex(model, bodyRowIndex);
if (group != null) {
return getTopMostParentGroup(group).getMemberRows(true).size();
} else {
return 0;
}
}
/**
*
* @param rowGroup
* The {@link IRowGroup} to check.
* @return the top-most parent group of the given group or the group
* specified if it has no parents.
*/
public static <T> IRowGroup<T> getTopMostParentGroup(final IRowGroup<T> rowGroup) {
return (rowGroup == null
? null
: (rowGroup.getParentGroup() == null)
? rowGroup
: getTopMostParentGroup(rowGroup.getParentGroup()));
}
/**
*
* @param model
* The {@link IRowGroupModel} to check.
* @param bodyRowIndex
* The index of a row whose row group should be inspected.
* @return <code>true</code> if <code>bodyRowIndex</code> is contained in
* the list of static rows of the row group this index belongs to
*/
public static <T> boolean isStaticRow(final IRowGroupModel<T> model, final int bodyRowIndex) {
final T row = model.getRowFromIndexCache(bodyRowIndex);
if (row != null) {
IRowGroup<T> group = model.getRowGroupForRow(row);
if (group != null) {
return group.getStaticMemberRows().contains(row);
}
}
return false;
}
public static boolean isRowIndexHiddenInUnderLyingLayer(int rowIndex, ILayer layer, IUniqueIndexLayer underlyingLayer) {
return underlyingLayer.getRowPositionByIndex(rowIndex) == -1;
}
/**
* Helper method to get the row positions for a specified layer
*
* If a row is currently invisible (-1) it will not be returned within the
* collection
*
* @param layer
* The layer for which the position transformation should be
* performed.
* @param bodyRowIndexes
* The row indexes for which the positions are requested.
* @return Unmodifiable list of the row positions for the given layer
*/
public static List<Integer> getRowPositionsInGroup(IUniqueIndexLayer layer, Collection<Integer> bodyRowIndexes) {
return Collections.unmodifiableList(bodyRowIndexes.stream()
.map(layer::getRowPositionByIndex)
.filter(pos -> pos != -1)
.sorted()
.collect(Collectors.toList()));
}
/**
*
* @param model
* The {@link IRowGroupModel} to check.
* @param rowIndex
* The index of a row whose row group should be inspected.
* @return Unmodifiable list of row indexes and static row indexes in the
* same group as this index
*/
public static <T> List<Integer> getRowIndexesInGroup(IRowGroupModel<T> model, int rowIndex) {
final IRowGroup<T> group = getRowGroupForRowIndex(model, rowIndex);
return getRowIndexesInGroup(model, group, true);
}
/**
* Return the row indexes of the rows that belong to a group.
*
* @param <T>
* The type of the objects in the backing data.
* @param model
* The {@link IRowGroupModel} to check.
* @param group
* The {@link IRowGroup} to check.
* @param includeStatic
* <code>true</code> if static rows should be included,
* <code>false</code> if not.
* @return The row indexes of the rows that belong to the given group.
*/
public static <T> List<Integer> getRowIndexesInGroup(IRowGroupModel<T> model, IRowGroup<T> group, boolean includeStatic) {
return Collections.unmodifiableList(group.getMemberRows(includeStatic).stream()
.map(model::getIndexFromRowCache)
.sorted()
.collect(Collectors.toList()));
}
/**
* Helper method to get the row positions for a specified layer
*
* If a row is currently invisible (-1) it will not be returned within the
* collection
*
* @param layer
* The layer for which the position transformation should be
* performed.
* @param bodyRowIndexes
* The row indexes for which the positions are requested.
* @return The row positions for the given layer.
* @since 2.0
*/
public static int[] getRowPositionsInGroup(IUniqueIndexLayer layer, int... bodyRowIndexes) {
return Arrays.stream(bodyRowIndexes)
.map(layer::getRowPositionByIndex)
.filter(pos -> pos != -1)
.sorted()
.toArray();
}
/**
*
* @param model
* The {@link IRowGroupModel} to check.
* @param rowIndex
* The index of a row whose row group should be inspected.
* @return The row indexes and static row indexes in the same group as this
* index.
* @since 2.0
*/
public static <T> int[] getRowIndexesInGroupAsArray(IRowGroupModel<T> model, int rowIndex) {
final IRowGroup<T> group = getRowGroupForRowIndex(model, rowIndex);
return getRowIndexesInGroupAsArray(model, group, true);
}
/**
* Return the row indexes of the rows that belong to a group.
*
* @param <T>
* The type of the objects in the backing data.
* @param model
* The {@link IRowGroupModel} to check.
* @param group
* The {@link IRowGroup} to check.
* @param includeStatic
* <code>true</code> if static rows should be included,
* <code>false</code> if not.
* @return The row indexes of the rows that belong to the given group.
* @since 2.0
*/
public static <T> int[] getRowIndexesInGroupAsArray(IRowGroupModel<T> model, IRowGroup<T> group, boolean includeStatic) {
return group.getMemberRows(includeStatic).stream()
.mapToInt(model::getIndexFromRowCache)
.sorted()
.toArray();
}
public static <T> String getRowGroupNameForIndex(IRowGroupModel<T> model, int bodyRowIndex) {
IRowGroup<T> group = getRowGroupForRowIndex(model, bodyRowIndex);
if (group != null) {
return group.getGroupName();
}
return null;
}
/**
* Checks if the two given row positions on the given layer belong to the
* same group at the given level on the given {@link RowGroupHeaderLayer}.
*
* @param layer
* The {@link RowGroupHeaderLayer} which is needed to perform the
* check against.
* @param level
* The grouping level to check.
* @param fromPosition
* The row position to check based on the position layer of the
* given {@link RowGroupHeaderLayer}.
* @param toPosition
* The row position to check based on the position layer of the
* given {@link RowGroupHeaderLayer}.
* @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(RowGroupHeaderLayer layer, int level, int fromPosition, int toPosition) {
Group fromGroup = layer.getGroupModel(level).getGroupByPosition(fromPosition);
Group toGroup = layer.getGroupModel(level).getGroupByPosition(toPosition);
return fromGroup != null && toGroup != null && fromGroup == toGroup;
}
/**
* Checks if the row positions at the given y coordinates belong to the same
* group or not.
*
* @param natLayer
* The layer to which the given positions match. Typically the
* NatTable itself.
* @param startY
* The y coordinate of the row that should be checked. Typically
* the drag start y coordinate.
* @param endY
* The y coordinate of the row that should be checked. Typically
* the drag end y coordinate.
* @param layer
* The {@link RowGroupHeaderLayer} which is needed to perform the
* check against.
* @param level
* The grouping level to check.
* @return <code>true</code> if there is a row group boundary between startY
* and endY, <code>false</code> if both positions are in the same
* group.
*
* @since 1.6
*/
public static boolean isBetweenTwoGroups(ILayer natLayer, int startY, int endY, RowGroupHeaderLayer layer, int level) {
int natFromPosition = natLayer.getRowPositionByY(startY);
int natToPosition = natLayer.getRowPositionByY(endY);
// convert grid position to layer position
int fromPosition = LayerUtil.convertRowPosition(natLayer, natFromPosition, layer.getPositionLayer());
int toPosition = LayerUtil.convertRowPosition(natLayer, natToPosition, layer.getPositionLayer());
boolean result = !RowGroupUtils.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 row position is the top-most or the bottom most
* row on any level of a row group.
*
* @param rowGroupHeaderLayer
* The {@link RowGroupHeaderLayer} to handle the checks.
* @param toPosition
* The position to check. Needs to be related to the
* positionLayer.
* @param reorderToTopEdge
* <code>true</code> if the check should be performed to the top
* edge or the bottom 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(
RowGroupHeaderLayer rowGroupHeaderLayer, int toPosition, boolean reorderToTopEdge, MoveDirectionEnum moveDirection) {
if ((toPosition == 0 && reorderToTopEdge)
|| (toPosition == (rowGroupHeaderLayer.getPositionLayer().getRowCount() - 1) && !reorderToTopEdge)) {
// start or end of the table
return true;
}
int toPositionToCheck = toPosition;
if (reorderToTopEdge
&& MoveDirectionEnum.DOWN == moveDirection) {
toPositionToCheck--;
}
boolean valid = true;
for (int level = 0; level < rowGroupHeaderLayer.getLevelCount(); level++) {
if (MoveDirectionEnum.DOWN == moveDirection) {
valid = !isInTheSameGroup(rowGroupHeaderLayer, level, toPosition, toPositionToCheck);
} else {
valid = !isInTheSameGroup(rowGroupHeaderLayer, level, toPosition, toPositionToCheck + (reorderToTopEdge ? -1 : 1));
}
if (!valid) {
break;
}
}
return valid;
}
/**
* Checks if the edge of a row position for a specific grouping level is the
* top-most or the bottom most row of a row group.
*
* @param rowGroupHeaderLayer
* The {@link RowGroupHeaderLayer} 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 reorderToTopEdge
* <code>true</code> if the check should be performed to the top
* edge or the bottom 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(
RowGroupHeaderLayer rowGroupHeaderLayer, int level, int toPosition, boolean reorderToTopEdge, MoveDirectionEnum moveDirection) {
if ((toPosition == 0 && reorderToTopEdge)
|| (toPosition == (rowGroupHeaderLayer.getPositionLayer().getRowCount() - 1) && !reorderToTopEdge)) {
// start or end of the table
return true;
}
int toPositionToCheck = toPosition;
if (MoveDirectionEnum.DOWN == moveDirection) {
if (reorderToTopEdge) {
toPositionToCheck--;
} else {
toPositionToCheck++;
}
}
boolean valid = true;
if (MoveDirectionEnum.DOWN == moveDirection) {
valid = !isInTheSameGroup(rowGroupHeaderLayer, level, toPosition, toPositionToCheck);
} else {
valid = !isInTheSameGroup(rowGroupHeaderLayer, level, toPosition, toPositionToCheck + (reorderToTopEdge ? -1 : 1));
}
if (valid && level > 0) {
// check on deeper levels
valid = isBetweenTwoGroups(rowGroupHeaderLayer, level - 1, toPosition, reorderToTopEdge, 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 rowGroupHeaderLayer
* The {@link RowGroupHeaderLayer} to get the groups to check.
* @param fromPosition
* The position from which a row should be reordered.
* @param toPosition
* The position to which a row should be reordered.
* @param reorderToTopEdge
* <code>true</code> if the reorder should be performed to the
* top 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(RowGroupHeaderLayer rowGroupHeaderLayer, int fromPosition, int toPosition, boolean reorderToTopEdge) {
for (int level = 0; level < rowGroupHeaderLayer.getLevelCount(); level++) {
if (!isReorderValid(rowGroupHeaderLayer, level, fromPosition, toPosition, reorderToTopEdge)) {
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 rowGroupHeaderLayer
* The {@link RowGroupHeaderLayer} to get the groups to check.
* @param level
* The grouping level that should be checked.
* @param fromPosition
* The position from which a row should be reordered.
* @param toPosition
* The position to which a row should be reordered.
* @param reorderToTopEdge
* <code>true</code> if the reorder should be performed to the
* top 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(
RowGroupHeaderLayer rowGroupHeaderLayer, int level, int fromPosition, int toPosition, boolean reorderToTopEdge) {
MoveDirectionEnum moveDirection = PositionUtil.getVerticalMoveDirection(fromPosition, toPosition);
int toPositionToCheck = toPosition;
if (MoveDirectionEnum.DOWN == moveDirection && reorderToTopEdge
|| toPosition == rowGroupHeaderLayer.getPositionLayer().getRowCount() && !reorderToTopEdge) {
toPositionToCheck--;
}
boolean fromUnbreakable = false;
boolean valid = true;
// check if the from position is unbreakable
GroupModel model = rowGroupHeaderLayer.getGroupModel(level);
Group group = model.getGroupByPosition(fromPosition);
if (group != null && group.isUnbreakable() && group.getVisibleSpan() > 1) {
fromUnbreakable = true;
valid = isInTheSameGroup(rowGroupHeaderLayer, level, fromPosition, toPositionToCheck);
}
// if the from position is part of an unbreakable group, we already know
// the result
if (!fromUnbreakable) {
if ((toPosition == 0 && reorderToTopEdge)
|| (toPosition == (rowGroupHeaderLayer.getPositionLayer().getRowCount() - 1) && !reorderToTopEdge)) {
// 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.DOWN == moveDirection) {
valid = !isInTheSameGroup(rowGroupHeaderLayer, level, toPosition, toPositionToCheck);
} else {
valid = !isInTheSameGroup(rowGroupHeaderLayer, level, toPosition, toPositionToCheck + (reorderToTopEdge ? -1 : 1));
}
}
}
return valid;
}
/**
* Check if a complete group is reordered.
*
* @param fromGroup
* The group to check.
* @param fromPositions
* The positions to check.
* @return <code>true</code> if the fromPositions are all part of the given
* group.
* @since 1.6
*/
public static boolean isGroupReordered(Group fromGroup, int[] fromPositions) {
int[] visiblePositions = fromGroup.getVisiblePositions();
if (visiblePositions.length > fromPositions.length) {
return false;
} else if (visiblePositions.length < fromPositions.length) {
MutableIntList from = IntLists.mutable.of(fromPositions);
return from.containsAll(visiblePositions);
} else {
return Arrays.equals(visiblePositions, fromPositions);
}
}
}