| /******************************************************************************* |
| * Copyright (c) 2012, 2020 Original authors and others. |
| * All rights reserved. This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License v1.0 |
| * which accompanies this distribution, and is available at |
| * http://www.eclipse.org/legal/epl-v10.html |
| * |
| * Contributors: |
| * Original authors and others - initial API and implementation |
| ******************************************************************************/ |
| package org.eclipse.nebula.widgets.nattable.reorder; |
| |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.List; |
| import java.util.Properties; |
| import java.util.StringTokenizer; |
| import java.util.stream.Collectors; |
| |
| import org.apache.commons.logging.Log; |
| import org.apache.commons.logging.LogFactory; |
| import org.eclipse.collections.api.list.primitive.MutableIntList; |
| import org.eclipse.collections.api.map.primitive.MutableIntIntMap; |
| import org.eclipse.collections.impl.factory.primitive.IntIntMaps; |
| import org.eclipse.collections.impl.factory.primitive.IntLists; |
| import org.eclipse.nebula.widgets.nattable.coordinate.PositionUtil; |
| import org.eclipse.nebula.widgets.nattable.coordinate.Range; |
| import org.eclipse.nebula.widgets.nattable.layer.AbstractLayerTransform; |
| 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.layer.event.ColumnStructuralRefreshEvent; |
| import org.eclipse.nebula.widgets.nattable.layer.event.ILayerEvent; |
| import org.eclipse.nebula.widgets.nattable.layer.event.IStructuralChangeEvent; |
| import org.eclipse.nebula.widgets.nattable.layer.event.StructuralChangeEventHelper; |
| import org.eclipse.nebula.widgets.nattable.layer.event.StructuralDiff; |
| import org.eclipse.nebula.widgets.nattable.persistence.IPersistable; |
| import org.eclipse.nebula.widgets.nattable.reorder.action.ColumnReorderDragMode; |
| import org.eclipse.nebula.widgets.nattable.reorder.command.ColumnReorderCommandHandler; |
| import org.eclipse.nebula.widgets.nattable.reorder.command.ColumnReorderEndCommandHandler; |
| import org.eclipse.nebula.widgets.nattable.reorder.command.ColumnReorderStartCommand; |
| import org.eclipse.nebula.widgets.nattable.reorder.command.ColumnReorderStartCommandHandler; |
| import org.eclipse.nebula.widgets.nattable.reorder.command.MultiColumnReorderCommandHandler; |
| import org.eclipse.nebula.widgets.nattable.reorder.command.ResetColumnReorderCommandHandler; |
| import org.eclipse.nebula.widgets.nattable.reorder.config.DefaultColumnReorderLayerConfiguration; |
| import org.eclipse.nebula.widgets.nattable.reorder.event.ColumnReorderEvent; |
| |
| /** |
| * Layer that is used to add the functionality for column reordering. |
| * |
| * @see DefaultColumnReorderLayerConfiguration |
| */ |
| public class ColumnReorderLayer extends AbstractLayerTransform implements IUniqueIndexLayer { |
| |
| private static final Log LOG = LogFactory.getLog(ColumnReorderLayer.class); |
| |
| public static final String PERSISTENCE_KEY_COLUMN_INDEX_ORDER = ".columnIndexOrder"; //$NON-NLS-1$ |
| |
| private final IUniqueIndexLayer underlyingLayer; |
| |
| /** |
| * The internal cache of the column index order. Used to track the |
| * reordering performed by this layer. Position X in the List contains the |
| * index of column at position X. |
| */ |
| protected final MutableIntList columnIndexOrder = IntLists.mutable.empty(); |
| |
| /** |
| * The internal mapping of index to position values. Used for performance |
| * reasons in {@link #getColumnPositionByIndex(int)} because |
| * {@link List#indexOf(Object)} doesn't scale well. |
| * |
| * @since 1.5 |
| */ |
| protected final MutableIntIntMap indexPositionMapping = IntIntMaps.mutable.empty(); |
| |
| private final MutableIntIntMap startXCache = IntIntMaps.mutable.empty(); |
| |
| private int reorderFromColumnPosition; |
| |
| /** |
| * Creates a {@link ColumnReorderLayer} on top of the given |
| * {@link IUniqueIndexLayer} and adds the |
| * {@link DefaultColumnReorderLayerConfiguration}. |
| * |
| * @param underlyingLayer |
| * The underlying layer. |
| */ |
| public ColumnReorderLayer(IUniqueIndexLayer underlyingLayer) { |
| this(underlyingLayer, true); |
| } |
| |
| /** |
| * Creates a {@link ColumnReorderLayer} on top of the given |
| * {@link IUniqueIndexLayer}. |
| * |
| * @param underlyingLayer |
| * The underlying layer. |
| * @param useDefaultConfiguration |
| * <code>true</code> to add the |
| * {@link DefaultColumnReorderLayerConfiguration} |
| */ |
| public ColumnReorderLayer(IUniqueIndexLayer underlyingLayer, boolean useDefaultConfiguration) { |
| super(underlyingLayer); |
| this.underlyingLayer = underlyingLayer; |
| |
| populateIndexOrder(); |
| |
| registerCommandHandlers(); |
| |
| if (useDefaultConfiguration) { |
| addConfiguration(new DefaultColumnReorderLayerConfiguration()); |
| } |
| } |
| |
| @Override |
| public void handleLayerEvent(ILayerEvent event) { |
| if (event instanceof IStructuralChangeEvent) { |
| IStructuralChangeEvent structuralChangeEvent = (IStructuralChangeEvent) event; |
| if (structuralChangeEvent.isHorizontalStructureChanged()) { |
| Collection<StructuralDiff> structuralDiffs = structuralChangeEvent.getColumnDiffs(); |
| if (structuralDiffs == null) { |
| // Assume everything changed |
| populateIndexOrder(); |
| } else { |
| // only react on ADD or DELETE and not on CHANGE |
| StructuralChangeEventHelper.handleColumnDelete( |
| structuralDiffs, this.underlyingLayer, this.columnIndexOrder, true); |
| StructuralChangeEventHelper.handleColumnInsert( |
| structuralDiffs, this.underlyingLayer, this.columnIndexOrder, true); |
| // update index-position mapping |
| refreshIndexPositionMapping(); |
| } |
| invalidateCache(); |
| } |
| } |
| super.handleLayerEvent(event); |
| } |
| |
| // Configuration |
| |
| @Override |
| protected void registerCommandHandlers() { |
| registerCommandHandler(new ColumnReorderCommandHandler(this)); |
| registerCommandHandler(new ColumnReorderStartCommandHandler(this)); |
| registerCommandHandler(new ColumnReorderEndCommandHandler(this)); |
| registerCommandHandler(new MultiColumnReorderCommandHandler(this)); |
| registerCommandHandler(new ResetColumnReorderCommandHandler(this)); |
| } |
| |
| // Persistence |
| |
| @Override |
| public void saveState(String prefix, Properties properties) { |
| super.saveState(prefix, properties); |
| if (this.columnIndexOrder.size() > 0) { |
| properties.setProperty( |
| prefix + PERSISTENCE_KEY_COLUMN_INDEX_ORDER, |
| this.columnIndexOrder.makeString(IPersistable.VALUE_SEPARATOR)); |
| } |
| } |
| |
| @Override |
| public void loadState(String prefix, Properties properties) { |
| super.loadState(prefix, properties); |
| String property = properties.getProperty(prefix + PERSISTENCE_KEY_COLUMN_INDEX_ORDER); |
| |
| if (property != null) { |
| MutableIntList newColumnIndexOrder = IntLists.mutable.empty(); |
| StringTokenizer tok = new StringTokenizer(property, IPersistable.VALUE_SEPARATOR); |
| while (tok.hasMoreTokens()) { |
| String index = tok.nextToken(); |
| newColumnIndexOrder.add(Integer.parseInt(index)); |
| } |
| |
| if (isRestoredStateValid(newColumnIndexOrder.toArray())) { |
| this.columnIndexOrder.clear(); |
| this.columnIndexOrder.addAll(newColumnIndexOrder); |
| // refresh index-position mapping |
| refreshIndexPositionMapping(); |
| } |
| |
| } |
| invalidateCache(); |
| fireLayerEvent(new ColumnStructuralRefreshEvent(this)); |
| } |
| |
| /** |
| * Ensure that columns haven't changed in the underlying data source |
| * |
| * @param newColumnIndexOrder |
| * restored from the properties file. |
| * @since 2.0 |
| */ |
| protected boolean isRestoredStateValid(int[] newColumnIndexOrder) { |
| if (newColumnIndexOrder.length != getColumnCount()) { |
| LOG.error("Number of persisted columns (" + newColumnIndexOrder.length + ") " + //$NON-NLS-1$ //$NON-NLS-2$ |
| "is not the same as the number of columns in the data source (" //$NON-NLS-1$ |
| + getColumnCount() + ").\n" + //$NON-NLS-1$ |
| "Skipping restore of column ordering"); //$NON-NLS-1$ |
| return false; |
| } |
| |
| for (int index : newColumnIndexOrder) { |
| if (!this.indexPositionMapping.containsKey(index)) { |
| LOG.error("Column index: " + index + " being restored, is not a available in the data soure.\n" + //$NON-NLS-1$ //$NON-NLS-2$ |
| "Skipping restore of column ordering"); //$NON-NLS-1$ |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| // Columns |
| |
| /** |
| * |
| * @return the internal kept ordering of column indexes. |
| */ |
| public List<Integer> getColumnIndexOrder() { |
| return this.columnIndexOrder.primitiveStream().boxed().collect(Collectors.toList()); |
| } |
| |
| /** |
| * |
| * @return the internal kept ordering of column indexes. |
| * @since 2.0 |
| */ |
| public int[] getColumnIndexOrderArray() { |
| return this.columnIndexOrder.toArray(); |
| } |
| |
| @Override |
| public int getColumnIndexByPosition(int columnPosition) { |
| if (columnPosition >= 0 && columnPosition < this.columnIndexOrder.size()) { |
| return this.columnIndexOrder.get(columnPosition); |
| } else { |
| return -1; |
| } |
| } |
| |
| @Override |
| public int getColumnPositionByIndex(int columnIndex) { |
| return this.indexPositionMapping.getIfAbsent(columnIndex, -1); |
| } |
| |
| @Override |
| public int localToUnderlyingColumnPosition(int localColumnPosition) { |
| int columnIndex = getColumnIndexByPosition(localColumnPosition); |
| return this.underlyingLayer.getColumnPositionByIndex(columnIndex); |
| } |
| |
| @Override |
| public int underlyingToLocalColumnPosition(ILayer sourceUnderlyingLayer, int underlyingColumnPosition) { |
| int columnIndex = this.underlyingLayer.getColumnIndexByPosition(underlyingColumnPosition); |
| return getColumnPositionByIndex(columnIndex); |
| } |
| |
| @Override |
| public Collection<Range> underlyingToLocalColumnPositions(ILayer sourceUnderlyingLayer, Collection<Range> underlyingColumnPositionRanges) { |
| MutableIntList reorderedColumnPositions = IntLists.mutable.empty(); |
| for (Range underlyingColumnPositionRange : underlyingColumnPositionRanges) { |
| for (int underlyingColumnPosition = underlyingColumnPositionRange.start; underlyingColumnPosition < underlyingColumnPositionRange.end; underlyingColumnPosition++) { |
| int localColumnPosition = underlyingToLocalColumnPosition(sourceUnderlyingLayer, underlyingColumnPosition); |
| reorderedColumnPositions.add(localColumnPosition); |
| } |
| } |
| |
| return PositionUtil.getRanges(reorderedColumnPositions.toSortedArray()); |
| } |
| |
| // X |
| |
| @Override |
| public int getColumnPositionByX(int x) { |
| return LayerUtil.getColumnPositionByX(this, x); |
| } |
| |
| @Override |
| public int getStartXOfColumnPosition(int targetColumnPosition) { |
| int cachedStartX = this.startXCache.getIfAbsent(targetColumnPosition, -1); |
| if (cachedStartX != -1) { |
| return cachedStartX; |
| } |
| |
| int aggregateWidth = 0; |
| for (int columnPosition = 0; columnPosition < targetColumnPosition; columnPosition++) { |
| aggregateWidth += this.underlyingLayer.getColumnWidthByPosition(localToUnderlyingColumnPosition(columnPosition)); |
| } |
| |
| this.startXCache.put(targetColumnPosition, aggregateWidth); |
| return aggregateWidth; |
| } |
| |
| /** |
| * Initialize the internal column index ordering from a clean state, which |
| * means it reflects the ordering from the underlying layer. |
| * |
| * @since 1.6 |
| */ |
| protected void populateIndexOrder() { |
| this.columnIndexOrder.clear(); |
| ILayer underlyingLayer = getUnderlyingLayer(); |
| for (int columnPosition = 0; columnPosition < underlyingLayer.getColumnCount(); columnPosition++) { |
| int index = underlyingLayer.getColumnIndexByPosition(columnPosition); |
| this.columnIndexOrder.add(index); |
| this.indexPositionMapping.put(index, columnPosition); |
| } |
| } |
| |
| /** |
| * Initializes the internal index-position-mapping to reflect the internal |
| * column-index-order. |
| * |
| * @since 1.6 |
| */ |
| protected void refreshIndexPositionMapping() { |
| this.indexPositionMapping.clear(); |
| for (int position = 0; position < this.columnIndexOrder.size(); position++) { |
| int index = this.columnIndexOrder.get(position); |
| this.indexPositionMapping.put(index, position); |
| } |
| } |
| |
| // Vertical features |
| |
| // Rows |
| |
| @Override |
| public int getRowPositionByIndex(int rowIndex) { |
| return this.underlyingLayer.getRowPositionByIndex(rowIndex); |
| } |
| |
| /** |
| * Moves the given from-column to the specified edge of the column to move |
| * to. |
| * |
| * @param fromColumnPosition |
| * column position to move |
| * @param toColumnPosition |
| * position to move the column to |
| * @param reorderToLeftEdge |
| * <code>true</code> if the column should be moved to the left of |
| * the given column to move to, <code>false</code> if it should |
| * be positioned to the right |
| */ |
| private void moveColumn(int fromColumnPosition, int toColumnPosition, boolean reorderToLeftEdge) { |
| if (!reorderToLeftEdge) { |
| toColumnPosition++; |
| } |
| |
| int fromColumnIndex = this.columnIndexOrder.get(fromColumnPosition); |
| this.columnIndexOrder.addAtIndex(toColumnPosition, fromColumnIndex); |
| this.columnIndexOrder.removeAtIndex(fromColumnPosition + (fromColumnPosition > toColumnPosition ? 1 : 0)); |
| |
| // update index-position mapping |
| refreshIndexPositionMapping(); |
| |
| invalidateCache(); |
| } |
| |
| /** |
| * Moves the given from-column to the <b>left</b> edge of the column to move |
| * to. |
| * |
| * @param fromColumnPosition |
| * column position to move |
| * @param toColumnPosition |
| * position to move the column to |
| */ |
| public void reorderColumnPosition(int fromColumnPosition, int toColumnPosition) { |
| boolean reorderToLeftEdge; |
| if (toColumnPosition < getColumnCount()) { |
| reorderToLeftEdge = true; |
| } else { |
| reorderToLeftEdge = false; |
| toColumnPosition--; |
| } |
| reorderColumnPosition(fromColumnPosition, toColumnPosition, reorderToLeftEdge); |
| } |
| |
| /** |
| * Reorders the given from-column to the specified edge of the column to |
| * move to and fires a {@link ColumnReorderEvent}. |
| * |
| * @param fromColumnPosition |
| * column position to move |
| * @param toColumnPosition |
| * position to move the column to |
| * @param reorderToLeftEdge |
| * <code>true</code> if the column should be moved to the left of |
| * the given column to move to, <code>false</code> if it should |
| * be positioned to the right |
| */ |
| public void reorderColumnPosition(int fromColumnPosition, int toColumnPosition, boolean reorderToLeftEdge) { |
| // get the indexes before the move operation |
| int fromColumnIndex = getColumnIndexByPosition(fromColumnPosition); |
| int toColumnIndex = getColumnIndexByPosition(toColumnPosition); |
| moveColumn(fromColumnPosition, toColumnPosition, reorderToLeftEdge); |
| fireLayerEvent(new ColumnReorderEvent(this, fromColumnPosition, fromColumnIndex, toColumnPosition, toColumnIndex, reorderToLeftEdge)); |
| } |
| |
| /** |
| * Reorders the given from-columns to the <b>left</b> edge of the column to |
| * move to. |
| * |
| * @param fromColumnPositions |
| * column positions to move |
| * @param toColumnPosition |
| * position to move the columns to |
| */ |
| public void reorderMultipleColumnPositions(List<Integer> fromColumnPositions, int toColumnPosition) { |
| reorderMultipleColumnPositions( |
| fromColumnPositions.stream().mapToInt(Integer::intValue).toArray(), |
| toColumnPosition); |
| } |
| |
| /** |
| * Reorders the given from-columns to the <b>left</b> edge of the column to |
| * move to. |
| * |
| * @param fromColumnPositions |
| * column positions to move |
| * @param toColumnPosition |
| * position to move the columns to |
| * @since 2.0 |
| */ |
| public void reorderMultipleColumnPositions(int[] fromColumnPositions, int toColumnPosition) { |
| boolean reorderToLeftEdge; |
| if (toColumnPosition < getColumnCount()) { |
| reorderToLeftEdge = true; |
| } else { |
| reorderToLeftEdge = false; |
| toColumnPosition--; |
| } |
| reorderMultipleColumnPositions(fromColumnPositions, toColumnPosition, reorderToLeftEdge); |
| } |
| |
| /** |
| * Reorders the given from-columns to the specified edge of the column to |
| * move to and fires a {@link ColumnReorderEvent}. |
| * |
| * @param fromColumnPositions |
| * column positions to move |
| * @param toColumnPosition |
| * position to move the columns to |
| * @param reorderToLeftEdge |
| * <code>true</code> if the columns should be moved to the left |
| * of the given column to move to, <code>false</code> if they |
| * should be positioned to the right |
| */ |
| public void reorderMultipleColumnPositions(List<Integer> fromColumnPositions, int toColumnPosition, boolean reorderToLeftEdge) { |
| reorderMultipleColumnPositions( |
| fromColumnPositions.stream().mapToInt(Integer::intValue).toArray(), |
| toColumnPosition, |
| reorderToLeftEdge); |
| } |
| |
| /** |
| * Reorders the given from-columns to the specified edge of the column to |
| * move to and fires a {@link ColumnReorderEvent}. |
| * |
| * @param fromColumnPositions |
| * column positions to move |
| * @param toColumnPosition |
| * position to move the columns to |
| * @param reorderToLeftEdge |
| * <code>true</code> if the columns should be moved to the left |
| * of the given column to move to, <code>false</code> if they |
| * should be positioned to the right |
| * @since 2.0 |
| */ |
| public void reorderMultipleColumnPositions(int[] fromColumnPositions, int toColumnPosition, boolean reorderToLeftEdge) { |
| // the position collection needs to be sorted so the move works |
| // correctly |
| Arrays.sort(fromColumnPositions); |
| |
| // get the indexes before the move operation |
| int[] fromColumnIndexes = Arrays.stream(fromColumnPositions).map(this::getColumnIndexByPosition).toArray(); |
| int toColumnIndex = getColumnIndexByPosition(toColumnPosition); |
| |
| // Moving from left to right |
| final int fromColumnPositionsCount = fromColumnPositions.length; |
| |
| if (toColumnPosition > fromColumnPositions[fromColumnPositionsCount - 1]) { |
| int firstColumnPosition = fromColumnPositions[0]; |
| |
| int moved = 0; |
| for (int columnCount = 0; columnCount < fromColumnPositionsCount; columnCount++) { |
| final int fromColumnPosition = fromColumnPositions[columnCount] - moved; |
| moveColumn(fromColumnPosition, toColumnPosition, reorderToLeftEdge); |
| moved++; |
| if (fromColumnPosition < firstColumnPosition) { |
| firstColumnPosition = fromColumnPosition; |
| } |
| } |
| } else if (toColumnPosition < fromColumnPositions[fromColumnPositionsCount - 1]) { |
| // Moving from right to left |
| int targetColumnPosition = toColumnPosition; |
| for (int fromColumnPosition : fromColumnPositions) { |
| final int fromColumnPositionInt = fromColumnPosition; |
| moveColumn(fromColumnPositionInt, targetColumnPosition++, reorderToLeftEdge); |
| } |
| } |
| |
| fireLayerEvent(new ColumnReorderEvent(this, fromColumnPositions, fromColumnIndexes, toColumnPosition, toColumnIndex, reorderToLeftEdge)); |
| } |
| |
| /** |
| * Reorders the given from-columns identified by index to the specified edge |
| * of the column to move to and fires a {@link ColumnReorderEvent}. This |
| * method can be used to reorder columns that are hidden in a higher level, |
| * e.g. to reorder a column group that has hidden columns. |
| * |
| * @param fromColumnIndexes |
| * column indexes to move |
| * @param toColumnPosition |
| * position to move the columns to |
| * @param reorderToLeftEdge |
| * <code>true</code> if the columns should be moved to the left |
| * of the given column to move to, <code>false</code> if they |
| * should be positioned to the right |
| * |
| * @since 1.6 |
| */ |
| public void reorderMultipleColumnIndexes(List<Integer> fromColumnIndexes, int toColumnPosition, boolean reorderToLeftEdge) { |
| reorderMultipleColumnIndexes( |
| fromColumnIndexes.stream().mapToInt(Integer::intValue).toArray(), |
| toColumnPosition, |
| reorderToLeftEdge); |
| } |
| |
| /** |
| * Reorders the given from-columns identified by index to the specified edge |
| * of the column to move to and fires a {@link ColumnReorderEvent}. This |
| * method can be used to reorder columns that are hidden in a higher level, |
| * e.g. to reorder a column group that has hidden columns. |
| * |
| * @param fromColumnIndexes |
| * column indexes to move |
| * @param toColumnPosition |
| * position to move the columns to |
| * @param reorderToLeftEdge |
| * <code>true</code> if the columns should be moved to the left |
| * of the given column to move to, <code>false</code> if they |
| * should be positioned to the right |
| * |
| * @since 2.0 |
| */ |
| public void reorderMultipleColumnIndexes(int[] fromColumnIndexes, int toColumnPosition, boolean reorderToLeftEdge) { |
| // calculate positions from indexes |
| int[] fromColumnPositions = Arrays.stream(fromColumnIndexes).map(this::getColumnPositionByIndex).toArray(); |
| reorderMultipleColumnPositions(fromColumnPositions, toColumnPosition, reorderToLeftEdge); |
| } |
| |
| /** |
| * Clear the internal cache. |
| * |
| * @since 1.6 |
| */ |
| protected void invalidateCache() { |
| this.startXCache.clear(); |
| } |
| |
| /** |
| * Returns the column position from where the reorder process started. Used |
| * by the {@link ColumnReorderEndCommandHandler} which is triggered by the |
| * {@link ColumnReorderDragMode} when dragging a column is finished. |
| * |
| * @return The column position where the reorder started. |
| */ |
| public int getReorderFromColumnPosition() { |
| return this.reorderFromColumnPosition; |
| } |
| |
| /** |
| * Sets the column position where a reorder process started. Typically done |
| * by calling the {@link ColumnReorderStartCommand} which is triggered by |
| * the {@link ColumnReorderDragMode}. |
| * |
| * @param fromColumnPosition |
| * The column position where the reorder started. |
| */ |
| public void setReorderFromColumnPosition(int fromColumnPosition) { |
| this.reorderFromColumnPosition = fromColumnPosition; |
| } |
| |
| /** |
| * Resets the reordering tracked by this layer. |
| * |
| * @since 1.6 |
| */ |
| public void resetReorder() { |
| populateIndexOrder(); |
| invalidateCache(); |
| fireLayerEvent(new ColumnStructuralRefreshEvent(this)); |
| } |
| |
| } |