| /******************************************************************************* |
| * Copyright (c) 2017, 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.List; |
| import java.util.ListIterator; |
| |
| import org.eclipse.nebula.widgets.nattable.command.ILayerCommandHandler; |
| import org.eclipse.nebula.widgets.nattable.datachange.command.DiscardDataChangesCommandHandler; |
| import org.eclipse.nebula.widgets.nattable.datachange.command.SaveDataChangesCommandHandler; |
| import org.eclipse.nebula.widgets.nattable.datachange.config.DefaultDataChangeConfiguration; |
| import org.eclipse.nebula.widgets.nattable.datachange.event.DiscardDataChangesCompletedEvent; |
| import org.eclipse.nebula.widgets.nattable.datachange.event.SaveDataChangesCompletedEvent; |
| import org.eclipse.nebula.widgets.nattable.edit.command.UpdateDataCommand; |
| import org.eclipse.nebula.widgets.nattable.layer.AbstractIndexLayerTransform; |
| import org.eclipse.nebula.widgets.nattable.layer.DataLayer; |
| import org.eclipse.nebula.widgets.nattable.layer.ILayer; |
| import org.eclipse.nebula.widgets.nattable.layer.IUniqueIndexLayer; |
| import org.eclipse.nebula.widgets.nattable.layer.LabelStack; |
| import org.eclipse.nebula.widgets.nattable.layer.event.ILayerEvent; |
| import org.eclipse.nebula.widgets.nattable.layer.event.ILayerEventHandler; |
| import org.eclipse.nebula.widgets.nattable.layer.event.IStructuralChangeEvent; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| /** |
| * {@link ILayer} that can be used to add a mechanism that highlights cells |
| * whose data has been changed. |
| * |
| * @since 1.6 |
| */ |
| public class DataChangeLayer extends AbstractIndexLayerTransform { |
| |
| private static final Logger LOG = LoggerFactory.getLogger(DataChangeLayer.class); |
| |
| /** |
| * Label that is applied to cells that are marked as modified/dirty in this |
| * layer. |
| */ |
| public static final String DIRTY = "DIRTY"; //$NON-NLS-1$ |
| |
| /** |
| * The {@link DataChangeHandler} registered with this |
| * {@link DataChangeLayer} to keep track of data changes. |
| */ |
| protected final List<DataChangeHandler> dataChangeHandler = new ArrayList<>(); |
| |
| /** |
| * The list of {@link DataChange}s that need to be handled on save or |
| * discard. |
| */ |
| protected final List<DataChange> dataChanges = new ArrayList<>(); |
| |
| /** |
| * Data provider that returns temporary stored data changes. |
| */ |
| private TemporaryDataProvider temporaryDataProvider; |
| |
| /** |
| * Create a new {@link DataChangeLayer} that does not track row structural |
| * changes and uses the default configuration. |
| * |
| * @param underlyingLayer |
| * The {@link ILayer} on top of which this |
| * {@link DataChangeLayer} should be created. Typically the |
| * {@link DataLayer}. |
| * @param keyHandler |
| * The {@link CellKeyHandler} that should be used to store |
| * dataChanges for a specific key. |
| * @param temporaryDataStorage |
| * <code>true</code> if the data changes should be handled |
| * temporary in this layer and update the model on save, |
| * <code>false</code> if the data changes should be directly |
| * applied to the underlying model and on save some additional |
| * save operations should be performed. |
| */ |
| public DataChangeLayer(IUniqueIndexLayer underlyingLayer, CellKeyHandler<?> keyHandler, boolean temporaryDataStorage) { |
| this(underlyingLayer, keyHandler, temporaryDataStorage, false, true); |
| } |
| |
| /** |
| * Create a new {@link DataChangeLayer} that uses the default configuration. |
| * |
| * @param underlyingLayer |
| * The {@link ILayer} on top of which this |
| * {@link DataChangeLayer} should be created. Typically the |
| * {@link DataLayer}. |
| * @param keyHandler |
| * The {@link CellKeyHandler} that should be used to store |
| * dataChanges for a specific key. |
| * @param temporaryDataStorage |
| * <code>true</code> if the data changes should be handled |
| * temporary in this layer and update the model on save, |
| * <code>false</code> if the data changes should be directly |
| * applied to the underlying model and on save some additional |
| * save operations should be performed. |
| * @param trackRowStructuralChanges |
| * <code>true</code> if structural changes like inserting or |
| * deleting a row should be tracked, <code>false</code> if such |
| * changes should not be tracked. |
| */ |
| public DataChangeLayer(IUniqueIndexLayer underlyingLayer, CellKeyHandler<?> keyHandler, boolean temporaryDataStorage, boolean trackRowStructuralChanges) { |
| this(underlyingLayer, keyHandler, temporaryDataStorage, trackRowStructuralChanges, true); |
| } |
| |
| /** |
| * Create a new {@link DataChangeLayer}. |
| * |
| * @param underlyingLayer |
| * The {@link ILayer} on top of which this |
| * {@link DataChangeLayer} should be created. Typically the |
| * {@link DataLayer}. |
| * @param keyHandler |
| * The {@link CellKeyHandler} that should be used to store |
| * dataChanges for a specific key. |
| * @param temporaryDataStorage |
| * <code>true</code> if the data changes should be handled |
| * temporary in this layer and update the model on save, |
| * <code>false</code> if the data changes should be directly |
| * applied to the underlying model and on save some additional |
| * save operations should be performed. |
| * @param trackRowStructuralChanges |
| * <code>true</code> if structural changes like inserting or |
| * deleting a row should be tracked, <code>false</code> if such |
| * changes should not be tracked. |
| * @param useDefaultConfiguration |
| * <code>true</code> if the default configuration should be |
| * applied, <code>false</code> if not. |
| */ |
| public DataChangeLayer(IUniqueIndexLayer underlyingLayer, CellKeyHandler<?> keyHandler, boolean temporaryDataStorage, boolean trackRowStructuralChanges, boolean useDefaultConfiguration) { |
| super(underlyingLayer); |
| registerCommandHandlers(); |
| |
| if (temporaryDataStorage && trackRowStructuralChanges) { |
| LOG.warn("tracking row structural changes is not supported in temporary data storage mode"); //$NON-NLS-1$ |
| } |
| |
| if (temporaryDataStorage) { |
| TemporaryUpdateDataChangeHandler handler = new TemporaryUpdateDataChangeHandler(this, keyHandler); |
| registerDataChangeHandler(handler); |
| this.temporaryDataProvider = handler; |
| } else { |
| PersistenceUpdateDataChangeHandler handler = new PersistenceUpdateDataChangeHandler(this, keyHandler); |
| registerDataChangeHandler(handler); |
| if (trackRowStructuralChanges) { |
| handler.setUpdateOnVerticalChanges(false); |
| registerDataChangeHandler(new RowInsertDataChangeHandler(this, keyHandler)); |
| registerDataChangeHandler(new RowDeleteDataChangeHandler(this, keyHandler)); |
| } |
| } |
| |
| if (useDefaultConfiguration) { |
| addConfiguration(new DefaultDataChangeConfiguration()); |
| } |
| } |
| |
| @Override |
| protected void registerCommandHandlers() { |
| registerCommandHandler(new DiscardDataChangesCommandHandler(this)); |
| registerCommandHandler(new SaveDataChangesCommandHandler(this)); |
| } |
| |
| @Override |
| public LabelStack getConfigLabelsByPosition(int columnPosition, int rowPosition) { |
| LabelStack labels = super.getConfigLabelsByPosition(columnPosition, rowPosition); |
| if (isCellDirty(columnPosition, rowPosition)) { |
| labels.addLabelOnTop(DIRTY); |
| } |
| return labels; |
| } |
| |
| @Override |
| public Object getDataValueByPosition(int columnPosition, int rowPosition) { |
| if (this.temporaryDataProvider != null && this.temporaryDataProvider.tracksDataChange(columnPosition, rowPosition)) { |
| return this.temporaryDataProvider.getDataValueByPosition(columnPosition, rowPosition); |
| } |
| return super.getDataValueByPosition(columnPosition, rowPosition); |
| } |
| |
| @Override |
| public void handleLayerEvent(ILayerEvent event) { |
| if (event instanceof IStructuralChangeEvent) { |
| IStructuralChangeEvent structuralChangeEvent = (IStructuralChangeEvent) event; |
| if (structuralChangeEvent.getColumnDiffs() == null |
| && structuralChangeEvent.getRowDiffs() == null |
| && structuralChangeEvent.isHorizontalStructureChanged() |
| && structuralChangeEvent.isVerticalStructureChanged()) { |
| // Assume everything changed |
| clearDataChanges(); |
| } else { |
| for (DataChangeHandler handler : this.dataChangeHandler) { |
| handler.handleStructuralChange(structuralChangeEvent); |
| } |
| } |
| } |
| super.handleLayerEvent(event); |
| } |
| |
| /** |
| * Registers the given {@link DataChangeHandler} to keep track of data |
| * changes. |
| * |
| * @param handler |
| * The {@link DataChangeHandler} to register. |
| */ |
| public final void registerDataChangeHandler(DataChangeHandler handler) { |
| this.dataChangeHandler.add(handler); |
| |
| if (handler instanceof ILayerCommandHandler) { |
| registerCommandHandler((ILayerCommandHandler<?>) handler); |
| } |
| |
| if (handler instanceof ILayerEventHandler) { |
| registerEventHandler((ILayerEventHandler<?>) handler); |
| } |
| } |
| |
| /** |
| * Unregisters the given {@link DataChangeHandler}. |
| * |
| * @param handler |
| * The {@link DataChangeHandler} to unregister. |
| */ |
| public final void unregisterDataChangeHandler(DataChangeHandler handler) { |
| this.dataChangeHandler.remove(handler); |
| |
| if (handler instanceof ILayerCommandHandler) { |
| unregisterCommandHandler(((ILayerCommandHandler<?>) handler).getCommandClass()); |
| } |
| |
| if (handler instanceof ILayerEventHandler) { |
| unregisterEventHandler((ILayerEventHandler<?>) handler); |
| } |
| } |
| |
| /** |
| * |
| * @return The {@link DataChangeHandler} registered with this |
| * {@link DataChangeLayer} to keep track of data changes. |
| */ |
| public final List<DataChangeHandler> getDataChangeHandler() { |
| return this.dataChangeHandler; |
| } |
| |
| /** |
| * Adds a {@link DataChange} to the list of locally tracked data changes |
| * that need to be handled on save or discard. |
| * |
| * @param change |
| * The {@link DataChange} to add. |
| */ |
| public void addDataChange(DataChange change) { |
| synchronized (this.dataChanges) { |
| this.dataChanges.add(change); |
| } |
| } |
| |
| /** |
| * |
| * @return The list of {@link DataChange}s that need to be handled on save |
| * or discard. |
| */ |
| public List<DataChange> getDataChanges() { |
| return this.dataChanges; |
| } |
| |
| /** |
| * Discards the tracked data changes. In case temporary data storage is |
| * disabled, the applied changes are undone by restoring the previous values |
| * via dedicated {@link UpdateDataCommand}s. |
| */ |
| public void discardDataChanges() { |
| // avoid handling of data change tracking that are caused by restoring |
| // the previous data states |
| for (DataChangeHandler handler : this.dataChangeHandler) { |
| handler.disableTracking(); |
| } |
| |
| synchronized (this.dataChanges) { |
| ListIterator<DataChange> listIter = this.dataChanges.listIterator(this.dataChanges.size()); |
| while (listIter.hasPrevious()) { |
| DataChange change = listIter.previous(); |
| change.discard(this); |
| } |
| } |
| |
| // enable tracking again |
| for (DataChangeHandler handler : this.dataChangeHandler) { |
| handler.enableTracking(); |
| } |
| |
| clearDataChanges(); |
| fireLayerEvent(new DiscardDataChangesCompletedEvent(this)); |
| } |
| |
| /** |
| * Saves the tracked data changes. In case temporary data storage is enabled |
| * this means the underlying data model is updated. Otherwise the stored |
| * data changes are simply cleared. |
| * <p> |
| * <b>Note:</b> In case temporary data storage is disabled and a custom save |
| * operation should be performed on save, a custom |
| * {@link SaveDataChangesCommandHandler} should be registered that first |
| * performs a custom action and afterwards calls this method to ensure a |
| * clear state in this layer. |
| * </p> |
| */ |
| public void saveDataChanges() { |
| // avoid handling of data change tracking that are caused by saving data |
| // states |
| for (DataChangeHandler handler : this.dataChangeHandler) { |
| handler.disableTracking(); |
| } |
| |
| synchronized (this.dataChanges) { |
| for (DataChange change : this.dataChanges) { |
| change.save(this); |
| } |
| } |
| |
| // enable tracking again |
| for (DataChangeHandler handler : this.dataChangeHandler) { |
| handler.enableTracking(); |
| } |
| |
| clearDataChanges(); |
| fireLayerEvent(new SaveDataChangesCompletedEvent(this)); |
| } |
| |
| /** |
| * Clear the locally stored changes. |
| */ |
| public void clearDataChanges() { |
| for (DataChangeHandler handler : this.dataChangeHandler) { |
| handler.clearDataChanges(); |
| } |
| synchronized (this.dataChanges) { |
| this.dataChanges.clear(); |
| } |
| } |
| |
| /** |
| * Checks if the column with the given position contains cells in a dirty |
| * state. |
| * |
| * @param columnPosition |
| * The position of the column whose dirty state should be |
| * checked. |
| * @return <code>true</code> if the column contains cells that are marked as |
| * dirty (data has changed and not saved yet), <code>false</code> if |
| * not. |
| */ |
| public boolean isColumnDirty(int columnPosition) { |
| for (DataChangeHandler handler : this.dataChangeHandler) { |
| if (handler.isColumnDirty(columnPosition)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Checks if the row with the given position contains cells in a dirty |
| * state. |
| * |
| * @param rowPosition |
| * The position of the row whose dirty state should be checked. |
| * @return <code>true</code> if the row contains cells that are marked as |
| * dirty (data has changed and not saved yet), <code>false</code> if |
| * not. |
| */ |
| public boolean isRowDirty(int rowPosition) { |
| for (DataChangeHandler handler : this.dataChangeHandler) { |
| if (handler.isRowDirty(rowPosition)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Checks if the cell at the given position is dirty. |
| * |
| * @param columnPosition |
| * The column position of the cell whose dirty state should be |
| * checked. |
| * @param rowPosition |
| * The row position of the cell whose dirty state should be |
| * checked. |
| * @return <code>true</code> if the cell is dirty (data has changed and not |
| * saved yet), <code>false</code> if not. |
| */ |
| public boolean isCellDirty(int columnPosition, int rowPosition) { |
| for (DataChangeHandler handler : this.dataChangeHandler) { |
| if (handler.isCellDirty(columnPosition, rowPosition)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| @Override |
| public Collection<String> getProvidedLabels() { |
| Collection<String> providedLabels = super.getProvidedLabels(); |
| providedLabels.add(DIRTY); |
| return providedLabels; |
| } |
| } |