| /******************************************************************************* |
| * 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); |
| |
| } |
| } |