blob: 3e997dd922fde3c947eccf9b7a9ed623a52b6890 [file] [log] [blame]
/*******************************************************************************
* 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;
}
}