/*******************************************************************************
* Copyright (c) 2012, 2016 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.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.StringTokenizer;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
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.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.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 List<Integer> columnIndexOrder = new ArrayList<Integer>();
/**
* 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 Map<Integer, Integer> indexPositionMapping = new HashMap<Integer, Integer>();
private final Map<Integer, Integer> startXCache = new HashMap<Integer, Integer>();
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));
}
// Persistence
@Override
public void saveState(String prefix, Properties properties) {
super.saveState(prefix, properties);
if (this.columnIndexOrder.size() > 0) {
StringBuilder strBuilder = new StringBuilder();
for (Integer index : this.columnIndexOrder) {
strBuilder.append(index);
strBuilder.append(',');
}
properties.setProperty(prefix + PERSISTENCE_KEY_COLUMN_INDEX_ORDER, strBuilder.toString());
}
}
@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) {
List<Integer> newColumnIndexOrder = new ArrayList<Integer>();
StringTokenizer tok = new StringTokenizer(property, ","); //$NON-NLS-1$
while (tok.hasMoreTokens()) {
String index = tok.nextToken();
newColumnIndexOrder.add(Integer.valueOf(index));
}
if (isRestoredStateValid(newColumnIndexOrder)) {
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.
*/
protected boolean isRestoredStateValid(List<Integer> newColumnIndexOrder) {
if (newColumnIndexOrder.size() != getColumnCount()) {
LOG.error("Number of persisted columns (" + newColumnIndexOrder.size() + ") " + //$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 (Integer 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;
}
@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) {
Integer result = this.indexPositionMapping.get(columnIndex);
return (result != null) ? result : -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) {
List<Integer> reorderedColumnPositions = new ArrayList<Integer>();
for (Range underlyingColumnPositionRange : underlyingColumnPositionRanges) {
for (int underlyingColumnPosition = underlyingColumnPositionRange.start; underlyingColumnPosition < underlyingColumnPositionRange.end; underlyingColumnPosition++) {
int localColumnPosition = underlyingToLocalColumnPosition(sourceUnderlyingLayer, underlyingColumnPositionRange.start);
reorderedColumnPositions.add(Integer.valueOf(localColumnPosition));
}
}
Collections.sort(reorderedColumnPositions);
return PositionUtil.getRanges(reorderedColumnPositions);
}
// X
@Override
public int getColumnPositionByX(int x) {
return LayerUtil.getColumnPositionByX(this, x);
}
@Override
public int getStartXOfColumnPosition(int targetColumnPosition) {
Integer cachedStartX = this.startXCache.get(Integer.valueOf(targetColumnPosition));
if (cachedStartX != null) {
return cachedStartX.intValue();
}
int aggregateWidth = 0;
for (int columnPosition = 0; columnPosition < targetColumnPosition; columnPosition++) {
aggregateWidth += this.underlyingLayer.getColumnWidthByPosition(localToUnderlyingColumnPosition(columnPosition));
}
this.startXCache.put(Integer.valueOf(targetColumnPosition), Integer.valueOf(aggregateWidth));
return aggregateWidth;
}
/**
* Initialize the internal column index ordering from a clean state, which
* means it reflects the ordering from the underlying layer.
*/
private 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.
*/
private 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++;
}
Integer fromColumnIndex = this.columnIndexOrder.get(fromColumnPosition);
this.columnIndexOrder.add(toColumnPosition, fromColumnIndex);
this.columnIndexOrder.remove(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) {
moveColumn(fromColumnPosition, toColumnPosition, reorderToLeftEdge);
fireLayerEvent(new ColumnReorderEvent(this, fromColumnPosition, toColumnPosition, 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) {
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) {
// Moving from left to right
final int fromColumnPositionsCount = fromColumnPositions.size();
if (toColumnPosition > fromColumnPositions.get(fromColumnPositionsCount - 1)) {
int firstColumnPosition = fromColumnPositions.get(0).intValue();
for (int columnCount = 0; columnCount < fromColumnPositionsCount; columnCount++) {
final int fromColumnPosition = fromColumnPositions.get(0);
moveColumn(fromColumnPosition, toColumnPosition, reorderToLeftEdge);
if (fromColumnPosition < firstColumnPosition) {
firstColumnPosition = fromColumnPosition;
}
}
} else if (toColumnPosition < fromColumnPositions.get(fromColumnPositionsCount - 1).intValue()) {
// Moving from right to left
int targetColumnPosition = toColumnPosition;
for (Integer fromColumnPosition : fromColumnPositions) {
final int fromColumnPositionInt = fromColumnPosition.intValue();
moveColumn(fromColumnPositionInt, targetColumnPosition++, reorderToLeftEdge);
}
}
fireLayerEvent(new ColumnReorderEvent(this, fromColumnPositions, toColumnPosition, reorderToLeftEdge));
}
/**
* Clear the internal cache.
*/
private 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;
}
}