blob: 474d2c5280d40a8a277b8cdc31de4d0af8e275df [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2013, 2020 Dirk Fauth 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:
* Dirk Fauth <dirk.fauth@googlemail.com> - initial API and implementation
*******************************************************************************/
package org.eclipse.nebula.widgets.nattable.extension.glazedlists.filterrow;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import org.eclipse.nebula.widgets.nattable.data.IColumnAccessor;
import org.eclipse.nebula.widgets.nattable.filterrow.combobox.FilterRowComboBoxDataProvider;
import org.eclipse.nebula.widgets.nattable.filterrow.combobox.FilterRowComboUpdateEvent;
import org.eclipse.nebula.widgets.nattable.layer.ILayer;
import org.eclipse.nebula.widgets.nattable.layer.event.CellVisualChangeEvent;
import org.eclipse.nebula.widgets.nattable.layer.event.ILayerEvent;
import org.eclipse.nebula.widgets.nattable.util.Scheduler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ca.odell.glazedlists.EventList;
import ca.odell.glazedlists.event.ListEvent;
import ca.odell.glazedlists.event.ListEventListener;
/**
* Special implementation of FilterRowComboBoxDataProvider that performs
* FilterRowComboUpdateEvents if the underlying list is changed.
* <p>
* This implementation is necessary for a special case. If a filter is applied
* and a new row is added to the data model, the FilterList won't show the new
* row because the current applied filter is not aware of the new values. This
* is because of the inverse filter logic in then Excel like filter row. As the
* FilterList doesn't show the new value, there is no ListEvent fired further,
* so the FilterRowComboBoxDataProvider is not informed about the structural
* change.
* <p>
* This implementation solves this issue by listening to the wrapped source
* EventList of the FilterList instead of the NatTable IStructuralChangeEvent.
*/
public class GlazedListsFilterRowComboBoxDataProvider<T> extends
FilterRowComboBoxDataProvider<T> implements ListEventListener<T> {
private static final Logger LOG = LoggerFactory.getLogger(GlazedListsFilterRowComboBoxDataProvider.class);
private static final Scheduler SCHEDULER = new Scheduler("GlazedListsFilterRowComboBoxDataProvider"); //$NON-NLS-1$
private AtomicBoolean changeHandlingProcessing = new AtomicBoolean(false);
private EventList<T> baseEventList;
/**
* @param bodyLayer
* A layer in the body region. Usually the DataLayer or a layer
* that is responsible for list event handling. Needed to
* register ourself as listener for data changes.
* @param baseCollection
* The base collection used to collect the unique values from.
* This need to be a collection that is not filtered, otherwise
* after modifications the content of the filter row combo boxes
* will only contain the current visible (not filtered) elements.
* @param columnAccessor
* The IColumnAccessor to be able to read the values out of the
* base collection objects.
*/
public GlazedListsFilterRowComboBoxDataProvider(
ILayer bodyLayer,
Collection<T> baseCollection,
IColumnAccessor<T> columnAccessor) {
this(bodyLayer, baseCollection, columnAccessor, true);
}
/**
* @param bodyLayer
* A layer in the body region. Usually the DataLayer or a layer
* that is responsible for list event handling. Needed to
* register ourself as listener for data changes.
* @param baseCollection
* The base collection used to collect the unique values from.
* This need to be a collection that is not filtered, otherwise
* after modifications the content of the filter row combo boxes
* will only contain the current visible (not filtered) elements.
* @param columnAccessor
* The IColumnAccessor to be able to read the values out of the
* base collection objects.
* @param lazy
* <code>true</code> to configure this
* {@link FilterRowComboBoxDataProvider} should load the combobox
* values lazily, <code>false</code> to pre-build the value
* cache.
* @since 1.4
*/
public GlazedListsFilterRowComboBoxDataProvider(
ILayer bodyLayer,
Collection<T> baseCollection,
IColumnAccessor<T> columnAccessor,
boolean lazy) {
super(bodyLayer, baseCollection, columnAccessor, lazy);
if (baseCollection instanceof EventList) {
this.baseEventList = ((EventList<T>) baseCollection);
this.baseEventList.addListEventListener(this);
} else {
LOG.error("baseCollection is not of type EventList. List changes can not be tracked."); //$NON-NLS-1$
}
}
@Override
public void listChanged(ListEvent<T> listChanges) {
if (!this.changeHandlingProcessing.getAndSet(true)) {
// a new row was added or a row was deleted
SCHEDULER.schedule(() -> {
List<FilterRowComboUpdateEvent> updateEvents = new ArrayList<>();
getValueCacheLock().writeLock().lock();
try {
// remember the cache before updating
Map<Integer, List<?>> cacheBefore = new HashMap<>(getValueCache());
// perform a refresh of the whole cache
getValueCache().clear();
if (!GlazedListsFilterRowComboBoxDataProvider.this.lazyLoading) {
buildValueCache();
} else {
// to determine the diff for the update event
// the current values need to be collected,
// otherwise on clear() - addAll() a full reset
// will be triggered since there are no cached
// values
for (Map.Entry<Integer, List<?>> entry : cacheBefore.entrySet()) {
getValueCache().put(entry.getKey(),
collectValues(entry.getKey()));
}
}
// fire events for every column that has cached data
for (Map.Entry<Integer, List<?>> entry : cacheBefore.entrySet()) {
updateEvents.add(buildUpdateEvent(
entry.getKey(),
entry.getValue(),
getValueCache().get(entry.getKey())));
}
GlazedListsFilterRowComboBoxDataProvider.this.changeHandlingProcessing.set(false);
} finally {
getValueCacheLock().writeLock().unlock();
}
if (isUpdateEventsEnabled()) {
for (FilterRowComboUpdateEvent event : updateEvents) {
fireCacheUpdateEvent(event);
}
}
}, 100);
}
}
@Override
public void handleLayerEvent(final ILayerEvent event) {
// we only need to perform event handling if caching is enabled
if (this.cachingEnabled && event instanceof CellVisualChangeEvent) {
SCHEDULER.schedule(() -> {
// usually this is fired for data updates so we need to update
// the value cache for the updated column
getValueCacheLock().writeLock().lock();
try {
int column = ((CellVisualChangeEvent) event).getColumnPosition();
List<?> cacheBefore = getValueCache().get(column);
// only update the cache in case a cache was build already
if (!GlazedListsFilterRowComboBoxDataProvider.this.lazyLoading
|| cacheBefore != null) {
getValueCache().put(column, collectValues(column));
}
if (isUpdateEventsEnabled()) {
// get the diff and fire the event
fireCacheUpdateEvent(buildUpdateEvent(column, cacheBefore, getValueCache().get(column)));
}
} finally {
getValueCacheLock().writeLock().unlock();
}
}, 0);
}
}
@Override
public void dispose() {
super.dispose();
SCHEDULER.shutdownNow();
}
}