| /******************************************************************************* |
| * Copyright (c) 2018, 2020 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: |
| * Dirk Fauth <dirk.fauth@googlemail.com> - initial API and implementation |
| ******************************************************************************/ |
| package org.eclipse.nebula.widgets.nattable.datachange; |
| |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| |
| import org.eclipse.nebula.widgets.nattable.coordinate.Range; |
| import org.eclipse.nebula.widgets.nattable.edit.event.DataUpdateEvent; |
| 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; |
| |
| /** |
| * Abstract implementation of {@link DataChangeHandler} to handle data updates. |
| * |
| * @since 1.6 |
| */ |
| public abstract class UpdateDataChangeHandler<T extends UpdateDataChange> extends AbstractDataChangeHandler<T> { |
| |
| /** |
| * The column indexes of columns that contain dirty cells. |
| */ |
| protected final Set<Integer> changedColumns = new HashSet<>(); |
| |
| /** |
| * The row indexes of rows that contain dirty cells. |
| */ |
| protected final Set<Integer> changedRows = new HashSet<>(); |
| |
| /** |
| * Flag to configure if the tracked changes in the {@link DataChangeLayer} |
| * should be updated on horizontal/column structural changes. |
| */ |
| private boolean updateOnHorizontalChanges = true; |
| |
| /** |
| * Flag to configure if the tracked changes in the {@link DataChangeLayer} |
| * should be updated on vertical/row structural changes. |
| */ |
| private boolean updateOnVerticalChanges = true; |
| |
| /** |
| * Creates an {@link PersistenceUpdateDataChangeHandler} to handle |
| * {@link DataUpdateEvent}s to be able to track and revert data changes. |
| * |
| * @param layer |
| * The {@link DataChangeLayer} this handler should be assigned |
| * to. |
| * @param keyHandler |
| * The {@link CellKeyHandler} that is used to store data changes |
| * for a specific key. |
| * @param dataChanges |
| * The map to track the data changes locally. |
| */ |
| public UpdateDataChangeHandler(DataChangeLayer layer, CellKeyHandler<?> keyHandler, Map<Object, T> dataChanges) { |
| super(layer, keyHandler, dataChanges); |
| } |
| |
| @Override |
| public void handleStructuralChange(IStructuralChangeEvent structuralChangeEvent) { |
| if (structuralChangeEvent.isHorizontalStructureChanged() |
| && structuralChangeEvent.getColumnDiffs() != null) { |
| |
| if (this.keyHandler.updateOnHorizontalStructuralChange()) { |
| Collection<StructuralDiff> structuralDiffs = structuralChangeEvent.getColumnDiffs(); |
| handleColumnDelete(structuralDiffs); |
| handleColumnInsert(structuralDiffs); |
| } else { |
| removeChangesForDeletedColumnObjects(); |
| } |
| } else if (structuralChangeEvent.isVerticalStructureChanged() |
| && structuralChangeEvent.getRowDiffs() != null) { |
| |
| if (this.keyHandler.updateOnVerticalStructuralChange()) { |
| Collection<StructuralDiff> structuralDiffs = structuralChangeEvent.getRowDiffs(); |
| handleRowDelete(structuralDiffs); |
| handleRowInsert(structuralDiffs); |
| } else { |
| removeChangesForDeletedRowObjects(); |
| } |
| } |
| rebuildPositionCollections(); |
| } |
| |
| /** |
| * Will check for events that indicate that rows have been deleted. In that |
| * case the cached dataChanges need to be updated because the index of the |
| * rows might have changed. E.g. cell with row at index 3 is changed in the |
| * given layer, deleting row at index 1 will cause the row at index 3 to be |
| * moved to index 2. Without transforming the index regarding the delete |
| * event, the wrong cell at the incorrect row would be shown as changed. |
| * |
| * @param rowDiffs |
| * The collection of {@link StructuralDiff}s to handle. |
| */ |
| @SuppressWarnings("unchecked") |
| private void handleRowDelete(Collection<StructuralDiff> rowDiffs) { |
| // for correct calculation the diffs need to be processed from lowest |
| // position to highest |
| List<StructuralDiff> diffs = new ArrayList<>(rowDiffs); |
| Collections.sort(diffs, (o1, o2) -> o1.getBeforePositionRange().start - o2.getBeforePositionRange().start); |
| |
| List<Integer> toRemove = new ArrayList<>(); |
| for (StructuralDiff rowDiff : diffs) { |
| if (rowDiff.getDiffType() != null |
| && rowDiff.getDiffType().equals(DiffTypeEnum.DELETE)) { |
| Range beforePositionRange = rowDiff.getBeforePositionRange(); |
| for (int i = beforePositionRange.start; i < beforePositionRange.end; i++) { |
| int index = i; |
| if (index >= 0) { |
| toRemove.add(index); |
| } |
| } |
| } |
| } |
| |
| if (!toRemove.isEmpty()) { |
| // modify row indexes regarding the deleted rows |
| Map<Object, T> modifiedRows = new HashMap<>(); |
| for (Map.Entry<Object, T> entry : this.dataChanges.entrySet()) { |
| int rowIndex = this.keyHandler.getRowIndex(entry.getKey()); |
| if (!toRemove.contains(rowIndex)) { |
| // check number of removed indexes that are lower than the |
| // current one |
| int deletedBefore = 0; |
| for (Integer removed : toRemove) { |
| if (removed < rowIndex) { |
| deletedBefore++; |
| } |
| } |
| int modRow = rowIndex - deletedBefore; |
| if (modRow >= 0) { |
| Object oldKey = entry.getKey(); |
| Object updatedKey = this.keyHandler.getKeyWithRowUpdate(oldKey, modRow); |
| entry.getValue().updateKey(updatedKey); |
| modifiedRows.put(updatedKey, entry.getValue()); |
| |
| if (this.updateOnVerticalChanges) { |
| // update the changes in the DataChangeLayer too |
| synchronized (this.layer.dataChanges) { |
| for (Iterator<DataChange> it = this.layer.dataChanges.iterator(); it.hasNext();) { |
| DataChange change = it.next(); |
| if (change.getClass().equals(entry.getValue().getClass()) |
| && change.getKey().equals(oldKey)) { |
| Object uk = this.keyHandler.getKeyWithRowUpdate(oldKey, modRow); |
| change.updateKey(uk); |
| } |
| } |
| } |
| } |
| } |
| } else if (this.updateOnVerticalChanges) { |
| synchronized (this.layer.dataChanges) { |
| for (Iterator<DataChange> it = this.layer.dataChanges.iterator(); it.hasNext();) { |
| DataChange change = it.next(); |
| if (change.getClass().equals(entry.getValue().getClass()) |
| && this.keyHandler.getRowIndex(change.getKey()) == this.keyHandler.getRowIndex(entry.getValue().getKey())) { |
| it.remove(); |
| } |
| } |
| } |
| } |
| } |
| |
| this.dataChanges.clear(); |
| this.dataChanges.putAll(modifiedRows); |
| } |
| } |
| |
| /** |
| * Will check for events that indicate that rows are added. In that case the |
| * cached dataChanges need to be updated because the index of the rows might |
| * have changed. E.g. Row with index 3 is hidden in the given layer, adding |
| * a row at index 1 will cause the row at index 3 to be moved to index 4. |
| * Without transforming the index regarding the add event, the wrong row |
| * would be hidden. |
| * |
| * @param rowDiffs |
| * The collection of {@link StructuralDiff}s to handle. |
| */ |
| @SuppressWarnings("unchecked") |
| private void handleRowInsert(Collection<StructuralDiff> rowDiffs) { |
| // for correct calculation the diffs need to be processed from highest |
| // position to lowest |
| List<StructuralDiff> diffs = new ArrayList<>(rowDiffs); |
| Collections.sort(diffs, (o1, o2) -> o2.getBeforePositionRange().start - o1.getBeforePositionRange().start); |
| |
| for (StructuralDiff rowDiff : diffs) { |
| if (rowDiff.getDiffType() != null |
| && rowDiff.getDiffType().equals(DiffTypeEnum.ADD)) { |
| Range beforePositionRange = rowDiff.getBeforePositionRange(); |
| // modify row indexes regarding the inserted rows |
| Map<Object, T> modifiedRows = new HashMap<>(); |
| for (Map.Entry<Object, T> entry : this.dataChanges.entrySet()) { |
| int rowIndex = this.keyHandler.getRowIndex(entry.getKey()); |
| |
| Object oldKey = entry.getKey(); |
| int modRow = -1; |
| if (rowIndex >= beforePositionRange.start) { |
| modRow = rowIndex + 1; |
| } else { |
| modRow = rowIndex; |
| } |
| |
| Object updatedKey = this.keyHandler.getKeyWithRowUpdate(entry.getKey(), modRow); |
| entry.getValue().updateKey(updatedKey); |
| modifiedRows.put(updatedKey, entry.getValue()); |
| |
| if (this.updateOnVerticalChanges) { |
| // update the changes in the DataChangeLayer too |
| synchronized (this.layer.dataChanges) { |
| for (Iterator<DataChange> it = this.layer.dataChanges.iterator(); it.hasNext();) { |
| DataChange change = it.next(); |
| if (change.getClass().equals(entry.getValue().getClass()) |
| && change.getKey().equals(oldKey)) { |
| Object uk = this.keyHandler.getKeyWithRowUpdate(oldKey, modRow); |
| change.updateKey(uk); |
| } |
| } |
| } |
| } |
| } |
| |
| this.dataChanges.clear(); |
| this.dataChanges.putAll(modifiedRows); |
| } |
| } |
| } |
| |
| /** |
| * Will check for events that indicate that columns have been deleted. In |
| * that case the cached dataChanges need to be updated because the index of |
| * the columns might have changed. E.g. cell with column at index 3 is |
| * changed in the given layer, deleting column at index 1 will cause the |
| * column at index 3 to be moved to index 2. Without transforming the index |
| * regarding the delete event, the wrong cell at the incorrect column would |
| * be shown as changed. |
| * |
| * @param columnDiffs |
| * The collection of {@link StructuralDiff}s to handle. |
| */ |
| @SuppressWarnings("unchecked") |
| public void handleColumnDelete(Collection<StructuralDiff> columnDiffs) { |
| // for correct calculation the diffs need to be processed from lowest |
| // position to highest |
| List<StructuralDiff> diffs = new ArrayList<>(columnDiffs); |
| Collections.sort(diffs, (o1, o2) -> o1.getBeforePositionRange().start - o2.getBeforePositionRange().start); |
| |
| List<Integer> toRemove = new ArrayList<>(); |
| for (StructuralDiff columnDiff : diffs) { |
| if (columnDiff.getDiffType() != null |
| && columnDiff.getDiffType().equals(DiffTypeEnum.DELETE)) { |
| Range beforePositionRange = columnDiff.getBeforePositionRange(); |
| for (int i = beforePositionRange.start; i < beforePositionRange.end; i++) { |
| int index = i; |
| if (index >= 0) { |
| toRemove.add(index); |
| } |
| } |
| } |
| } |
| |
| // only perform modifications if items where deleted |
| if (!toRemove.isEmpty()) { |
| // modify column indexes regarding the deleted column |
| Map<Object, T> modifiedColumns = new HashMap<>(); |
| for (Map.Entry<Object, T> entry : this.dataChanges.entrySet()) { |
| int columnIndex = this.keyHandler.getColumnIndex(entry.getKey()); |
| if (!toRemove.contains(columnIndex)) { |
| // check number of removed indexes that are lower than the |
| // current one |
| int deletedBefore = 0; |
| for (Integer removed : toRemove) { |
| if (removed < columnIndex) { |
| deletedBefore++; |
| } |
| } |
| int modColumn = columnIndex - deletedBefore; |
| if (modColumn >= 0) { |
| Object oldKey = entry.getKey(); |
| Object updatedKey = this.keyHandler.getKeyWithColumnUpdate(oldKey, modColumn); |
| entry.getValue().updateKey(updatedKey); |
| modifiedColumns.put(updatedKey, entry.getValue()); |
| |
| if (this.updateOnVerticalChanges) { |
| // update the changes in the DataChangeLayer too |
| synchronized (this.layer.dataChanges) { |
| for (Iterator<DataChange> it = this.layer.dataChanges.iterator(); it.hasNext();) { |
| DataChange change = it.next(); |
| if (change.getClass().equals(entry.getValue().getClass()) |
| && change.getKey().equals(oldKey)) { |
| Object uk = this.keyHandler.getKeyWithColumnUpdate(oldKey, modColumn); |
| change.updateKey(uk); |
| } |
| } |
| } |
| } |
| } |
| } else if (this.updateOnHorizontalChanges) { |
| for (Iterator<DataChange> it = this.layer.dataChanges.iterator(); it.hasNext();) { |
| DataChange change = it.next(); |
| if (change.getClass().equals(entry.getValue().getClass()) |
| && this.keyHandler.getColumnIndex(change.getKey()) == this.keyHandler.getColumnIndex(entry.getValue().getKey())) { |
| it.remove(); |
| } |
| } |
| } |
| } |
| |
| this.dataChanges.clear(); |
| this.dataChanges.putAll(modifiedColumns); |
| } |
| } |
| |
| /** |
| * Will check for events that indicate that columns are added. In that case |
| * the cached dataChanges need to be updated because the index of the |
| * columns might have changed. E.g. column with index 3 is hidden in the |
| * given layer, adding a column at index 1 will cause the column at index 3 |
| * to be moved to index 4. Without transforming the index regarding the add |
| * event, the wrong column would be hidden. |
| * |
| * @param columnDiffs |
| * The collection of {@link StructuralDiff}s to handle. |
| */ |
| @SuppressWarnings("unchecked") |
| public void handleColumnInsert(Collection<StructuralDiff> columnDiffs) { |
| // for correct calculation the diffs need to be processed from highest |
| // position to lowest |
| List<StructuralDiff> diffs = new ArrayList<>(columnDiffs); |
| Collections.sort(diffs, (o1, o2) -> o2.getBeforePositionRange().start - o1.getBeforePositionRange().start); |
| |
| for (StructuralDiff columnDiff : diffs) { |
| if (columnDiff.getDiffType() != null |
| && columnDiff.getDiffType().equals(DiffTypeEnum.ADD)) { |
| Range beforePositionRange = columnDiff.getBeforePositionRange(); |
| // modify column indexes regarding the inserted columns |
| Map<Object, T> modifiedColumns = new HashMap<>(); |
| for (Map.Entry<Object, T> entry : this.dataChanges.entrySet()) { |
| int columnIndex = this.keyHandler.getColumnIndex(entry.getKey()); |
| |
| Object oldKey = entry.getKey(); |
| int modColumn = -1; |
| if (columnIndex >= beforePositionRange.start) { |
| modColumn = columnIndex + 1; |
| } else { |
| modColumn = columnIndex; |
| } |
| |
| Object updatedKey = this.keyHandler.getKeyWithColumnUpdate(entry.getKey(), modColumn); |
| entry.getValue().updateKey(updatedKey); |
| modifiedColumns.put(updatedKey, entry.getValue()); |
| |
| if (this.updateOnHorizontalChanges) { |
| // update the changes in the DataChangeLayer too |
| synchronized (this.layer.dataChanges) { |
| for (Iterator<DataChange> it = this.layer.dataChanges.iterator(); it.hasNext();) { |
| DataChange change = it.next(); |
| if (change.getClass().equals(entry.getValue().getClass()) |
| && change.getKey().equals(oldKey)) { |
| Object uk = this.keyHandler.getKeyWithColumnUpdate(oldKey, modColumn); |
| change.updateKey(uk); |
| } |
| } |
| } |
| } |
| } |
| |
| this.dataChanges.clear(); |
| this.dataChanges.putAll(modifiedColumns); |
| } |
| } |
| } |
| |
| /** |
| * Clear the locally stored changes. |
| */ |
| @Override |
| public void clearDataChanges() { |
| super.clearDataChanges(); |
| this.changedColumns.clear(); |
| this.changedRows.clear(); |
| } |
| |
| /** |
| * Iterates over the locally stored data changes and checks if the |
| * referenced object does still exist. If not the data change is removed. |
| * <p> |
| * This method is intended to be used with {@link CellKeyHandler} |
| * implementations whose created keys do not need to be updated on |
| * structural changes as they update automatically, e.g. via unique |
| * identifier. |
| * </p> |
| */ |
| @SuppressWarnings("unchecked") |
| protected void removeChangesForDeletedColumnObjects() { |
| // we need to ensure that changes for deleted rows are |
| // removed from the data changes collection |
| for (Iterator<Object> it = this.dataChanges.keySet().iterator(); it.hasNext();) { |
| Object key = it.next(); |
| int columnIndex = this.keyHandler.getColumnIndex(key); |
| if (columnIndex < 0) { |
| if (this.updateOnHorizontalChanges) { |
| T localChange = this.dataChanges.get(key); |
| for (Iterator<DataChange> iterator = this.layer.dataChanges.iterator(); iterator.hasNext();) { |
| DataChange change = iterator.next(); |
| if (change.getClass().equals(localChange.getClass()) |
| && this.keyHandler.getColumnIndex(change.getKey()) == this.keyHandler.getColumnIndex(localChange.getKey())) { |
| iterator.remove(); |
| } |
| } |
| } |
| it.remove(); |
| } |
| } |
| } |
| |
| @SuppressWarnings("unchecked") |
| protected void removeChangesForDeletedRowObjects() { |
| // we need to ensure that changes for deleted rows are |
| // removed from the data changes collection |
| for (Iterator<Object> it = this.dataChanges.keySet().iterator(); it.hasNext();) { |
| Object key = it.next(); |
| int rowIndex = this.keyHandler.getRowIndex(key); |
| if (rowIndex < 0) { |
| if (this.updateOnVerticalChanges) { |
| T localChange = this.dataChanges.get(key); |
| for (Iterator<DataChange> iterator = this.layer.dataChanges.iterator(); iterator.hasNext();) { |
| DataChange change = iterator.next(); |
| if (change.getClass().equals(localChange.getClass()) |
| && this.keyHandler.getRowIndex(change.getKey()) == this.keyHandler.getRowIndex(localChange.getKey())) { |
| iterator.remove(); |
| } |
| } |
| |
| } |
| it.remove(); |
| } |
| } |
| } |
| |
| /** |
| * Rebuilds the {@link #changedColumns} and {@link #changedRows} collections |
| * based on the updated {@link #dataChanges} map. |
| */ |
| @SuppressWarnings("unchecked") |
| protected void rebuildPositionCollections() { |
| this.changedColumns.clear(); |
| this.changedRows.clear(); |
| for (Iterator<Object> it = this.dataChanges.keySet().iterator(); it.hasNext();) { |
| Object key = it.next(); |
| int columnIndex = this.keyHandler.getColumnIndex(key); |
| int rowIndex = this.keyHandler.getRowIndex(key); |
| if (columnIndex >= 0 && rowIndex >= 0) { |
| this.changedColumns.add(columnIndex); |
| this.changedRows.add(rowIndex); |
| } |
| } |
| } |
| |
| @Override |
| public boolean isColumnDirty(int columnPosition) { |
| return this.changedColumns.contains(columnPosition); |
| } |
| |
| @Override |
| public boolean isRowDirty(int rowPosition) { |
| return this.changedRows.contains(rowPosition); |
| } |
| |
| @Override |
| public boolean isCellDirty(int columnPosition, int rowPosition) { |
| Object key = this.keyHandler.getKey(columnPosition, rowPosition); |
| if (key != null) { |
| return this.dataChanges.containsKey(key); |
| } |
| return false; |
| } |
| |
| /** |
| * Configure if the changes tracked by the {@link DataChangeLayer} should |
| * also be updated on a horizontal/column structural changes. The update is |
| * needed in case the change does not cause a {@link DataChange} that is |
| * created by some other handler and performed in the correct order. |
| * |
| * @param update |
| * <code>true</code> if the changes tracked by the |
| * {@link DataChangeLayer} should be updated, <code>false</code> |
| * if not. |
| */ |
| public void setUpdateOnHorizontalChanges(boolean update) { |
| this.updateOnHorizontalChanges = update; |
| } |
| |
| /** |
| * Configure if the changes tracked by the {@link DataChangeLayer} should |
| * also be updated on a vertical/row structural changes. The update is |
| * needed in case the change does not cause a {@link DataChange} that is |
| * created by some other handler and performed in the correct order. |
| * |
| * @param update |
| * <code>true</code> if the changes tracked by the |
| * {@link DataChangeLayer} should be updated, <code>false</code> |
| * if not. |
| */ |
| public void setUpdateOnVerticalChanges(boolean update) { |
| this.updateOnVerticalChanges = update; |
| } |
| } |