blob: 708ee96ad8309bedd865db2153c04cc462a47a9f [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2014, 2020 Jonas Hugo, Markus Wahl, 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:
* Jonas Hugo <Jonas.Hugo@jeppesen.com>,
* Markus Wahl <Markus.Wahl@jeppesen.com> - initial API and implementation
* Dirk Fauth <dirk.fauth@googlemail.com> - Bug 453851, 446275, 447396
******************************************************************************/
package org.eclipse.nebula.widgets.nattable.selection.preserve;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.eclipse.nebula.widgets.nattable.coordinate.Range;
import org.eclipse.nebula.widgets.nattable.data.IRowDataProvider;
import org.eclipse.nebula.widgets.nattable.data.IRowIdAccessor;
import org.eclipse.nebula.widgets.nattable.layer.IUniqueIndexLayer;
import org.eclipse.nebula.widgets.nattable.layer.cell.ILayerCell;
import org.eclipse.nebula.widgets.nattable.layer.event.IStructuralChangeEvent;
import org.eclipse.nebula.widgets.nattable.layer.event.ResizeStructuralRefreshEvent;
import org.eclipse.nebula.widgets.nattable.layer.event.StructuralDiff;
import org.eclipse.nebula.widgets.nattable.layer.event.StructuralDiff.DiffTypeEnum;
import org.eclipse.nebula.widgets.nattable.reorder.event.ColumnReorderEvent;
import org.eclipse.nebula.widgets.nattable.resize.event.ColumnResizeEvent;
import org.eclipse.nebula.widgets.nattable.resize.event.RowResizeEvent;
import org.eclipse.nebula.widgets.nattable.selection.IMarkerSelectionModel;
import org.eclipse.nebula.widgets.nattable.selection.SelectionLayer;
import org.eclipse.nebula.widgets.nattable.selection.SelectionUtils;
import org.eclipse.nebula.widgets.nattable.selection.preserve.Selections.CellPosition;
import org.eclipse.nebula.widgets.nattable.selection.preserve.Selections.Row;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
/**
* Individual cell selection model that copes with the reordering of rows.
*
* @param <T>
* the type of object underlying each row
*/
public class PreserveSelectionModel<T> implements IMarkerSelectionModel {
/**
* Provider of cell information.
*
* @since 1.6
*/
protected final IUniqueIndexLayer selectionLayer;
/**
* Provider of underlying row objects
*/
private final IRowDataProvider<T> rowDataProvider;
/**
* Provider of unique IDs for the rows
*/
private final IRowIdAccessor<T> rowIdAccessor;
/**
* Whether to allow multiple selections
*/
private boolean allowMultiSelection;
/**
* The selected cells
*/
private Selections<T> selections = new Selections<>();
/**
* Lock for ensuring thread safety
*/
private final ReadWriteLock selectionsLock;
/**
* Position of the selection anchor marker, expressed in row object and
* column position
*/
CellPosition<T> selectionAnchor;
/**
* The selection anchor point calculated out of the selection anchor marker.
* Tracked here to reduce the number of calculations on rendering.
*/
Point selectionAnchorPoint;
/**
* Position of the last selected cell marker, expressed in row object and
* column position
*/
CellPosition<T> lastSelectedCell;
/**
* Area of the last selected region marker, expressed in row position,
* column position, number of column width and number of rows height
*/
Rectangle lastSelectedRegion;
/**
* The row object of the origin of the last selected region
*/
T lastSelectedRegionOriginRowObject;
/**
* Creates a row sortable selection model
*
* @param selectionLayer
* provider of cell information
* @param rowDataProvider
* provider of underlying row objects
* @param rowIdAccessor
* provider of unique IDs for the rows
*/
public PreserveSelectionModel(
IUniqueIndexLayer selectionLayer,
IRowDataProvider<T> rowDataProvider,
IRowIdAccessor<T> rowIdAccessor) {
this.selectionLayer = selectionLayer;
this.rowDataProvider = rowDataProvider;
this.rowIdAccessor = rowIdAccessor;
this.allowMultiSelection = true;
this.selectionsLock = new ReentrantReadWriteLock();
}
@Override
public boolean isMultipleSelectionAllowed() {
return this.allowMultiSelection;
}
@Override
public void setMultipleSelectionAllowed(boolean multipleSelectionAllowed) {
this.allowMultiSelection = multipleSelectionAllowed;
}
@Override
public void addSelection(int columnPosition, int rowPosition) {
this.selectionsLock.writeLock().lock();
try {
if (!this.allowMultiSelection) {
clearSelection();
}
Serializable rowId = getRowIdByPosition(rowPosition);
if (rowId != null) {
T rowObject = getRowObjectByPosition(rowPosition);
this.selections.select(rowId, rowObject, columnPosition);
}
} finally {
this.selectionsLock.writeLock().unlock();
}
}
@Override
public void addSelection(Rectangle range) {
this.selectionsLock.writeLock().lock();
try {
if (!this.allowMultiSelection) {
clearSelection();
}
int startColumnPosition = range.x;
int startRowPosition = range.y;
if (startColumnPosition < this.selectionLayer.getColumnCount() && startRowPosition < this.selectionLayer.getRowCount()) {
int numberOfVisibleColumnsToBeSelected = getNumberOfColumnsToBeSelected(range);
int numberOfVisibleRowsToBeSelected = getNumberOfRowsToBeSelected(range);
for (int rowPosition = startRowPosition; rowPosition < startRowPosition + numberOfVisibleRowsToBeSelected; rowPosition++) {
Serializable rowId = getRowIdByPosition(rowPosition);
if (rowId != null) {
T rowObject = getRowObjectByPosition(rowPosition);
for (int columnPosition = startColumnPosition; columnPosition < (startColumnPosition + numberOfVisibleColumnsToBeSelected); columnPosition++) {
this.selections.select(rowId, rowObject, columnPosition);
}
}
}
}
} finally {
this.selectionsLock.writeLock().unlock();
}
}
@Override
public void clearSelection() {
this.selectionsLock.writeLock().lock();
try {
this.selections.clear();
} finally {
this.selectionsLock.writeLock().unlock();
}
}
@Override
public void clearSelection(int columnPosition, int rowPosition) {
this.selectionsLock.writeLock().lock();
try {
Serializable rowId = getRowIdByPosition(rowPosition);
if (rowId != null) {
this.selections.deselect(rowId, columnPosition);
}
} finally {
this.selectionsLock.writeLock().unlock();
}
}
@Override
public void clearSelection(Rectangle removedSelection) {
this.selectionsLock.writeLock().lock();
try {
int startColumnPosition = removedSelection.x;
int startRowPosition = removedSelection.y;
if (startColumnPosition < this.selectionLayer.getColumnCount() && startRowPosition < this.selectionLayer.getRowCount()) {
int numberOfVisibleColumnsToBeSelected = getNumberOfColumnsToBeSelected(removedSelection);
int numberOfVisibleRowsToBeSelected = getNumberOfRowsToBeSelected(removedSelection);
for (int rowPosition = startRowPosition; rowPosition < startRowPosition + numberOfVisibleRowsToBeSelected; rowPosition++) {
Serializable rowId = getRowIdByPosition(rowPosition);
if (rowId != null) {
for (int columnPosition = startColumnPosition; columnPosition < startColumnPosition + numberOfVisibleColumnsToBeSelected; columnPosition++) {
this.selections.deselect(rowId, columnPosition);
}
}
}
}
} finally {
this.selectionsLock.writeLock().unlock();
}
}
/**
* Return the number of columns to select. Determines the number based on
* the columns that are known. Needed in case of full row selection as the
* range will be from 0 to {@link Integer#MAX_VALUE}, where only the range
* from 0 to total column count is needed.
*
* @param selection
* The rectangle that should be selected.
* @return The number of columns to select.
*/
private int getNumberOfColumnsToBeSelected(Rectangle selection) {
int columnCount = this.selectionLayer.getColumnCount();
return (selection.x + selection.width <= columnCount) ? selection.width : columnCount;
}
/**
* Return the number of rows to select. Determines the number based on the
* rows that are known. Needed in case of full column selection as the range
* will be from 0 to {@link Integer#MAX_VALUE}, where only the range from 0
* to total row count is needed.
*
* @param selection
* The rectangle that should be selected.
* @return The number of rows to select.
*/
private int getNumberOfRowsToBeSelected(Rectangle selection) {
int rowCount = this.selectionLayer.getRowCount();
return (selection.y + selection.height <= rowCount) ? selection.height : rowCount;
}
@Override
public boolean isEmpty() {
this.selectionsLock.readLock().lock();
try {
return this.selections.isEmpty();
} finally {
this.selectionsLock.readLock().unlock();
}
}
@Override
public List<Rectangle> getSelections() {
ArrayList<Rectangle> selectedCells = null;
this.selectionsLock.readLock().lock();
try {
Collection<CellPosition<T>> selectedPositions = this.selections.getSelections();
selectedCells = new ArrayList<>(selectedPositions.size());
for (CellPosition<T> cellPosition : selectedPositions) {
int rowPosition = getRowPositionByRowObject(cellPosition.getRowObject());
if (isRowVisible(rowPosition)) {
Integer columnPosition = cellPosition.getColumnPosition();
Rectangle selectedCell = new Rectangle(columnPosition, rowPosition, 1, 1);
selectedCells.add(selectedCell);
}
}
} finally {
this.selectionsLock.readLock().unlock();
}
return selectedCells;
}
/**
* Determines if rowPosition represents a visible row
*
* @param rowPosition
* position of row to inspect
* @return whether rowPosition represents a visible row
*/
private boolean isRowVisible(int rowPosition) {
return rowPosition != -1;
}
@Override
public boolean isCellPositionSelected(int columnPosition, int rowPosition) {
this.selectionsLock.readLock().lock();
try {
ILayerCell cell = this.selectionLayer.getCellByPosition(columnPosition, rowPosition);
int cellOriginRowPosition = cell.getOriginRowPosition();
for (int candidateRowPosition = cellOriginRowPosition; candidateRowPosition < cellOriginRowPosition + cell.getRowSpan(); candidateRowPosition++) {
Serializable rowId = getRowIdByPosition(candidateRowPosition);
int cellOriginColumnPosition = cell.getOriginColumnPosition();
for (int candidateColumnPosition = cellOriginColumnPosition; candidateColumnPosition < cellOriginColumnPosition + cell.getColumnSpan(); candidateColumnPosition++) {
if (this.selections.isSelected(rowId, candidateColumnPosition)) {
return true;
}
}
}
} finally {
this.selectionsLock.readLock().unlock();
}
return false;
}
@Override
public int[] getSelectedColumnPositions() {
this.selectionsLock.readLock().lock();
try {
return this.selections.getColumnPositions();
} finally {
this.selectionsLock.readLock().unlock();
}
}
@Override
public boolean isColumnPositionSelected(int columnPosition) {
this.selectionsLock.readLock().lock();
try {
for (Selections.Row<T> row : this.selections.getRows()) {
if (row.contains(columnPosition)) {
return true;
}
}
} finally {
this.selectionsLock.readLock().unlock();
}
return false;
}
@Override
public int[] getFullySelectedColumnPositions(int columnHeight) {
this.selectionsLock.readLock().lock();
try {
return Arrays.stream(this.selections.getColumnPositions())
.filter(selectedColumn -> isColumnPositionFullySelected(selectedColumn, columnHeight))
.toArray();
} finally {
this.selectionsLock.readLock().unlock();
}
}
@Override
public boolean isColumnPositionFullySelected(int columnPosition, int columnHeight) {
this.selectionsLock.readLock().lock();
try {
Selections.Column selectedRowsInColumn = this.selections.getSelectedRows(columnPosition);
if (hasColumnsSelectedRows(selectedRowsInColumn)) {
return selectedRowsInColumn.getItems().size() >= columnHeight;
}
return false;
} finally {
this.selectionsLock.readLock().unlock();
}
}
/**
* Determines if there are selected cells in a column
*
* @param column
* collections of selected cells for a column
* @return whether there are selected cells in column
*/
private boolean hasColumnsSelectedRows(Selections.Column column) {
return column != null;
}
@Override
public int getSelectedRowCount() {
this.selectionsLock.readLock().lock();
try {
return this.selections.getRows().size();
} finally {
this.selectionsLock.readLock().unlock();
}
}
@Override
public Set<Range> getSelectedRowPositions() {
HashSet<Range> visiblySelectedRowPositions = new HashSet<>();
this.selectionsLock.readLock().lock();
try {
for (Selections.Row<T> row : this.selections.getRows()) {
int rowPosition = getRowPositionByRowObject(row.getRowObject());
if (isRowVisible(rowPosition)) {
visiblySelectedRowPositions.add(new Range(rowPosition, rowPosition + 1));
}
}
} finally {
this.selectionsLock.readLock().unlock();
}
return visiblySelectedRowPositions;
}
@Override
public boolean isRowPositionSelected(int rowPosition) {
this.selectionsLock.readLock().lock();
try {
Serializable rowId = getRowIdByPosition(rowPosition);
return this.selections.isRowSelected(rowId);
} finally {
this.selectionsLock.readLock().unlock();
}
}
@Override
public int[] getFullySelectedRowPositions(int rowWidth) {
this.selectionsLock.readLock().lock();
try {
return this.selections.getRows().stream()
.map(Selections.Row::getRowObject)
.mapToInt(this::getRowPositionByRowObject)
.filter(rowPosition -> (isRowVisible(rowPosition)
&& isRowPositionFullySelected(rowPosition, rowWidth)))
.sorted()
.toArray();
} finally {
this.selectionsLock.readLock().unlock();
}
}
@Override
public boolean isRowPositionFullySelected(int rowPosition, int rowWidth) {
int[] selectedColumnPositions = null;
this.selectionsLock.readLock().lock();
try {
Serializable rowId = getRowIdByPosition(rowPosition);
if (rowId != null) {
Selections.Row<T> selectedColumnsInRow = this.selections.getSelectedColumns(rowId);
if (hasRowSelectedColumns(selectedColumnsInRow)) {
selectedColumnPositions = selectedColumnsInRow.getItems().stream()
.mapToInt(Integer::intValue)
.toArray();
}
}
} finally {
this.selectionsLock.readLock().unlock();
}
return (selectedColumnPositions == null || selectedColumnPositions.length < rowWidth)
? false
: SelectionUtils.isConsecutive(selectedColumnPositions);
}
/**
* Determines if there are selected cells in a row
*
* @param row
* collections of selected cells for a row
* @return whether there are selected cells in row
*/
private boolean hasRowSelectedColumns(Selections.Row<T> row) {
return row != null;
}
/**
* Retrieves the row ID for a row position
*
* @param rowPosition
* row position for retrieving row ID
* @return row ID for rowPosition, or null if undefined
*
* @since 1.6
*/
protected Serializable getRowIdByPosition(int rowPosition) {
T rowObject = getRowObjectByPosition(rowPosition);
if (rowObject != null) {
return this.rowIdAccessor.getRowId(rowObject);
}
return null;
}
/**
* Retrieves the row object for a row position
*
* @param rowPosition
* row position for retrieving row object
* @return row object for rowPosition, or null if undefined
*/
private T getRowObjectByPosition(int rowPosition) {
int rowIndex = this.selectionLayer.getRowIndexByPosition(rowPosition);
if (rowIndex >= 0) {
try {
return this.rowDataProvider.getRowObject(rowIndex);
} catch (Exception e) {
// row index is invalid for the data provider
}
}
return null;
}
/**
* Retrieves the row position for a row object
*
* @param rowObject
* row object for retrieving row position
* @return row position for rowObject, or -1 if undefined
*
* @since 1.6
*/
protected int getRowPositionByRowObject(T rowObject) {
int rowIndex = this.rowDataProvider.indexOfRowObject(rowObject);
if (rowIndex == -1) {
return -1;
}
return this.selectionLayer.getRowPositionByIndex(rowIndex);
}
@Override
public Point getSelectionAnchor() {
if (this.selectionAnchorPoint == null) {
this.selectionsLock.readLock().lock();
try {
this.selectionAnchorPoint = createMarkerPoint(this.selectionAnchor);
} finally {
this.selectionsLock.readLock().unlock();
}
}
return this.selectionAnchorPoint;
}
@Override
public Point getLastSelectedCell() {
this.selectionsLock.readLock().lock();
try {
return createMarkerPoint(this.lastSelectedCell);
} finally {
this.selectionsLock.readLock().unlock();
}
}
/**
* Creates a point from a cell position. The point is expressed in row
* position and column position. The row position is calculated by
* translating the row object of the cell position. It uses the column
* position of the cell position without translation.
*
* @param cellPosition
* cell position to translate into a point
* @return cellPosition expressed in row position and column position
*/
private Point createMarkerPoint(CellPosition<T> cellPosition) {
if (cellPosition == null
|| (cellPosition.getColumnPosition() == SelectionLayer.NO_SELECTION && cellPosition.getRowObject() == null)) {
return createUndefinedPoint();
}
int rowPosition = getRowPositionByRowObject(cellPosition.getRowObject());
return new Point(cellPosition.getColumnPosition(), rowPosition);
}
/**
* Creates an undefined point, using the SelectionLayer.NO_SELECTION
* constant.
*
* @return an undefined point
*/
private Point createUndefinedPoint() {
return new Point(SelectionLayer.NO_SELECTION, SelectionLayer.NO_SELECTION);
}
@Override
public Rectangle getLastSelectedRegion() {
this.selectionsLock.readLock().lock();
try {
if (this.lastSelectedRegion == null) {
return null;
} else {
correctLastSelectedRegion();
return this.lastSelectedRegion;
}
} finally {
this.selectionsLock.readLock().unlock();
}
}
/**
* Corrects the last selected region by moving it so that it is originating
* on the row position identified by the last selected region origin row
* object.
*/
private void correctLastSelectedRegion() {
this.lastSelectedRegion.y = getRowPositionByRowObject(this.lastSelectedRegionOriginRowObject);
}
@Override
public void setSelectionAnchor(Point coordinate) {
this.selectionsLock.writeLock().lock();
try {
if (coordinate.x == SelectionLayer.NO_SELECTION
&& coordinate.y == SelectionLayer.NO_SELECTION) {
this.selectionAnchor = null;
} else {
this.selectionAnchor =
new CellPosition<>(getRowObjectByPosition(coordinate.y), coordinate.x);
}
} finally {
this.selectionsLock.writeLock().unlock();
this.selectionAnchorPoint = null;
}
}
@Override
public void setLastSelectedCell(Point coordinate) {
this.selectionsLock.writeLock().lock();
try {
if (coordinate.x == SelectionLayer.NO_SELECTION
&& coordinate.y == SelectionLayer.NO_SELECTION) {
this.lastSelectedCell = null;
} else {
this.lastSelectedCell =
new CellPosition<>(getRowObjectByPosition(coordinate.y), coordinate.x);
}
} finally {
this.selectionsLock.writeLock().unlock();
}
}
@Override
public void setLastSelectedRegion(Rectangle region) {
// clear the selection in the current last selected region
if (region != null && this.lastSelectedRegion != null) {
clearSelection(this.lastSelectedRegion);
}
this.selectionsLock.writeLock().lock();
try {
this.lastSelectedRegion = region;
if (region != null) {
this.lastSelectedRegionOriginRowObject = getRowObjectByPosition(region.y);
}
} finally {
this.selectionsLock.writeLock().unlock();
}
}
@Override
public void setLastSelectedRegion(int x, int y, int width, int height) {
this.selectionsLock.writeLock().lock();
try {
this.lastSelectedRegion.x = x;
this.lastSelectedRegion.y = y;
this.lastSelectedRegion.width = width;
this.lastSelectedRegion.height = height;
this.lastSelectedRegionOriginRowObject = getRowObjectByPosition(y);
} finally {
this.selectionsLock.writeLock().unlock();
}
}
@Override
public void handleLayerEvent(IStructuralChangeEvent event) {
// we are not interested in resize events
if (event instanceof ResizeStructuralRefreshEvent
|| event instanceof ColumnResizeEvent
|| event instanceof RowResizeEvent) {
return;
}
// ensure the selection anchor point is calculated on the next access
this.selectionAnchorPoint = null;
// handling for deleting columns
if (event.isHorizontalStructureChanged()) {
Collection<StructuralDiff> diffs = event.getColumnDiffs();
if (diffs != null) {
// first handle deletion, then handle insert
// this is to avoid mixed operations that might lead to
// confusing indexes
List<Integer> removed = new ArrayList<>();
for (StructuralDiff columnDiff : diffs) {
if (columnDiff.getDiffType() != null
&& columnDiff.getDiffType().equals(DiffTypeEnum.DELETE)) {
Range beforePositionRange = columnDiff.getBeforePositionRange();
// first de-select removed columns
for (int i = beforePositionRange.start; i < beforePositionRange.end; i++) {
if (!(event instanceof ColumnReorderEvent)) {
// in case the column was reordered we don't
// want to deselect the column
this.selections.deselectColumn(i);
}
removed.add(i);
}
}
}
// now update still visible column selections
Collections.sort(removed);
int mod = 0;
for (int i : removed) {
// ask for further column selections that need to be
// modified
this.selections.updateColumnsForRemoval(i - mod);
mod++;
}
for (StructuralDiff columnDiff : diffs) {
if (columnDiff.getDiffType() != null
&& columnDiff.getDiffType().equals(DiffTypeEnum.ADD)) {
Range afterPositionRange = columnDiff.getAfterPositionRange();
for (int i = afterPositionRange.start; i < afterPositionRange.end; i++) {
// ask for column selections that need to be
// modified
this.selections.updateColumnsForAddition(i);
}
}
}
}
}
// handling for deleting rows
if (event.isVerticalStructureChanged()) {
// the change is already done and we don't know about indexes, so we
// need to check if the selected objects still exist
Collection<Serializable> keysToRemove = new ArrayList<>();
for (Selections.Row<T> row : this.selections.getRows()) {
if (!ignoreVerticalChange(row)) {
int rowIndex = this.rowDataProvider.indexOfRowObject(row.getRowObject());
if (rowIndex == -1 || this.selectionLayer.getRowPositionByIndex(rowIndex) == -1) {
keysToRemove.add(row.getId());
}
}
}
for (Serializable toRemove : keysToRemove) {
this.selections.deselectRow(toRemove);
}
}
}
/**
* Check if the default handling for vertical structure changes should be
* performed for the given {@link Row}, or if it should be skipped. Skipping
* for example would make sense for selections that are stored for rows that
* have no row data in the backing data structure, e.g. a summary row
* selection.
*
* @param row
* The internal selected row representation.
* @return <code>false</code> if the default handling for vertical changes
* should be performed, <code>true</code> if the default handling
* should be skipped.
*
* @since 1.6
*/
protected boolean ignoreVerticalChange(Selections.Row<T> row) {
return false;
}
@Override
public Class<IStructuralChangeEvent> getLayerEventClass() {
return IStructuralChangeEvent.class;
}
}