| /******************************************************************************* |
| * 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; |
| } |
| } |