blob: 8ea11f6cdaf379fd9d947ed78657f9ec5dc5cbe3 [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.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.StringTokenizer;
import org.eclipse.collections.api.list.primitive.MutableIntList;
import org.eclipse.collections.api.set.primitive.IntSet;
import org.eclipse.collections.api.set.primitive.MutableIntSet;
import org.eclipse.collections.impl.factory.primitive.IntLists;
import org.eclipse.collections.impl.factory.primitive.IntSets;
import org.eclipse.nebula.widgets.nattable.layer.IUniqueIndexLayer;
import org.eclipse.nebula.widgets.nattable.persistence.IPersistable;
/**
* Model implementation to track groups of columns/rows.
*
* @since 1.6
*/
public class GroupModel implements IPersistable {
/**
* Persistence key for persisting the group model states.
*/
private static final String PERSISTENCE_KEY_GROUP_MODEL = ".groupModel"; //$NON-NLS-1$
/**
* Converter to support layer based position-index conversion.
*/
protected IndexPositionConverter indexPositionConverter;
/**
* Flag to configure whether newly created groups should be initially
* expanded or collapsed.
*/
private boolean defaultCollapseable = true;
/**
* Flag to configure whether newly created groups should be initially
* unbreakable or not.
*/
private boolean defaultUnbreakable = false;
/**
* Collection of groups managed by this GroupModel.
*/
private final List<Group> groups = new LinkedList<>();
/**
*
* @return The unmodifiable list of {@link Group}s contained in this
* {@link GroupModel}.
*/
List<Group> getGroups() {
return Collections.unmodifiableList(this.groups);
}
/**
* This method is typically called by a group header layer to ensure that
* the cell positions match the underlying scrollable layer below the
* ViewportLayer.
*
* @param converter
* Converter to support layer based position-index conversion.
*/
void setIndexPositionConverter(IndexPositionConverter converter) {
this.indexPositionConverter = converter;
// update the visible start positions of the already registered groups
updateVisibleStartPositions();
}
/**
* Converts the given position to the corresponding index in case the
* {@link #indexPositionConverter} is set. Otherwise simply returns the
* given position.
*
* @param position
* The position to convert.
* @return The index that corresponds to the given position or the position
* itself if a conversion is not possible.
*/
int getIndexByPosition(int position) {
if (this.indexPositionConverter != null) {
return this.indexPositionConverter.convertPositionToIndex(position);
}
return position;
}
/**
* Converts the given index to the corresponding position in case the
* {@link #indexPositionConverter} is set. Otherwise simply returns the
* given index.
*
* @param index
* The index to convert.
* @return The position for the given index or the position itself if a
* conversion is not possible.
*/
int getPositionByIndex(int index) {
if (this.indexPositionConverter != null) {
return this.indexPositionConverter.convertIndexToPosition(index);
}
return index;
}
/**
* Updates the visible start position of all {@link Group}s based on the
* currently set visible start index.
*/
void updateVisibleStartPositions() {
for (Group group : this.groups) {
group.updateVisibleStartPosition();
}
}
/**
* Executes a consistency check for the configured groups to ensure that the
* current visible state matches the position state. Should only be
* triggered in case it is expected that the consistency is not given, e.g.
* if hide events without details where triggered.
*
* @param updateStartIndex
* flag to indicate if the start index of the group should also
* be updated. Needed in case of complete structural refreshes to
* be able to reset the group completely.
*/
void performConsistencyCheck(boolean updateStartIndex) {
for (Group group : this.groups) {
group.consistencyCheck(updateStartIndex);
}
}
@Override
public void saveState(String prefix, Properties properties) {
StringBuilder strBuilder = new StringBuilder();
for (Group group : this.groups) {
String groupName = group.getName();
// if this group has no valid start index, continue without
// saving state.
// A group can have an invalid start index if groups are used to
// organize columns/rows on a higher abstraction level ...
if (group.getStartIndex() < 0) {
continue;
}
strBuilder.append(groupName);
strBuilder.append('=');
strBuilder.append(group.startIndex).append(':');
strBuilder.append(group.visibleStartIndex).append(':');
strBuilder.append(group.visibleStartPosition).append(':');
strBuilder.append(group.originalSpan).append(':');
strBuilder.append(group.visibleSpan).append(':');
strBuilder.append(group.collapsed ? "collapsed" : "expanded"); //$NON-NLS-1$ //$NON-NLS-2$
strBuilder.append(':');
strBuilder.append(group.collapseable ? "collapseable" : "uncollapseable"); //$NON-NLS-1$ //$NON-NLS-2$
strBuilder.append(':');
strBuilder.append(group.unbreakable ? "unbreakable" : "breakable"); //$NON-NLS-1$ //$NON-NLS-2$
if (!group.staticIndexes.isEmpty()) {
strBuilder.append(':').append(group.staticIndexes.toSortedList().makeString(IPersistable.VALUE_SEPARATOR));
}
strBuilder.append('|');
}
properties.setProperty(prefix + PERSISTENCE_KEY_GROUP_MODEL, strBuilder.toString());
}
@Override
public void loadState(String prefix, Properties properties) {
String property = properties.getProperty(prefix + PERSISTENCE_KEY_GROUP_MODEL);
if (property != null) {
clear();
StringTokenizer groupTokenizer = new StringTokenizer(property, "|"); //$NON-NLS-1$
while (groupTokenizer.hasMoreTokens()) {
String groupToken = groupTokenizer.nextToken();
int separatorIndex = groupToken.indexOf('=');
// group name
String groupName = groupToken.substring(0, separatorIndex);
String[] groupProperties = groupToken.substring(separatorIndex + 1).split(":"); //$NON-NLS-1$
String state = groupProperties[0];
int startIndex = Integer.parseInt(state);
state = groupProperties[1];
int visibleStartIndex = Integer.parseInt(state);
state = groupProperties[2];
int visibleStartPosition = Integer.parseInt(state);
state = groupProperties[3];
int originalSpan = Integer.parseInt(state);
state = groupProperties[4];
int visibleSpan = Integer.parseInt(state);
Group group = new Group(groupName, startIndex, originalSpan);
this.groups.add(group);
group.visibleStartIndex = visibleStartIndex;
group.visibleStartPosition = visibleStartPosition;
group.visibleSpan = visibleSpan;
// Expanded/collapsed
state = groupProperties[5];
if ("collapsed".equals(state)) { //$NON-NLS-1$
group.collapsed = true;
} else if ("expanded".equals(state)) { //$NON-NLS-1$
group.collapsed = false;
} else {
throw new IllegalArgumentException(state + " not one of 'expanded' or 'collapsed'"); //$NON-NLS-1$
}
// collapseble / uncollapseable
state = groupProperties[6];
if ("collapseable".equals(state)) { //$NON-NLS-1$
group.collapseable = true;
} else if ("uncollapseable".equals(state)) { //$NON-NLS-1$
group.collapseable = false;
} else {
throw new IllegalArgumentException(state + " not one of 'uncollapseable' or 'collapseable'"); //$NON-NLS-1$
}
// breakable / unbreakable
state = groupProperties[7];
if ("breakable".equals(state)) { //$NON-NLS-1$
group.unbreakable = false;
} else if ("unbreakable".equals(state)) { //$NON-NLS-1$
group.unbreakable = true;
} else {
throw new IllegalArgumentException(state + " not one of 'breakable' or 'unbreakable'"); //$NON-NLS-1$
}
if (groupProperties.length == 9) {
String statics = groupProperties[8];
StringTokenizer staticTokenizer = new StringTokenizer(statics, ","); //$NON-NLS-1$
while (staticTokenizer.hasMoreTokens()) {
int index = Integer.parseInt(staticTokenizer.nextToken());
group.staticIndexes.add(index);
}
}
}
}
}
/**
* Adds the given positions to the given group.
*
* @param group
* The {@link Group} to which the positions should be added.
* @param positions
* The positions to add.
*/
public void addPositionsToGroup(Group group, int... positions) {
if (group != null && !group.isUnbreakable()) {
// first sort the positions to ensure we check in the correct order
Arrays.sort(positions);
// separate position arrays by start position
MutableIntList beforeStartPosition = IntLists.mutable.empty();
MutableIntList afterStartPosition = IntLists.mutable.empty();
for (int pos : positions) {
if (pos < group.getVisibleStartPosition()) {
beforeStartPosition.add(pos);
} else {
afterStartPosition.add(pos);
}
}
// iterate backwards before start position
for (int i = beforeStartPosition.size() - 1; i >= 0; i--) {
int pos = beforeStartPosition.get(i);
// check if position is directly before start
// if there is a gap stop processing
if (pos == group.getVisibleStartPosition() - 1) {
group.setOriginalSpan(group.getOriginalSpan() + 1);
group.setVisibleSpan(group.getVisibleSpan() + 1);
int index = getIndexByPosition(pos);
// add index to group members
group.members.add(index);
group.setStartIndex(index);
group.setVisibleStartIndex(index);
group.updateVisibleStartPosition();
} else {
break;
}
}
// iterate forward after group end
for (int pos : afterStartPosition.toArray()) {
int nextPos = group.getVisibleStartPosition() + group.getVisibleSpan();
// check for gap
if (pos == nextPos) {
group.setOriginalSpan(group.getOriginalSpan() + 1);
group.setVisibleSpan(group.getVisibleSpan() + 1);
// add index to group members
group.members.add(getIndexByPosition(nextPos));
} else {
// there is a gap so we break and do not update
break;
}
}
}
}
/**
* Removes the given positions from the given group.
* <p>
* <b>Note:</b><br>
* A removal will only happen for positions at the beginning or the end of a
* group. Removing a position in the middle will cause removal of positions
* 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 group
* The {@link Group} from which the positions should be removed.
* @param positions
* The positions to remove.
*/
public void removePositionsFromGroup(Group group, int... positions) {
if (group != null && !group.isUnbreakable()) {
Arrays.sort(positions);
IntSet visiblePositions = IntSets.immutable.of(group.getVisiblePositions());
for (int i = positions.length - 1; i >= 0; i--) {
int pos = positions[i];
if (visiblePositions.contains(pos)) {
int index = getIndexByPosition(pos);
if (index == group.getStartIndex()) {
// the start index was removed, we need to update the
// start index
group.setStartIndex(getIndexByPosition(pos + 1));
group.members.remove(index);
group.staticIndexes.remove(index);
} else {
int memberIndex = getIndexByPosition(group.getVisibleStartPosition() + group.getVisibleSpan() - 1);
group.members.remove(memberIndex);
group.staticIndexes.remove(memberIndex);
}
group.setOriginalSpan(group.getOriginalSpan() - 1);
group.setVisibleSpan(group.getVisibleSpan() - 1);
// the visible start index was removed, we need to update
if (index == group.getVisibleStartIndex()) {
if (group.getOriginalSpan() > 0) {
group.setVisibleStartIndex(getIndexByPosition(pos + 1));
} else {
// all members where removed
group.setStartIndex(-1);
group.setVisibleStartIndex(-1);
removeGroup(group);
}
}
group.updateVisibleStartPosition();
}
}
}
}
/**
* Removes the given positions from corresponding groups. Searches the
* groups by position and removes the position in case the group is not
* unbreakable.
* <p>
* <b>Note:</b><br>
* A removal will only happen for positions at the beginning or the end of a
* group. Removing a position in the middle will cause removal of positions
* 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 positions
* The positions to remove.
* @return The collection of {@link Group}s that have been modified.
*/
public Collection<Group> removePositionsFromGroup(int... positions) {
Set<Group> changed = new HashSet<>();
Group group = null;
Arrays.sort(positions);
for (int i = positions.length - 1; i >= 0; i--) {
int pos = positions[i];
group = getGroupByPosition(pos);
if (group != null && !group.isUnbreakable()) {
int index = getIndexByPosition(pos);
if (index == group.getStartIndex()) {
// the start index was removed, we need to update the
// start index
group.setStartIndex(getIndexByPosition(pos + 1));
group.members.remove(index);
group.staticIndexes.remove(index);
} else {
int memberIndex = getIndexByPosition(group.getVisibleStartPosition() + group.getVisibleSpan() - 1);
group.members.remove(memberIndex);
group.staticIndexes.remove(memberIndex);
}
group.setOriginalSpan(group.getOriginalSpan() - 1);
group.setVisibleSpan(group.getVisibleSpan() - 1);
// the visible start index was removed, we need to update
if (index == group.getVisibleStartIndex()) {
if (group.getOriginalSpan() > 0) {
group.setVisibleStartIndex(getIndexByPosition(pos + 1));
} else {
// all members where removed
group.setStartIndex(-1);
group.setVisibleStartIndex(-1);
removeGroup(group);
}
}
group.updateVisibleStartPosition();
changed.add(group);
}
}
return changed;
}
/**
* This method will add static indexes to the given group.
* <p>
* Static indexes remain visible when a group is collapsed.
* </p>
*
* @param groupName
* The name of the group on which the static indexes should be
* inserted.
* @param indexes
* The static indexes to add.
*/
public void addStaticIndexesToGroup(String groupName, int... indexes) {
Group group = getGroupByName(groupName);
if (group != null) {
addStaticIndexesToGroup(group, indexes);
}
}
/**
* This method will add static indexes to the given group.
* <p>
* Static indexes remain visible when a group is collapsed.
* </p>
*
* @param position
* The position of a group on which the static indexes should be
* inserted.
* @param indexes
* The static indexes to add.
*/
public void addStaticIndexesToGroup(int position, int... indexes) {
Group group = getGroupByPosition(position);
if (group != null) {
addStaticIndexesToGroup(group, indexes);
}
}
/**
* This method will add static indexes to the given group.
* <p>
* Static indexes remain visible when a group is collapsed.
* </p>
*
* @param group
* The group on which the static indexes should be inserted.
* @param indexes
* The static indexes to add.
*/
public void addStaticIndexesToGroup(Group group, int... indexes) {
int[] staticIndexes = Arrays.stream(indexes)
.map(this::getPositionByIndex)
.filter(pos -> (pos >= group.getVisibleStartPosition()
&& pos < (group.getVisibleStartPosition() + group.getVisibleSpan())))
.toArray();
if (staticIndexes.length > 0) {
group.staticIndexes.addAll(staticIndexes);
}
}
// Getters
/**
*
* @param groupName
* 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 groupName) {
for (Group group : this.groups) {
if (group.getName().equals(groupName)) {
return group;
}
}
return null;
}
/**
* Checks if the given position is part of a group and returns the group if
* the position is part of it.
*
* @param position
* The position to check.
* @return The Group to which the given position belongs to or
* <code>null</code> if the position is not part of a group.
*/
public Group getGroupByPosition(int position) {
for (Group group : this.groups) {
// first check the visible start position of the group
if (position == group.getVisibleStartPosition()
|| (position >= group.getVisibleStartPosition() && position < (group.getVisibleStartPosition() + group.getVisibleSpan()))) {
return group;
}
}
return null;
}
/**
* Checks if there is a group that has the given index as static index.
* <p>
* <b>Note:</b> This method iterates over all groups and checks if the
* static index collection contains the given index. Could have a bad
* performance in case of huge groups.
* </p>
*
* @param staticIndex
* The index to check.
* @return The Group in which the given index is configured as static index
* or <code>null</code> if the index is not a static index in any
* group.
*/
public Group getGroupByStaticIndex(int staticIndex) {
for (Group group : this.groups) {
if (group.staticIndexes.contains(staticIndex)) {
return group;
}
}
return null;
}
/**
* Searches for a group that has a given member index.
* <p>
* <b>Note:</b> This method iterates over all groups and checks if the
* member collection contains the given index. Could have a bad performance
* in case of huge groups.
* </p>
*
* @param memberIndex
* The index to check.
* @return The Group that contains the given index or <code>null</code> if
* the index is not a member in any group.
*/
public Group findGroupByMemberIndex(int memberIndex) {
for (Group group : this.groups) {
if (group.hasMember(memberIndex)) {
return group;
}
}
return null;
}
/**
* Check if the given position is part of a group in this
* {@link GroupModel}.
*
* @param position
* The position to check.
* @return <code>true</code> if the position is part of a group,
* <code>false</code> if not.
*/
public boolean isPartOfAGroup(int position) {
Group group = getGroupByPosition(position);
return group != null;
}
/**
* 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) {
Group group = new Group(groupName, startIndex, span);
group.collapseable = this.defaultCollapseable;
group.unbreakable = this.defaultUnbreakable;
addGroup(group);
}
/**
* Adds the given group.
*
* @param group
* The group to add.
*/
public void addGroup(Group group) {
this.groups.add(group);
}
/**
* Removes the group identified by the given name.
*
* @param groupName
* The name of the group to remove.
* @return The group that was removed from the model.
*/
public Group removeGroup(String groupName) {
Group group = getGroupByName(groupName);
if (group != null) {
removeGroup(group);
}
return group;
}
/**
* Removes the group identified by the given position.
*
* @param position
* The group that contains the given position.
* @return The group that was removed from the model.
*/
public Group removeGroup(int position) {
Group group = getGroupByPosition(position);
if (group != null) {
removeGroup(group);
}
return group;
}
/**
* Removes the given group.
*
* @param group
* The group to remove.
*/
public void removeGroup(Group group) {
this.groups.remove(group);
}
/**
* Removes all groups from this {@link GroupModel}.
*/
public void clear() {
this.groups.clear();
}
/**
* @return Number of {@link Group}s configured in this {@link GroupModel}.
*/
public int size() {
return this.groups.size();
}
/**
* @return <code>true</code> if no group is configured in this
* {@link GroupModel}.
*/
public boolean isEmpty() {
return this.groups.isEmpty();
}
/**
* Checks if the given position is configured to be static in one of the
* groups.
*
* @param position
* The position to check.
* @return <code>true</code> if the given position is configured to be
* static in a group.
*/
public boolean isStatic(int position) {
Group group = getGroupByPosition(position);
if (group != null) {
return group.staticIndexes.contains(getIndexByPosition(position));
}
return false;
}
/**
* Check if the specified position belongs to a {@link Group} and if this
* {@link Group} is collabseable.
*
* @param position
* The position used to retrieve the corresponding group.
* @return <code>true</code> if the specified position belongs to a
* {@link Group} and this {@link Group} is collabseable,
* <code>false</code> if not.
*/
public boolean isPartOfACollapseableGroup(int position) {
Group group = getGroupByPosition(position);
if (group != null) {
return group.isCollapseable();
}
return false;
}
/**
* Set the {@link 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) {
Group group = getGroupByName(groupName);
if (group != null) {
group.setCollapseable(collabseable);
}
}
/**
* Set the {@link 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) {
Group group = getGroupByPosition(position);
if (group != null) {
group.setCollapseable(collabseable);
}
}
/**
* Check if the specified position belongs to a {@link Group} and if this
* {@link Group} is unbreakable.
*
* @param position
* The position used to retrieve the corresponding group.
* @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 position) {
Group group = getGroupByPosition(position);
if (group != null) {
return group.isUnbreakable();
}
return false;
}
/**
* 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) {
Group group = getGroupByName(groupName);
if (group != null) {
group.setUnbreakable(unbreakable);
}
}
/**
* Set the {@link 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) {
Group group = getGroupByPosition(position);
if (group != null) {
group.setUnbreakable(unbreakable);
}
}
/**
*
* @return The default value for the collapseable flag of newly created
* {@link Group} objects.
*/
public boolean isDefaultCollapseable() {
return this.defaultCollapseable;
}
/**
* Sets the default value for the collapseable flag when creating
* {@link Group} objects.
*
* @param defaultCollapseable
* the default value for {@link Group#collapseable} that should
* be set on creating {@link Group}.
*/
public void setDefaultCollapseable(boolean defaultCollapseable) {
this.defaultCollapseable = defaultCollapseable;
}
/**
*
* @return The default value for the unbreakable flag of newly created
* {@link Group} objects.
*/
public boolean isDefaultUnbreakable() {
return this.defaultUnbreakable;
}
/**
* Sets the default value for the unbreakable flag when creating
* {@link Group} objects.
*
* @param defaultUnbreakable
* the default value for {@link Group#unbreakable} that should be
* set on creating {@link Group}.
*/
public void setDefaultUnbreakable(boolean defaultUnbreakable) {
this.defaultUnbreakable = defaultUnbreakable;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("Group Model:\n"); //$NON-NLS-1$
for (Group group : this.groups) {
builder.append(group);
}
return builder.toString();
}
/**
* Model class to track the states of a group. Only carries the state, does
* not contain logic with regards to changing the states.
*/
public class Group {
/**
* The name of the group. Typically used as value in the cell.
*/
private String name;
/**
* The index of the first item in the group.
*/
private int startIndex;
/**
* The index of the first visible item in the group. Could differ from
* {@link #startIndex} if that item is hidden.
*/
private int visibleStartIndex;
/**
* The position of the first visible item in the group matching the
* position layer of the {@link GroupModel}. Needed in case the first
* column of a group is hidden for example.
*/
private int visibleStartPosition;
/**
* The configured number of items that belong to this group.
*/
private int originalSpan;
/**
* The number of items that are currently visible in this group. Might
* differ from the {@link #originalSpan} if columns/rows are hidden.
*/
private int visibleSpan;
/**
* The indexes that remain visible when collapsing this group.
*/
private final MutableIntSet staticIndexes = IntSets.mutable.empty();
/**
* Flag to configure whether this group can be collapsed or not.
*/
private boolean collapseable = true;
/**
* Flag that indicates whether this group is collapsed or not.
*/
private boolean collapsed = false;
/**
* Flag to configure whether this group can be broken or not. If a group
* is marked as unbreakable, the composition of the group cannot be
* changed. This means the originalSpan cannot be changed by adding or
* removing items. Items can be reordered within the group.
*/
private boolean unbreakable = false;
/**
* Indexes of the members of this group. Only for internal use in case
* hide operations performed at the end of a table lead to an
* inconsistent group state.
*/
private final MutableIntSet members = IntSets.mutable.empty();
/**
*
* @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.
*/
Group(String groupName, int startIndex, int span) {
this.name = groupName;
this.startIndex = startIndex;
this.visibleStartIndex = startIndex;
this.visibleStartPosition = getPositionByIndex(startIndex);
this.originalSpan = span;
this.visibleSpan = span;
for (int pos = this.visibleStartPosition; pos < this.visibleStartPosition + this.visibleSpan; pos++) {
this.members.add(getIndexByPosition(pos));
}
}
/**
* Returns the name of this group. Typically this value is used as value
* in the cell.
*
* @return The name of this group.
*/
public String getName() {
return this.name;
}
/**
* Set the name of this group. The value is typically used as value in
* the cell.
*
* @param name
* The name of the group.
*/
public void setName(String name) {
this.name = name;
}
/**
* Returns whether the group is collapsed or not.
*
* @return <code>true</code> if this group is collapsed,
* <code>false</code> if it is expanded.
*/
public boolean isCollapsed() {
return this.collapsed;
}
/**
* Set the collapsed state of this group.
*
* @param collapsed
* <code>true</code> to set the group in the collapsed state,
* <code>false</code> to set it to the expanded state.
*/
public void setCollapsed(boolean collapsed) {
if (this.collapseable) {
this.collapsed = collapsed;
}
}
/**
* Toggles the collapsed state.
*/
public void toggleCollapsed() {
setCollapsed(!this.collapsed);
}
/**
*
* @return <code>true</code> if this {@link Group} can be collapsed,
* <code>false</code> if not.
*/
public boolean isCollapseable() {
return this.collapseable;
}
/**
* Configure this {@link Group} whether it can be collapsed or not.
*
* @param collapseable
* <code>true</code> if this {@link Group} can be collapsed,
* <code>false</code> if not.
*/
public void setCollapseable(boolean collapseable) {
this.collapseable = collapseable;
if (!this.collapseable && this.isCollapsed()) {
// expand if the group is set to be not collapseable but
// currently collapsed
this.collapsed = false;
}
}
/**
*
* @return <code>true</code> if this {@link Group} can not be changed.
* <code>false</code> if the number of items in this
* {@link Group} can be modified.
*/
public boolean isUnbreakable() {
return this.unbreakable;
}
/**
*
* @param unbreakable
* <code>true</code> if this {@link Group} should not be
* changeable. <code>false</code> if the number of items in
* this {@link Group} should be changeable.
*/
public void setUnbreakable(boolean unbreakable) {
this.unbreakable = unbreakable;
}
/**
*
* @return The index of the first item in the group.
*/
public int getStartIndex() {
return this.startIndex;
}
/**
* Sets the index of the first item in the group. Needed in case of
* reordering or if the first item is removed from the group.
*
* @param startIndex
* The index of the first item in the group.
*/
public void setStartIndex(int startIndex) {
this.startIndex = startIndex;
}
/**
*
* @return The index of the first visible item in the group. Could
* differ from {@link #startIndex} if that item is hidden.
*/
public int getVisibleStartIndex() {
return this.visibleStartIndex;
}
/**
* Sets the index of the first visible item in the group. Needed in case
* the first real item in the group is hidden.
*
* @param visibleStartIndex
* The index of the first visible item in the group.
*/
public void setVisibleStartIndex(int visibleStartIndex) {
this.visibleStartIndex = visibleStartIndex;
}
/**
*
* @return The position of the first visible item in the group matching
* the position layer of the GroupModel. Needed in case the
* first column of a group is hidden for example.
*/
public int getVisibleStartPosition() {
return this.visibleStartPosition;
}
/**
* Updates the visible start position based on the currently set visible
* start index.
*/
void updateVisibleStartPosition() {
int startPosition = getPositionByIndex(this.startIndex);
if (startPosition >= 0) {
this.visibleStartIndex = this.startIndex;
}
this.visibleStartPosition = getPositionByIndex(this.visibleStartIndex);
if (this.visibleStartPosition == -1) {
// if a multi hide command was triggered for non-contiguous
// column ranges, where one range is at the end, the group could
// be in an inconsistent state which needs to be corrected.
consistencyCheck(false);
}
}
/**
* Triggers a consistency check on the group based on the locally stored
* group member indexes. Needed in case events where not processed in
* the corresponding header layer because of position transformations,
* which then lead to an inconsistent group state.
*
* @param updateStartIndex
* flag to indicate if the start index of the group should
* also be updated. Needed in case of complete structural
* refreshes to be able to reset the group completely.
*/
void consistencyCheck(boolean updateStartIndex) {
// check if the member indexes are all visible
MutableIntList memberPositions = this.members
.collectInt(member -> getPositionByIndex(member), IntLists.mutable.empty());
int hidden = memberPositions.count(pos -> pos == -1);
int smallestPosition = memberPositions.select(pos -> pos >= 0).minIfEmpty(-1);
setVisibleSpan(this.originalSpan - hidden);
int smallestIndex = getIndexByPosition(smallestPosition);
if (updateStartIndex
|| (this.startIndex < 0 && smallestIndex >= 0)) {
setStartIndex(smallestIndex);
}
setVisibleStartIndex(smallestIndex);
this.visibleStartPosition = smallestPosition;
}
/**
*
* @return The indexes of the members in this collection. Not modifiable
* to avoid side effects from the outside of the GroupModel.
*/
int[] getMembers() {
return this.members.toSortedArray();
}
/**
* Adds the given member indexes to the local list of members that are
* needed for consistency checks.
*
* @param memberIndexes
* The indexes of the positions that should be added to the
* local group members.
*/
void addMembers(int... memberIndexes) {
this.members.addAll(memberIndexes);
}
/**
* Removes the given member indexes from the local list of members that
* are needed for consistency checks.
*
* @param memberIndexes
* The indexes of the positions that should be removed from
* the local group members.
*/
void removeMembers(int... memberIndexes) {
this.members.removeAll(memberIndexes);
}
/**
* Check if the given index is a member of this {@link Group} or not.
*
* @param memberIndex
* The index to check.
* @return <code>true</code> if the given index is a member of this
* {@link Group}, <code>false</code> if not.
*/
public boolean hasMember(int memberIndex) {
return this.members.contains(memberIndex);
}
/**
*
* @return The configured number of items that belong to this group.
*/
public int getOriginalSpan() {
return this.originalSpan;
}
/**
*
* @param originalSpan
* The number of items that should belong to this group.
*/
public void setOriginalSpan(int originalSpan) {
this.originalSpan = originalSpan;
}
/**
*
* @return The number of items that are currently visible in this
* {@link Group}. Might differ from the {@link #originalSpan} if
* columns/rows are hidden.
*/
public int getVisibleSpan() {
return this.visibleSpan;
}
/**
*
* @param visibleSpan
* The number of items that are currently visible in this
* {@link Group}. Cannot be bigger than the original span.
*/
public void setVisibleSpan(int visibleSpan) {
if (visibleSpan <= this.originalSpan) {
this.visibleSpan = visibleSpan;
}
}
/**
*
* @return <code>true</code> if this {@link Group} has no spanning,
* <code>false</code> if at least one position is part of this
* {@link Group}.
*/
public boolean isEmpty() {
return this.originalSpan == 0;
}
/**
* Adds the given indexes as static indexes to this group. Static
* indexes are the indexes that stay visible when the group is
* collapsed.
*
* @param indexes
* The static indexes to add.
*/
public void addStaticIndexes(int... indexes) {
this.staticIndexes.addAll(indexes);
}
/**
* Removes the given indexes as static indexes from this group.
*
* @param indexes
* The static indexes to remove.
*/
public void removeStaticIndexes(int... indexes) {
this.staticIndexes.removeAll(indexes);
}
/**
* Checks if the given index is a static index of this group.
*
* @param index
* The index to check.
* @return <code>true</code> if the given index is configured as static
* index of this group, <code>false</code> if not.
* @since 2.0
*/
public boolean containsStaticIndex(int index) {
return this.staticIndexes.contains(index);
}
/**
* @return The indexes that remain visible when collapsing this group.
* @since 2.0
*/
public int[] getStaticIndexes() {
return this.staticIndexes.toSortedArray();
}
/**
*
* @return The positions of the visible items in the group matching the
* position layer of the GroupModel.
* @since 2.0
*/
public int[] getVisiblePositions() {
int[] groupPositions = new int[this.visibleSpan];
int i = 0;
for (int pos = this.visibleStartPosition; pos < (this.visibleStartPosition + this.visibleSpan); pos++) {
groupPositions[i] = pos;
i++;
}
return groupPositions;
}
/**
*
* @return The indexes of the positions that are currently visible.
* @since 2.0
*/
public int[] getVisibleIndexes() {
int[] groupIndexes = new int[this.visibleSpan];
int i = 0;
for (int pos = this.visibleStartPosition; pos < (this.visibleStartPosition + this.visibleSpan); pos++) {
groupIndexes[i] = getIndexByPosition(pos);
i++;
}
return groupIndexes;
}
/**
* Searches for the position of the last item in this group for the
* given layer. Needed as the Group positions are based on the
* positionLayer, but on transporting commands down the layer stack the
* positions might need to be converted, e.g. to find the last item even
* if it is hidden.
*
* @param layer
* The layer needed for index-position-transformation.
* @return The position of the last item in this group.
*
* @since 2.0
*/
public int getGroupEndPosition(IUniqueIndexLayer layer) {
return this.members
.collectInt(layer::getColumnPositionByIndex, IntSets.mutable.empty())
.maxIfEmpty(-1);
}
/**
* Checks if the given position is the left/top most position of this
* group. This actually means if the given position is the visible start
* position.
*
* @param position
* The position to check.
* @return <code>true</code> if the given position is the left/top most
* position of this group, <code>false</code> if not.
*/
public boolean isGroupStart(int position) {
return position == this.visibleStartPosition;
}
/**
* Checks if the given position is the right/bottom most position of
* this group. This actually means if the given position is the visible
* start position + visible span.
*
* @param position
* The position to check.
* @return <code>true</code> if the given position is the right/bottom
* most position of this group, <code>false</code> if not.
*/
public boolean isGroupEnd(int position) {
return (this.visibleStartPosition + this.visibleSpan - 1) == position;
}
@Override
public String toString() {
return "Group:\n\t name: " + this.name //$NON-NLS-1$
+ "\n\t startIndex: " + this.startIndex //$NON-NLS-1$
+ "\n\t visibleStartIndex: " + this.visibleStartIndex //$NON-NLS-1$
+ "\n\t visibleStartPosition: " + this.visibleStartPosition //$NON-NLS-1$
+ "\n\t originalSpan: " + this.originalSpan //$NON-NLS-1$
+ "\n\t visibleSpan: " + this.visibleSpan //$NON-NLS-1$
+ "\n\t collapseable: " + this.collapseable //$NON-NLS-1$
+ "\n\t collapsed: " + this.collapsed //$NON-NLS-1$
+ "\n\t unbreakable: " + this.unbreakable //$NON-NLS-1$
+ "\n\t staticIndexes: [ " + this.staticIndexes.makeString(", ") + " ]\n"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
}
}
/**
* Interface to support layer based position-index conversion.
*/
public interface IndexPositionConverter {
/**
* Convert the given position on the given position layer to the
* corresponding index.
*
* @param position
* The position to convert.
* @return The index for the given position.
*
* @since 2.0
*/
int convertPositionToIndex(int position);
/**
* Convert the given index to the corresponding position on the given
* position layer.
*
* @param index
* The index to convert.
* @return The position on the position layer for the given index.
*
* @since 2.0
*/
int convertIndexToPosition(int index);
}
}