| /******************************************************************************* |
| * 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 |
| * Dirk Fauth <dirk.fauth@googlemail.com> - Bug 447259, 446275, 447394, 446276 |
| * Vincent Lorenzo <vincent.lorenzo@cea.fr> - Bug 478622 |
| ******************************************************************************/ |
| package org.eclipse.nebula.widgets.nattable.selection; |
| |
| import java.io.Serializable; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.concurrent.locks.ReadWriteLock; |
| import java.util.concurrent.locks.ReentrantReadWriteLock; |
| import java.util.stream.IntStream; |
| |
| 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.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.selection.event.RowSelectionEvent; |
| import org.eclipse.swt.graphics.Rectangle; |
| |
| public class RowSelectionModel<R> implements IRowSelectionModel<R> { |
| |
| protected final SelectionLayer selectionLayer; |
| protected final IRowDataProvider<R> rowDataProvider; |
| protected final IRowIdAccessor<R> rowIdAccessor; |
| private boolean multipleSelectionAllowed; |
| |
| protected Map<Serializable, R> selectedRows; |
| /** |
| * *live* reference to last range parameter used in addSelection(range) |
| */ |
| protected Rectangle lastSelectedRange; |
| protected Set<Serializable> lastSelectedRowIds; |
| protected final ReadWriteLock selectionsLock; |
| |
| public RowSelectionModel(SelectionLayer selectionLayer, |
| IRowDataProvider<R> rowDataProvider, IRowIdAccessor<R> rowIdAccessor) { |
| this(selectionLayer, rowDataProvider, rowIdAccessor, true); |
| } |
| |
| public RowSelectionModel(SelectionLayer selectionLayer, |
| IRowDataProvider<R> rowDataProvider, |
| IRowIdAccessor<R> rowIdAccessor, boolean multipleSelectionAllowed) { |
| this.selectionLayer = selectionLayer; |
| this.rowDataProvider = rowDataProvider; |
| this.rowIdAccessor = rowIdAccessor; |
| this.multipleSelectionAllowed = multipleSelectionAllowed; |
| |
| this.selectedRows = new HashMap<>(); |
| 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) { |
| this.selectionsLock.writeLock().lock(); |
| |
| try { |
| if (!this.multipleSelectionAllowed) { |
| this.selectedRows.clear(); |
| } |
| |
| R rowObject = getRowObjectByPosition(rowPosition); |
| if (rowObject != null) { |
| Serializable rowId = this.rowIdAccessor.getRowId(rowObject); |
| this.selectedRows.put(rowId, rowObject); |
| } |
| } finally { |
| this.selectionsLock.writeLock().unlock(); |
| } |
| } |
| |
| @Override |
| public void addSelection(Rectangle range) { |
| this.selectionsLock.writeLock().lock(); |
| |
| try { |
| if (!this.multipleSelectionAllowed) { |
| // as no multiple selection is allowed, ensure that only one row |
| // will be selected |
| this.selectedRows.clear(); |
| range.height = 1; |
| } |
| |
| HashMap<Serializable, R> rowsToSelect = new HashMap<>(); |
| |
| int maxY = Math.min(range.y + range.height, this.selectionLayer.getRowCount()); |
| for (int rowPosition = range.y; rowPosition < maxY; rowPosition++) { |
| R rowObject = getRowObjectByPosition(rowPosition); |
| if (rowObject != null) { |
| Serializable rowId = this.rowIdAccessor.getRowId(rowObject); |
| rowsToSelect.put(rowId, rowObject); |
| } |
| } |
| |
| this.selectedRows.putAll(rowsToSelect); |
| |
| if (range.equals(this.lastSelectedRange)) { |
| this.lastSelectedRowIds = rowsToSelect.keySet(); |
| } else { |
| this.lastSelectedRowIds = null; |
| } |
| |
| this.lastSelectedRange = range; |
| } finally { |
| this.selectionsLock.writeLock().unlock(); |
| } |
| } |
| |
| @Override |
| public void clearSelection() { |
| this.selectionsLock.writeLock().lock(); |
| try { |
| this.selectedRows.clear(); |
| } finally { |
| this.selectionsLock.writeLock().unlock(); |
| } |
| } |
| |
| @Override |
| public void clearSelection(int columnPosition, int rowPosition) { |
| this.selectionsLock.writeLock().lock(); |
| |
| try { |
| Serializable rowId = getRowIdByPosition(rowPosition); |
| this.selectedRows.remove(rowId); |
| } finally { |
| this.selectionsLock.writeLock().unlock(); |
| } |
| } |
| |
| @Override |
| public void clearSelection(Rectangle removedSelection) { |
| this.selectionsLock.writeLock().lock(); |
| |
| try { |
| int maxY = Math.min( |
| removedSelection.y + removedSelection.height, |
| this.selectionLayer.getRowCount()); |
| for (int rowPosition = removedSelection.y; rowPosition < maxY; rowPosition++) { |
| clearSelection(0, rowPosition); |
| } |
| } finally { |
| this.selectionsLock.writeLock().unlock(); |
| } |
| } |
| |
| @Override |
| public void clearSelection(R rowObject) { |
| this.selectionsLock.writeLock().lock(); |
| |
| try { |
| this.selectedRows.remove(this.rowIdAccessor.getRowId(rowObject)); |
| } finally { |
| this.selectionsLock.writeLock().unlock(); |
| } |
| } |
| |
| @Override |
| public boolean isEmpty() { |
| this.selectionsLock.readLock().lock(); |
| |
| try { |
| return this.selectedRows.isEmpty(); |
| } finally { |
| this.selectionsLock.readLock().unlock(); |
| } |
| } |
| |
| @Override |
| public List<Rectangle> getSelections() { |
| ArrayList<Rectangle> selectionRectangles = new ArrayList<>(this.selectedRows.size()); |
| |
| this.selectionsLock.readLock().lock(); |
| |
| try { |
| int width = this.selectionLayer.getColumnCount(); |
| for (Serializable rowId : this.selectedRows.keySet()) { |
| int rowPosition = getRowPositionById(rowId); |
| selectionRectangles.add(new Rectangle(0, rowPosition, width, 1)); |
| } |
| } finally { |
| this.selectionsLock.readLock().unlock(); |
| } |
| |
| return selectionRectangles; |
| } |
| |
| // Cell features |
| |
| @Override |
| public boolean isCellPositionSelected(int columnPosition, int rowPosition) { |
| ILayerCell cell = this.selectionLayer.getCellByPosition(columnPosition, rowPosition); |
| if (cell != null) { |
| int cellOriginRowPosition = cell.getOriginRowPosition(); |
| for (int testRowPosition = cellOriginRowPosition; testRowPosition < cellOriginRowPosition + cell.getRowSpan(); testRowPosition++) { |
| if (isRowPositionSelected(testRowPosition)) { |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| |
| // Column features |
| |
| @Override |
| public int[] getSelectedColumnPositions() { |
| if (!isEmpty()) { |
| this.selectionsLock.readLock().lock(); |
| |
| int columnCount; |
| |
| try { |
| columnCount = this.selectionLayer.getColumnCount(); |
| } finally { |
| this.selectionsLock.readLock().unlock(); |
| } |
| |
| int[] columns = new int[columnCount]; |
| for (int i = 0; i < columnCount; i++) { |
| columns[i] = i; |
| } |
| return columns; |
| } |
| return new int[] {}; |
| } |
| |
| @Override |
| public boolean isColumnPositionSelected(int columnPosition) { |
| this.selectionsLock.readLock().lock(); |
| |
| try { |
| return !this.selectedRows.isEmpty(); |
| } finally { |
| this.selectionsLock.readLock().unlock(); |
| } |
| } |
| |
| @Override |
| public int[] getFullySelectedColumnPositions(int fullySelectedColumnRowCount) { |
| this.selectionsLock.readLock().lock(); |
| |
| try { |
| if (isColumnPositionFullySelected(0, fullySelectedColumnRowCount)) { |
| return getSelectedColumnPositions(); |
| } |
| } finally { |
| this.selectionsLock.readLock().unlock(); |
| } |
| |
| return new int[] {}; |
| } |
| |
| @Override |
| public boolean isColumnPositionFullySelected(int columnPosition, int fullySelectedColumnRowCount) { |
| this.selectionsLock.readLock().lock(); |
| |
| try { |
| int selectedRowCount = this.selectedRows.size(); |
| |
| if (selectedRowCount == 0) { |
| return false; |
| } |
| |
| return selectedRowCount == fullySelectedColumnRowCount; |
| } finally { |
| this.selectionsLock.readLock().unlock(); |
| } |
| } |
| |
| // Row features |
| |
| @Override |
| public List<R> getSelectedRowObjects() { |
| ArrayList<R> rowObjects = new ArrayList<>(this.selectedRows.size()); |
| |
| this.selectionsLock.readLock().lock(); |
| try { |
| rowObjects.addAll(this.selectedRows.values()); |
| } finally { |
| this.selectionsLock.readLock().unlock(); |
| } |
| |
| return rowObjects; |
| } |
| |
| @Override |
| public int getSelectedRowCount() { |
| this.selectionsLock.readLock().lock(); |
| |
| try { |
| return this.selectedRows.size(); |
| } finally { |
| this.selectionsLock.readLock().unlock(); |
| } |
| } |
| |
| @Override |
| public Set<Range> getSelectedRowPositions() { |
| HashSet<Range> selectedRowRanges = new HashSet<>(); |
| |
| this.selectionsLock.readLock().lock(); |
| |
| try { |
| for (Serializable rowId : this.selectedRows.keySet()) { |
| int rowPosition = getRowPositionById(rowId); |
| selectedRowRanges.add(new Range(rowPosition, rowPosition + 1)); |
| } |
| } finally { |
| this.selectionsLock.readLock().unlock(); |
| } |
| |
| return selectedRowRanges; |
| } |
| |
| @Override |
| public boolean isRowPositionSelected(int rowPosition) { |
| this.selectionsLock.readLock().lock(); |
| |
| try { |
| Serializable rowId = getRowIdByPosition(rowPosition); |
| return this.selectedRows.containsKey(rowId); |
| } finally { |
| this.selectionsLock.readLock().unlock(); |
| } |
| } |
| |
| @Override |
| public int[] getFullySelectedRowPositions(int rowWidth) { |
| this.selectionsLock.readLock().lock(); |
| |
| try { |
| int selectedRowCount = this.selectedRows.size(); |
| int[] selectedRowPositions = new int[selectedRowCount]; |
| int i = 0; |
| for (Serializable rowId : this.selectedRows.keySet()) { |
| selectedRowPositions[i] = getRowPositionById(rowId); |
| i++; |
| } |
| return selectedRowPositions; |
| } finally { |
| this.selectionsLock.readLock().unlock(); |
| } |
| } |
| |
| @Override |
| public boolean isRowPositionFullySelected(int rowPosition, int rowWidth) { |
| return isRowPositionSelected(rowPosition); |
| } |
| |
| private Serializable getRowIdByPosition(int rowPosition) { |
| R rowObject = getRowObjectByPosition(rowPosition); |
| if (rowObject != null) { |
| return this.rowIdAccessor.getRowId(rowObject); |
| } |
| return null; |
| } |
| |
| private R getRowObjectByPosition(int rowPosition) { |
| this.selectionsLock.readLock().lock(); |
| |
| try { |
| 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 |
| } |
| } |
| } finally { |
| this.selectionsLock.readLock().unlock(); |
| } |
| |
| return null; |
| } |
| |
| private int getRowPositionById(Serializable rowId) { |
| this.selectionsLock.readLock().lock(); |
| |
| try { |
| R rowObject = this.selectedRows.get(rowId); |
| int rowIndex = this.rowDataProvider.indexOfRowObject(rowObject); |
| if (rowIndex == -1) { |
| return -1; |
| } |
| return this.selectionLayer.getRowPositionByIndex(rowIndex); |
| } finally { |
| this.selectionsLock.readLock().unlock(); |
| } |
| } |
| |
| @Override |
| public void handleLayerEvent(IStructuralChangeEvent event) { |
| // 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 |
| ArrayList<Serializable> keysToRemove = new ArrayList<>(); |
| for (Map.Entry<Serializable, R> entry : this.selectedRows.entrySet()) { |
| int rowIndex = this.rowDataProvider.indexOfRowObject(entry.getValue()); |
| if (rowIndex == -1) { |
| keysToRemove.add(entry.getKey()); |
| } |
| } |
| |
| this.selectionsLock.readLock().lock(); |
| try { |
| for (Serializable toRemove : keysToRemove) { |
| this.selectedRows.remove(toRemove); |
| } |
| } finally { |
| this.selectionsLock.readLock().unlock(); |
| } |
| |
| // fire row selection event |
| // since we are not able to identify the row position of the deleted |
| // selection we use all rows in the event to indicate the selection |
| // change for all deleted rows |
| if (!keysToRemove.isEmpty()) { |
| Collection<StructuralDiff> diffs = event.getRowDiffs(); |
| |
| int[] rowPositions = null; |
| if (diffs != null) { |
| rowPositions = diffs.stream() |
| .filter(diff -> diff.getDiffType() != null && diff.getDiffType().equals(DiffTypeEnum.DELETE)) |
| .map(StructuralDiff::getBeforePositionRange) |
| .flatMapToInt(range -> IntStream.range(range.start, range.end)) |
| .toArray(); |
| } else { |
| rowPositions = new int[0]; |
| } |
| |
| // if there is no diff in the event we assume everything has |
| // changed, in such a case we are not able to fire an |
| // appropriate event the layer stack upwards since it will be |
| // stopped while converting it to the target layer |
| // for the RowSelectionProvider this is sufficient because it |
| // registers itself as a listener to the SelectionLayer and |
| // therefore gets informed about the selection change |
| this.selectionLayer.fireLayerEvent( |
| new RowSelectionEvent(this.selectionLayer, rowPositions, -1, false, false)); |
| } |
| } |
| } |
| |
| @Override |
| public Class<IStructuralChangeEvent> getLayerEventClass() { |
| return IStructuralChangeEvent.class; |
| } |
| } |