blob: 8bba251e4c5bf4ea6f37d4cc98e00dcb1ab1401c [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
* Roman Flueckiger <roman.flueckiger@mac.com> - Bug 450334
* Dirk Fauth <dirk.fauth@googlemail.com> - Bug 446276, 446275
******************************************************************************/
package org.eclipse.nebula.widgets.nattable.selection;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.eclipse.nebula.widgets.nattable.coordinate.PositionCoordinate;
import org.eclipse.nebula.widgets.nattable.coordinate.Range;
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.StructuralDiff;
import org.eclipse.nebula.widgets.nattable.layer.event.StructuralDiff.DiffTypeEnum;
import org.eclipse.nebula.widgets.nattable.util.ArrayUtil;
import org.eclipse.nebula.widgets.nattable.util.ObjectUtils;
import org.eclipse.swt.graphics.Rectangle;
/**
* Tracks the selections made in the table. All selections are tracked in terms
* of Rectangles.
*
* For example if the table has 10 rows and column 2 is selected, the Rectangle
* tracked is (0, 2, 10, 1)
*
* Coordinates are in <i>Selection Layer positions</i>
*
* @see SelectionLayer
*/
public class SelectionModel implements ISelectionModel {
protected final SelectionLayer selectionLayer;
private boolean multipleSelectionAllowed;
private final List<Rectangle> selections;
private final ReadWriteLock selectionsLock;
private boolean clearSelectionOnChange = true;
public SelectionModel(SelectionLayer selectionLayer) {
this(selectionLayer, true);
}
public SelectionModel(SelectionLayer selectionLayer, boolean multipleSelectionAllowed) {
this.selectionLayer = selectionLayer;
this.multipleSelectionAllowed = multipleSelectionAllowed;
this.selections = new LinkedList<>();
this.selectionsLock = new ReentrantReadWriteLock();
}
@Override
public boolean isMultipleSelectionAllowed() {
return this.multipleSelectionAllowed;
}
@Override
public void setMultipleSelectionAllowed(boolean multipleSelectionAllowed) {
this.multipleSelectionAllowed = multipleSelectionAllowed;
}
@Override
public void addSelection(int columnPosition, int rowPosition) {
addSelectionIntoList(new Rectangle(columnPosition, rowPosition, 1, 1));
}
@Override
public void addSelection(final Rectangle range) {
if (range != null) {
addSelectionIntoList(range);
}
}
private void addSelectionIntoList(Rectangle selection) {
this.selectionsLock.writeLock().lock();
try {
if (this.multipleSelectionAllowed) {
ArrayList<Rectangle> itemsToRemove = null;
for (Rectangle r : this.selections) {
if (selection.intersects(r)) {
if (r.equals(selection)) {
break;
}
Rectangle intersection = selection.intersection(r);
if (intersection.equals(r)) {
// r is a subset of intersection
if (itemsToRemove == null) {
itemsToRemove = new ArrayList<>();
}
itemsToRemove.add(r);
} else if (intersection.equals(selection)) {
// selection is a subset of r
break;
}
}
}
if (itemsToRemove != null) {
this.selections.removeAll(itemsToRemove);
}
} else {
this.selections.clear();
// as no multiple selection is allowed, ensure that only one
// column and one row will be selected
selection.height = 1;
selection.width = 1;
}
this.selections.add(selection);
} 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) {
clearSelection(new Rectangle(columnPosition, rowPosition, 1, 1));
}
@Override
public void clearSelection(Rectangle removedSelection) {
List<Rectangle> removedItems = new LinkedList<>();
List<Rectangle> addedItems = new LinkedList<>();
this.selectionsLock.readLock().lock();
try {
for (Rectangle r : this.selections) {
if (r.intersects(removedSelection)) {
Rectangle intersection = removedSelection.intersection(r);
removedItems.add(r);
Rectangle topSelection = getTopSelection(intersection, r);
if (topSelection != null) {
addedItems.add(topSelection);
}
Rectangle rightSelection = getRightSelection(intersection,
r);
if (rightSelection != null)
addedItems.add(rightSelection);
Rectangle leftSelection = getLeftSelection(intersection, r);
if (leftSelection != null)
addedItems.add(leftSelection);
Rectangle bottomSelection = getBottomSelection(
intersection, r);
if (bottomSelection != null)
addedItems.add(bottomSelection);
}
}
} finally {
this.selectionsLock.readLock().unlock();
}
if (!removedItems.isEmpty()) {
this.selectionsLock.writeLock().lock();
try {
this.selections.removeAll(removedItems);
} finally {
this.selectionsLock.writeLock().unlock();
}
removedItems.clear();
}
if (!addedItems.isEmpty()) {
this.selectionsLock.writeLock().lock();
try {
this.selections.addAll(addedItems);
} finally {
this.selectionsLock.writeLock().unlock();
}
addedItems.clear();
}
}
@Override
public boolean isEmpty() {
this.selectionsLock.readLock().lock();
try {
return this.selections.isEmpty();
} finally {
this.selectionsLock.readLock().unlock();
}
}
@Override
public List<Rectangle> getSelections() {
return this.selections;
}
// Cell features
@Override
public boolean isCellPositionSelected(int columnPosition, int rowPosition) {
this.selectionsLock.readLock().lock();
try {
ILayerCell cell = this.selectionLayer.getCellByPosition(columnPosition, rowPosition);
if (cell != null) {
Rectangle cellRectangle = new Rectangle(
cell.getOriginColumnPosition(),
cell.getOriginRowPosition(),
cell.getColumnSpan(),
cell.getRowSpan());
for (Rectangle selectionRectangle : this.selections) {
if (selectionRectangle.intersects(cellRectangle))
return true;
}
}
} finally {
this.selectionsLock.readLock().unlock();
}
return false;
}
// Column features
@Override
public int[] getSelectedColumnPositions() {
TreeSet<Integer> selectedColumns = new TreeSet<>();
this.selectionsLock.readLock().lock();
int columnCount = this.selectionLayer.getColumnCount();
try {
for (Rectangle r : this.selections) {
int startColumn = r.x;
if (startColumn < columnCount) {
int numColumns = (r.x + r.width <= columnCount) ? r.width : columnCount - r.x;
// Change from row < startRow to row < startRow+numRows
for (int column = startColumn; column < startColumn + numColumns; column++) {
selectedColumns.add(Integer.valueOf(column));
}
}
}
} finally {
this.selectionsLock.readLock().unlock();
}
// Convert to array
return ObjectUtils.asIntArray(selectedColumns);
}
/**
* @since 1.5
*/
protected Set<Range> internalGetSelectedColumnPositions() {
Set<Range> selectedColumnsRange = new HashSet<>();
this.selectionsLock.readLock().lock();
int columnCount = this.selectionLayer.getColumnCount();
try {
for (Rectangle r : this.selections) {
if (r.x < columnCount) {
int width = (r.x + r.width <= columnCount) ? r.width : columnCount - r.x;
selectedColumnsRange.add(new Range(r.x, r.x + width));
}
}
} finally {
this.selectionsLock.readLock().unlock();
}
ArrayList<Range> ranges = new ArrayList<>(selectedColumnsRange);
Range.sortByStart(ranges);
List<Range> uniqueRanges = new ArrayList<>(ranges.size());
// Adjust for overlaps - between consecutive selections
for (int i = 0; i < ranges.size(); i++) {
if (i > 0) {
Range previousRange = ranges.get(i - 1);
Range currentRange = ranges.get(i);
if (previousRange.overlap(currentRange)
|| (previousRange.end == currentRange.start)) {
int largerRangeEnd = (previousRange.end > currentRange.end) ? previousRange.end : currentRange.end;
uniqueRanges.get(uniqueRanges.size() - 1).end = largerRangeEnd;
ranges.get(i).end = largerRangeEnd;
} else {
uniqueRanges.add(ranges.get(i));
}
} else {
uniqueRanges.add(ranges.get(i));
}
}
return new HashSet<>(uniqueRanges);
}
@Override
public boolean isColumnPositionSelected(int columnPosition) {
this.selectionsLock.readLock().lock();
try {
for (Range columnRange : internalGetSelectedColumnPositions()) {
if (columnRange.contains(columnPosition)) {
return true;
}
}
} finally {
this.selectionsLock.readLock().unlock();
}
return false;
}
@Override
public int[] getFullySelectedColumnPositions(int columnHeight) {
final int[] selectedColumns = getSelectedColumnPositions();
int[] columnsToHide = new int[selectedColumns.length];
int index = 0;
for (int columnPosition : selectedColumns) {
if (isColumnPositionFullySelected(columnPosition, columnHeight)) {
columnsToHide[index++] = columnPosition;
}
}
return index > 0 ? ArrayUtil.subarray(columnsToHide, 0, index) : new int[0];
}
/**
* Are all cells in this column selected? Different selection rectangles
* might aggregate to cover the entire column. We need to take into account
* any overlapping selections or any selection rectangles contained within
* each other.
*
* See the related tests for a better understanding.
*/
@Override
public boolean isColumnPositionFullySelected(int columnPosition, int columnHeight) {
this.selectionsLock.readLock().lock();
try {
// Aggregate all rectangles in the column which are in the selection
// model
List<Rectangle> selectedRectanglesInColumn = new ArrayList<>(this.selections.size());
// If X is same add up the height of the selected area
for (Rectangle r : this.selections) {
// Column is within the bounds of the selected rectangle
if (columnPosition >= r.x && columnPosition < r.x + r.width) {
selectedRectanglesInColumn.add(new Rectangle(
columnPosition,
r.y,
1,
r.height));
}
}
if (selectedRectanglesInColumn.isEmpty()) {
return false;
}
sortByY(selectedRectanglesInColumn);
Rectangle finalRectangle = new Rectangle(
columnPosition,
selectedRectanglesInColumn.get(0).y,
0,
0);
// Ensure that selections in the column are contiguous and cover the
// entire column
for (int i = 0; i < selectedRectanglesInColumn.size(); i++) {
Rectangle rectangle = selectedRectanglesInColumn.get(i);
if (contains(finalRectangle, rectangle)) {
continue;
}
if (i > 0) {
Rectangle previousRect = selectedRectanglesInColumn
.get(i - 1);
if (rectangle.union(previousRect).height > (rectangle.height + previousRect.height)) {
// Rectangles not contiguous
return false;
}
}
// Union will resolve any overlapping area
finalRectangle = finalRectangle.union(rectangle);
}
return finalRectangle.height >= columnHeight;
} finally {
this.selectionsLock.readLock().unlock();
}
}
// Row features
@Override
public int getSelectedRowCount() {
Set<Range> selectedRows = getSelectedRowPositions();
int count = 0;
for (Range range : selectedRows) {
count += range.end - range.start;
}
return count;
}
@Override
public Set<Range> getSelectedRowPositions() {
Set<Range> selectedRowsRange = new HashSet<>();
this.selectionsLock.readLock().lock();
int rowCount = this.selectionLayer.getRowCount();
try {
for (Rectangle r : this.selections) {
if (r.y < rowCount) {
int height = (r.y + r.height <= rowCount) ? r.height : rowCount - r.y;
selectedRowsRange.add(new Range(r.y, r.y + height));
}
}
} finally {
this.selectionsLock.readLock().unlock();
}
ArrayList<Range> ranges = new ArrayList<>(selectedRowsRange);
Range.sortByStart(ranges);
List<Range> uniqueRanges = new ArrayList<>(ranges.size());
// Adjust for overlaps - between consecutive selections
for (int i = 0; i < ranges.size(); i++) {
if (i > 0) {
Range previousRange = ranges.get(i - 1);
Range currentRange = ranges.get(i);
if (previousRange.overlap(currentRange)
|| (previousRange.end == currentRange.start)) {
int largerRangeEnd = (previousRange.end > currentRange.end) ? previousRange.end : currentRange.end;
uniqueRanges.get(uniqueRanges.size() - 1).end = largerRangeEnd;
ranges.get(i).end = largerRangeEnd;
} else {
uniqueRanges.add(ranges.get(i));
}
} else {
uniqueRanges.add(ranges.get(i));
}
}
return new HashSet<>(uniqueRanges);
}
@Override
public boolean isRowPositionSelected(int rowPosition) {
this.selectionsLock.readLock().lock();
try {
for (Range rowRange : getSelectedRowPositions()) {
if (rowRange.contains(rowPosition)) {
return true;
}
}
} finally {
this.selectionsLock.readLock().unlock();
}
return false;
}
@Override
public int[] getFullySelectedRowPositions(int rowWidth) {
final Set<Range> selectedRows = getSelectedRowPositions();
int[] fullySelectedRows = new int[getSelectedRowCount()];
int index = 0;
for (Range rowRange : selectedRows) {
for (int i = rowRange.start; i < rowRange.end; i++) {
if (isRowPositionFullySelected(i, rowWidth)) {
fullySelectedRows[index++] = i;
}
}
}
return index > 0 ? ArrayUtil.subarray(fullySelectedRows, 0, index) : new int[0];
}
@Override
public boolean isRowPositionFullySelected(int rowPosition, int rowWidth) {
this.selectionsLock.readLock().lock();
try {
// Aggregate all rectangles in the row which are in the selection
// model
List<Rectangle> selectedRectanglesInRow = new ArrayList<>(this.selections.size());
// If X is same add up the width of the selected area
for (Rectangle r : this.selections) {
// Row is within the bounds of the selected rectangle
if (rowPosition >= r.y && rowPosition < r.y + r.height) {
selectedRectanglesInRow.add(new Rectangle(
r.x,
rowPosition,
r.width,
1));
}
}
if (selectedRectanglesInRow.isEmpty()) {
return false;
}
sortByX(selectedRectanglesInRow);
Rectangle finalRectangle = new Rectangle(
selectedRectanglesInRow.get(0).x,
rowPosition,
0,
0);
// Ensure that selections in the row are contiguous and cover the
// entire row
for (int i = 0; i < selectedRectanglesInRow.size(); i++) {
Rectangle rectangle = selectedRectanglesInRow.get(i);
if (contains(finalRectangle, rectangle)) {
continue;
}
if (i > 0) {
Rectangle previousRect = selectedRectanglesInRow.get(i - 1);
if (rectangle.union(previousRect).width > (rectangle.width + previousRect.width)) {
// Rectangles not contiguous
return false;
}
}
// Union will resolve any overlapping area
finalRectangle = finalRectangle.union(rectangle);
}
return finalRectangle.width >= rowWidth;
} finally {
this.selectionsLock.readLock().unlock();
}
}
protected boolean contains(Rectangle containerRectangle, Rectangle rectangle) {
Rectangle union = containerRectangle.union(rectangle);
return union.equals(containerRectangle);
}
protected void sortByX(List<Rectangle> selectionRectanglesInRow) {
Collections.sort(selectionRectanglesInRow, (rectangle1, rectangle2) -> Integer.valueOf(rectangle1.x).compareTo(Integer.valueOf(rectangle2.x)));
}
protected void sortByY(List<Rectangle> selectionRectanglesInColumn) {
Collections.sort(selectionRectanglesInColumn,
(rectangle1, rectangle2) -> Integer.valueOf(rectangle1.y).compareTo(Integer.valueOf(rectangle2.y)));
}
private Rectangle getLeftSelection(Rectangle intersection, Rectangle selection) {
if (intersection.x > selection.x) {
return new Rectangle(
selection.x,
selection.y,
intersection.x - selection.x,
selection.height);
}
return null;
}
private Rectangle getRightSelection(Rectangle intersection, Rectangle selection) {
int newX = intersection.x + intersection.width;
if (newX < selection.x + selection.width) {
return new Rectangle(
newX,
selection.y,
selection.x + selection.width - newX,
selection.height);
}
return null;
}
private Rectangle getTopSelection(Rectangle intersection, Rectangle selection) {
if (intersection.y > selection.y) {
return new Rectangle(
selection.x,
selection.y,
selection.width,
intersection.y - selection.y);
}
return null;
}
private Rectangle getBottomSelection(Rectangle intersection, Rectangle selection) {
int newY = intersection.y + intersection.height;
if (newY < selection.y + selection.height) {
return new Rectangle(
selection.x,
newY,
selection.width,
selection.y + selection.height - newY);
}
return null;
}
// Object methods
@Override
public String toString() {
this.selectionsLock.readLock().lock();
try {
return this.selections.toString();
} finally {
this.selectionsLock.readLock().unlock();
}
}
@Override
public void handleLayerEvent(IStructuralChangeEvent event) {
if (this.clearSelectionOnChange) {
if (event.isHorizontalStructureChanged()) {
if (event.getColumnDiffs() == null) {
Collection<Rectangle> rectangles = event.getChangedPositionRectangles();
for (Rectangle rectangle : rectangles) {
Range changedRange = new Range(rectangle.y, rectangle.y + rectangle.height);
if (selectedColumnModified(changedRange)) {
this.selectionLayer.clear();
break;
}
}
} else {
for (StructuralDiff diff : event.getColumnDiffs()) {
// DiffTypeEnum.CHANGE is used for resizing and
// shouldn't result in clearing the selection
if (diff.getDiffType() != DiffTypeEnum.CHANGE) {
if (selectedColumnModified(diff.getBeforePositionRange())) {
this.selectionLayer.clear();
break;
}
}
}
}
}
if (event.isVerticalStructureChanged()) {
// if there are no row diffs, it seems to be a complete refresh
if (event.getRowDiffs() == null) {
Collection<Rectangle> rectangles = event.getChangedPositionRectangles();
for (Rectangle rectangle : rectangles) {
Range changedRange = new Range(rectangle.y, rectangle.y + rectangle.height);
if (selectedRowModified(changedRange)) {
this.selectionLayer.clear();
break;
}
}
} else {
// there are row diffs so we try to determine the diffs to
// process
for (StructuralDiff diff : event.getRowDiffs()) {
// DiffTypeEnum.CHANGE is used for resizing and
// shouldn't result in clearing the selection
if (diff.getDiffType() != DiffTypeEnum.CHANGE) {
if (selectedRowModified(diff.getBeforePositionRange())) {
this.selectionLayer.clear();
break;
}
}
}
}
}
} else {
// keep the selection as is in case of changes
// Note:
// this is the the same code I posted in various forums as a
// workaround for the cleaning of the selection on changes
// search for PreserveSelectionStructuralChangeEventHandler to get
// more information on this
PositionCoordinate[] coords = this.selectionLayer.getSelectedCellPositions();
for (PositionCoordinate coord : coords) {
if (coord.getColumnPosition() >= this.selectionLayer.getColumnCount()
|| coord.getRowPosition() >= this.selectionLayer.getRowCount()) {
// if the coordinates of the selected cells are outside the
// valid range remove the selection
this.selectionLayer.clearSelection(
coord.getColumnPosition(),
coord.getRowPosition());
}
}
}
}
private boolean selectedRowModified(Range changedRange) {
Set<Range> selectedRows = this.selectionLayer.getSelectedRowPositions();
for (Range rowRange : selectedRows) {
if (rowRange.overlap(changedRange)) {
return true;
}
}
// if the selection layer is empty, we should clear the selection also
return (this.selectionLayer.getRowCount() == 0 && !this.isEmpty());
}
private boolean selectedColumnModified(Range changedRange) {
for (int i = changedRange.start; i <= changedRange.end; i++) {
if (isColumnPositionSelected(i)) {
return true;
}
}
return false;
}
/**
*
* @param clearSelectionOnChange
* <code>true</code> to simply clear the selection on structural
* changes, <code>false</code> to keep the valid selection
* (selection of cells that still exist)
*/
public void setClearSelectionOnChange(boolean clearSelectionOnChange) {
this.clearSelectionOnChange = clearSelectionOnChange;
}
@Override
public Class<IStructuralChangeEvent> getLayerEventClass() {
return IStructuralChangeEvent.class;
}
}