blob: a0c49f3b63617bf2cf01ab3549ac14fe659c3843 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2012, 2020 Original authors and others.
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Original authors and others - initial API and implementation
******************************************************************************/
package org.eclipse.nebula.widgets.nattable.hideshow;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import org.eclipse.collections.api.map.primitive.IntIntMap;
import org.eclipse.collections.api.map.primitive.MutableIntIntMap;
import org.eclipse.collections.impl.factory.primitive.IntIntMaps;
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.cell.ILayerCell;
import org.eclipse.nebula.widgets.nattable.layer.cell.SpanningLayerCell;
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.VisualRefreshEvent;
import org.eclipse.nebula.widgets.nattable.reorder.event.ColumnReorderEvent;
/**
* Abstract implementation for column hide/show operations.
*/
public abstract class AbstractColumnHideShowLayer extends AbstractLayerTransform implements IUniqueIndexLayer {
private MutableIntIntMap cachedVisibleColumnIndexPositionMapping;
private MutableIntIntMap cachedVisibleColumnPositionIndexMapping;
private MutableIntIntMap cachedHiddenColumnIndexPositionMapping;
private MutableIntIntMap startXCache = IntIntMaps.mutable.empty();
/**
* Constructor.
*
* @param underlyingLayer
* The underlying layer.
*/
public AbstractColumnHideShowLayer(IUniqueIndexLayer underlyingLayer) {
super(underlyingLayer);
}
/**
* @return The underlying layer.
* @since 2.0
*/
@Override
protected IUniqueIndexLayer getUnderlyingLayer() {
return (IUniqueIndexLayer) super.getUnderlyingLayer();
}
@Override
public void handleLayerEvent(ILayerEvent event) {
if (hasHiddenColumns() && event instanceof ColumnReorderEvent) {
// we need to convert the before positions in the event BEFORE the
// local states are changed, otherwise we are not able to convert
// the before positions as the changed layer states would return
// incorrect values
ColumnReorderEvent reorderEvent = (ColumnReorderEvent) event;
int[] fromPositions = reorderEvent.getBeforeFromColumnIndexes().stream()
.mapToInt(Integer::intValue)
.map(this::getColumnPositionByIndex)
.toArray();
Collection<Range> fromRanges = PositionUtil.getRanges(fromPositions);
int pos = -1;
if (!isColumnIndexHidden(reorderEvent.getBeforeToColumnIndex())) {
pos = getColumnPositionByIndex(reorderEvent.getBeforeToColumnIndex());
} else {
int i = 1;
while (pos < 0) {
int next = reorderEvent.getBeforeToColumnPosition() + i;
if (next >= this.underlyingLayer.getColumnCount()) {
break;
}
pos = underlyingToLocalColumnPosition(this.underlyingLayer, next);
i++;
}
if (pos >= 0) {
reorderEvent.setBeforeToColumnIndex(getColumnIndexByPosition(pos));
}
}
if (pos >= 0) {
reorderEvent.setConvertedBeforePositions(this, fromRanges, pos);
}
}
if (event instanceof IStructuralChangeEvent) {
IStructuralChangeEvent structuralChangeEvent = (IStructuralChangeEvent) event;
if (structuralChangeEvent.isHorizontalStructureChanged()) {
invalidateCache();
}
} else if (event instanceof VisualRefreshEvent) {
// visual change, e.g. font change, the startXCache needs to be
// cleared in order to re-render correctly
this.startXCache = IntIntMaps.mutable.empty();
}
super.handleLayerEvent(event);
}
// Horizontal features
// Columns
@Override
public int getColumnCount() {
if (!hasHiddenColumns()) {
return super.getColumnCount();
}
return getCachedVisibleColumnIndexPositionMapping().size();
}
@Override
public int getColumnIndexByPosition(int columnPosition) {
if (columnPosition < 0 || columnPosition >= getColumnCount()) {
return -1;
}
if (!hasHiddenColumns()) {
return super.getColumnIndexByPosition(columnPosition);
}
return getCachedVisibleColumnPositionIndexMapping().getIfAbsent(columnPosition, -1);
}
@Override
public int getColumnPositionByIndex(int columnIndex) {
if (!hasHiddenColumns()) {
return getUnderlyingLayer().getColumnPositionByIndex(columnIndex);
}
return getCachedVisibleColumnIndexPositionMapping().getIfAbsent(columnIndex, -1);
}
/**
* Get the local column positions for the given column indexes.
*
* @param columnIndexes
* The column indexes for which the local column positions are
* requested.
* @return The local column positions for the given column indexes.
*/
public Collection<Integer> getColumnPositionsByIndexes(Collection<Integer> columnIndexes) {
Collection<Integer> columnPositions = new HashSet<>();
for (int columnIndex : columnIndexes) {
columnPositions.add(getColumnPositionByIndex(columnIndex));
}
return columnPositions;
}
/**
* Get the local column positions for the given column indexes.
*
* @param columnIndexes
* The column indexes for which the local column positions are
* requested.
* @return The local column positions for the given column indexes.
*
* @since 2.0
*/
public int[] getColumnPositionsByIndexes(int... columnIndexes) {
return (columnIndexes != null && columnIndexes.length > 0)
? Arrays.stream(columnIndexes).map(this::getColumnPositionByIndex).toArray()
: new int[0];
}
@Override
public int localToUnderlyingColumnPosition(int localColumnPosition) {
if (localColumnPosition < 0 || localColumnPosition >= getColumnCount()) {
return -1;
}
if (!hasHiddenColumns()) {
return localColumnPosition;
}
int columnIndex = getColumnIndexByPosition(localColumnPosition);
return getUnderlyingLayer().getColumnPositionByIndex(columnIndex);
}
@Override
public int underlyingToLocalColumnPosition(ILayer sourceUnderlyingLayer, int underlyingColumnPosition) {
if (!hasHiddenColumns()) {
return underlyingColumnPosition;
}
int columnIndex = getUnderlyingLayer().getColumnIndexByPosition(underlyingColumnPosition);
int columnPosition = getColumnPositionByIndex(columnIndex);
if (columnPosition >= 0) {
return columnPosition;
} else {
return getCachedHiddenColumnIndexPositionMapping().getIfAbsent(columnIndex, -1);
}
}
@Override
public Collection<Range> underlyingToLocalColumnPositions(
ILayer sourceUnderlyingLayer, Collection<Range> underlyingColumnPositionRanges) {
Collection<Range> localColumnPositionRanges = new ArrayList<>(underlyingColumnPositionRanges.size());
for (Range underlyingColumnPositionRange : underlyingColumnPositionRanges) {
int startColumnPosition = getAdjustedUnderlyingToLocalStartPosition(
sourceUnderlyingLayer,
underlyingColumnPositionRange.start,
underlyingColumnPositionRange.end);
int endColumnPosition = getAdjustedUnderlyingToLocalEndPosition(
sourceUnderlyingLayer,
underlyingColumnPositionRange.end,
underlyingColumnPositionRange.start);
// teichstaedt: fixes the problem that ranges where added even if
// the corresponding startPosition weren't found in the underlying
// layer. Without that fix a bunch of ranges of kind Range [-1, 180]
// which causes strange behaviour in Freeze- and other Layers were
// returned.
if (startColumnPosition > -1) {
localColumnPositionRanges.add(new Range(startColumnPosition, endColumnPosition));
}
}
return localColumnPositionRanges;
}
private int getAdjustedUnderlyingToLocalStartPosition(
ILayer sourceUnderlyingLayer,
int startUnderlyingPosition,
int endUnderlyingPosition) {
int localStartColumnPosition =
underlyingToLocalColumnPosition(
sourceUnderlyingLayer,
startUnderlyingPosition);
int offset = 0;
while (localStartColumnPosition < 0
&& (startUnderlyingPosition + offset < endUnderlyingPosition)) {
localStartColumnPosition =
underlyingToLocalColumnPosition(
sourceUnderlyingLayer,
startUnderlyingPosition + offset++);
}
return localStartColumnPosition;
}
private int getAdjustedUnderlyingToLocalEndPosition(
ILayer sourceUnderlyingLayer,
int endUnderlyingPosition,
int startUnderlyingPosition) {
int localEndColumnPosition =
underlyingToLocalColumnPosition(
sourceUnderlyingLayer,
endUnderlyingPosition - 1);
int offset = 0;
while (localEndColumnPosition < 0
&& (endUnderlyingPosition - offset > startUnderlyingPosition)) {
localEndColumnPosition =
underlyingToLocalColumnPosition(
sourceUnderlyingLayer,
endUnderlyingPosition - offset++);
}
return localEndColumnPosition + 1;
}
// Width
@Override
public int getWidth() {
if (getColumnCount() == 0) {
return 0;
}
int lastColumnPosition = getColumnCount() - 1;
return getStartXOfColumnPosition(lastColumnPosition) + getColumnWidthByPosition(lastColumnPosition);
}
// X
@Override
public int getColumnPositionByX(int x) {
return LayerUtil.getColumnPositionByX(this, x);
}
@Override
public int getStartXOfColumnPosition(int localColumnPosition) {
int cachedStartX = this.startXCache.getIfAbsent(localColumnPosition, -1);
if (cachedStartX != -1) {
return cachedStartX;
}
IUniqueIndexLayer underlyingLayer = getUnderlyingLayer();
int underlyingPosition = localToUnderlyingColumnPosition(localColumnPosition);
if (underlyingPosition < 0) {
return -1;
}
int underlyingStartX = underlyingLayer.getStartXOfColumnPosition(underlyingPosition);
if (underlyingStartX < 0) {
return -1;
}
for (int hiddenIndex : getHiddenColumnIndexesArray()) {
int hiddenPosition = underlyingLayer.getColumnPositionByIndex(hiddenIndex);
// if the hidden position is -1, it is hidden in the underlying
// layer therefore the underlying layer should handle the
// positioning
if (hiddenPosition >= 0 && hiddenPosition <= underlyingPosition) {
underlyingStartX -= underlyingLayer.getColumnWidthByPosition(hiddenPosition);
}
}
this.startXCache.put(localColumnPosition, underlyingStartX);
return underlyingStartX;
}
// Vertical features
// Rows
@Override
public int getRowPositionByIndex(int rowIndex) {
return getUnderlyingLayer().getRowPositionByIndex(rowIndex);
}
// Hide/show
/**
* Will check if the column at the specified index is hidden or not.
*
* @param columnIndex
* The column index of the column whose visibility state should
* be checked.
* @return <code>true</code> if the column at the specified index is hidden,
* <code>false</code> if it is visible.
*/
public abstract boolean isColumnIndexHidden(int columnIndex);
/**
* Will collect and return all indexes of the columns that are hidden in
* this layer.
* <p>
* <b>Note:</b> It is not intended that it also collects the column indexes
* of underlying layers. This would cause issues on calculating positions,
* as every layer is responsible for those calculations itself.
* </p>
* <p>
* Since 2.0 it is recommended to use {@link #getHiddenColumnIndexesArray()}
* to avoid unnecessary autoboxing operations.
* </p>
*
* @return Collection of all column indexes that are hidden in this layer.
*/
public abstract Collection<Integer> getHiddenColumnIndexes();
/**
* Will collect and return all indexes of the columns that are hidden in
* this layer.
* <p>
* <b>Note:</b> It is not intended that it also collects the column indexes
* of underlying layers. This would cause issues on calculating positions,
* as every layer is responsible for those calculations itself.
* </p>
*
* @return All column indexes that are hidden in this layer.
*
* @since 2.0
*/
public abstract int[] getHiddenColumnIndexesArray();
/**
* Check if this layer actively hides columns.
*
* @return <code>true</code> if columns are hidden by this layer,
* <code>false</code> if not.
*
* @since 2.0
*/
public abstract boolean hasHiddenColumns();
@Override
public ILayerCell getCellByPosition(int columnPosition, int rowPosition) {
ILayerCell cell = super.getCellByPosition(columnPosition, rowPosition);
if (cell != null && cell.isSpannedCell()) {
// the spanning needs to be updated to reflect the
// hiding accordingly
int underlyingColumnPosition = localToUnderlyingColumnPosition(columnPosition);
int underlyingRowPosition = localToUnderlyingRowPosition(rowPosition);
ILayerCell underlyingCell = this.underlyingLayer.getCellByPosition(underlyingColumnPosition, underlyingRowPosition);
boolean columnSpanUpdated = false;
int columnSpan = underlyingCell.getColumnSpan();
for (int column = 0; column < underlyingCell.getColumnSpan(); column++) {
int columnIndex = this.underlyingLayer.getColumnIndexByPosition(underlyingCell.getOriginColumnPosition() + column);
if (isColumnIndexHidden(columnIndex)) {
columnSpan--;
columnSpanUpdated = true;
}
}
if (columnSpanUpdated) {
cell = new SpanningLayerCell(cell, columnSpan, cell.getRowSpan());
}
}
return cell;
}
// Cache
/**
* Invalidate the cache to ensure that information is rebuild.
*/
protected synchronized void invalidateCache() {
this.cachedVisibleColumnIndexPositionMapping = null;
this.cachedVisibleColumnPositionIndexMapping = null;
this.cachedHiddenColumnIndexPositionMapping = null;
this.startXCache = IntIntMaps.mutable.empty();
}
private synchronized IntIntMap getCachedVisibleColumnIndexPositionMapping() {
if (this.cachedVisibleColumnIndexPositionMapping == null) {
cacheVisibleColumnIndexes();
}
return this.cachedVisibleColumnIndexPositionMapping;
}
private synchronized IntIntMap getCachedVisibleColumnPositionIndexMapping() {
if (this.cachedVisibleColumnPositionIndexMapping == null) {
cacheVisibleColumnIndexes();
}
return this.cachedVisibleColumnPositionIndexMapping;
}
private synchronized IntIntMap getCachedHiddenColumnIndexPositionMapping() {
if (this.cachedHiddenColumnIndexPositionMapping == null) {
cacheVisibleColumnIndexes();
}
return this.cachedHiddenColumnIndexPositionMapping;
}
/**
* Build up the column caches.
*
* @since 2.0
*/
protected synchronized void cacheVisibleColumnIndexes() {
this.cachedVisibleColumnIndexPositionMapping = IntIntMaps.mutable.empty();
this.cachedVisibleColumnPositionIndexMapping = IntIntMaps.mutable.empty();
this.cachedHiddenColumnIndexPositionMapping = IntIntMaps.mutable.empty();
this.startXCache = IntIntMaps.mutable.empty();
// only build up a cache if it is necessary
if (hasHiddenColumns()) {
ILayer underlyingLayer = getUnderlyingLayer();
int columnPosition = 0;
for (int parentColumnPosition = 0; parentColumnPosition < underlyingLayer.getColumnCount(); parentColumnPosition++) {
int columnIndex = underlyingLayer.getColumnIndexByPosition(parentColumnPosition);
if (!isColumnIndexHidden(columnIndex)) {
this.cachedVisibleColumnIndexPositionMapping.put(columnIndex, columnPosition);
this.cachedVisibleColumnPositionIndexMapping.put(columnPosition, columnIndex);
columnPosition++;
} else {
this.cachedHiddenColumnIndexPositionMapping.put(columnIndex, columnPosition);
}
}
}
}
}