blob: f02b386d363f7ae4e97e07b09414e1629238ab3a [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;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.nebula.widgets.nattable.coordinate.Range;
import org.eclipse.nebula.widgets.nattable.layer.AbstractLayerTransform;
import org.eclipse.nebula.widgets.nattable.layer.ILayer;
import org.eclipse.nebula.widgets.nattable.layer.IUniqueIndexLayer;
import org.eclipse.nebula.widgets.nattable.layer.event.PropertyUpdateEvent;
import org.eclipse.nebula.widgets.nattable.layer.event.RowDeleteEvent;
import org.eclipse.nebula.widgets.nattable.layer.event.RowInsertEvent;
import org.eclipse.swt.widgets.Display;
import ca.odell.glazedlists.EventList;
import ca.odell.glazedlists.event.ListEvent;
import ca.odell.glazedlists.event.ListEventListener;
/**
* This layer acts as the event listener for:
* <ol>
* <li>Glazed list events - {@link ListEvent}
* <li>Bean updates - PropertyChangeEvent(s)
* </ol>
*
* Compared to the GlazedListsEventLayer, this layer does not conflate events
* and only fire a single RowStructuralRefreshEvent for all events within 100ms.
* Instead it will fire a corresponding NatTable event with the detail
* information for every {@link ListEvent} fired by the GlazedLists immediately.
*
* @param <T>
* Type of the bean in the backing list.
*/
public class DetailGlazedListsEventLayer<T>
extends AbstractLayerTransform
implements IUniqueIndexLayer, ListEventListener<T>, PropertyChangeListener {
/**
* The underlying layer of type {@link IUniqueIndexLayer} This is necessary
* because {@link AbstractLayerTransform} only specifies {@link ILayer} as
* the type of the underlying layer. But as this event layer implements
* {@link IUniqueIndexLayer} the underlying layer needs to be of type
* {@link IUniqueIndexLayer} too so the necessary methods can delegate to
* it. Storing the underlying layer reference as {@link IUniqueIndexLayer}
* in here avoids casting operations at every access.
*/
private final IUniqueIndexLayer underlying;
/**
* The {@link EventList} whose events this layer is processing. Needed here
* so it is possible to exchange the list at runtime.
*/
private EventList<T> eventList;
/**
* Flag that indicates whether GlazedLists list change events are propagated
* to the NatTable or not.
*/
private boolean active = true;
/**
* Create a new {@link DetailGlazedListsEventLayer} which is in fact a
* {@link ListEventListener} that listens to GlazedLists events and
* translate them into events that are understandable by the NatTable.
*
* @param underlyingLayer
* The underlying layer of type {@link IUniqueIndexLayer}
* @param eventList
* The {@link EventList} this layer should be added as listener.
*/
public DetailGlazedListsEventLayer(IUniqueIndexLayer underlyingLayer, EventList<T> eventList) {
super(underlyingLayer);
this.underlying = underlyingLayer;
// add ourself as listener to the EventList
this.eventList = eventList;
this.eventList.addListEventListener(this);
}
// GlazedLists ListEventListener
/**
* GlazedLists event handling. Will transform received GlazedLists
* ListEvents into corresponding NatTable RowStructuralChangeEvents. Ensures
* that no other changes can be made to the GlazedLists instance until the
* events are processed in NatTable itself. This is necessary to avoid
* concurrent modifications which will lead to asynchronous states of
* NatTable and GlazedLists.
*/
@Override
public void listChanged(final ListEvent<T> event) {
if (this.active) {
try {
this.eventList.getReadWriteLock().readLock().lock();
int currentEventType = -1;
// as the delete events in GlazedLists are containing indexes
// that are related to prior deletes we need to ensure index
// consistency within NatTable,
// e.g. filtering so the complete list would be empty would
// result in getting events that all tell that index 0 is
// deleted
int deleteCount = 0;
final List<Range> deleteRanges = new ArrayList<>();
final List<Range> insertRanges = new ArrayList<>();
while (event.next()) {
int eventType = event.getType();
// first event, go ahead
if (currentEventType == -1) {
currentEventType = eventType;
} else if (currentEventType != eventType) {
// there is a new event type, fire the collected events
internalFireEvents(deleteRanges, insertRanges);
// and clear for clean further processing
deleteRanges.clear();
deleteCount = 0;
insertRanges.clear();
}
if (eventType == ListEvent.DELETE) {
int index = event.getIndex() + deleteCount;
deleteRanges.add(new Range(index, index + 1));
deleteCount++;
} else if (eventType == ListEvent.INSERT) {
insertRanges.add(new Range(event.getIndex(), event.getIndex() + 1));
}
}
internalFireEvents(deleteRanges, insertRanges);
} finally {
this.eventList.getReadWriteLock().readLock().unlock();
}
}
}
/**
* Create {@link RowDeleteEvent}s and {@link RowInsertEvent}s based on the
* given information and fire them synchronously to the UI thread to update
* and repaint the NatTable accordingly.
* <p>
* As there is a structural change, there need to be some processing for
* indexes and positions in layers above this one. Therefore we need to
* ensure that the processing is handled synchronous, otherwise we would get
* into an asynchronous state were we try to process events based on a
* ListEvent, while the list itself has already changed again. e.g.
* filtering: clear + apply
* </p>
*
* @param deleteRanges
* The ranges that were deleted and should be fired in an event.
* @param insertRanges
* The ranges that were inserted and should be fired in an event.
*/
private void internalFireEvents(final List<Range> deleteRanges, final List<Range> insertRanges) {
if (!deleteRanges.isEmpty()) {
Display.getDefault().syncExec(() -> fireLayerEvent(new RowDeleteEvent(getUnderlyingLayer(), deleteRanges)));
}
if (!insertRanges.isEmpty()) {
Display.getDefault().syncExec(() -> fireLayerEvent(new RowInsertEvent(getUnderlyingLayer(), insertRanges)));
}
}
// PropertyChangeListener
@SuppressWarnings("unchecked")
@Override
public void propertyChange(PropertyChangeEvent event) {
// We can cast since we know that the EventList is of type T
final PropertyUpdateEvent<T> updateEvent =
new PropertyUpdateEvent<>(
this,
(T) event.getSource(),
event.getPropertyName(),
event.getOldValue(),
event.getNewValue());
// The PropertyUpdateEvent will cause a repaint of the NatTable.
// We need to fire the event from the SWT Display thread, otherwise
// there will be an exception because painting can only be triggered
// from the SWT Display thread.
// As a property change doesn't indicate a structural change, the
// event can be fired asynchronously.
Display.getDefault().asyncExec(() -> fireLayerEvent(updateEvent));
}
/**
* Activates the handling of GlazedLists events. By activating on receiving
* GlazedLists change events, there will be NatTable events fired to
* indicate that re-rendering is necessary.
* <p>
* This is usually necessary to perform huge updates of the data model to
* avoid concurrency issues. By default the
* {@link DetailGlazedListsEventLayer} is activated. You can deactivate it
* prior performing bulk updates and activate it again after the update is
* finished for a better event handling.
* </p>
* <p>
* <b>Note:</b> When activating the list change handling again, there will
* be no event fired in NatTable automatically. For bulk updates with
* deactivated internal handling it is therefore necessary to fire a custom
* event to trigger the NatTable refresh operation.
* </p>
*
* @since 1.6
*/
public void activate() {
this.active = true;
}
/**
* Deactivates the handling of GlazedLists events. By deactivating there
* will be no NatTable events fired on GlazedLists change events.
* <p>
* This is usually necessary to perform huge updates of the data model to
* avoid concurrency issues. By default the
* {@link DetailGlazedListsEventLayer} is activated. You can deactivate it
* prior performing bulk updates and activate it again after the update is
* finished for a better event handling.
* </p>
* <p>
* <b>Note:</b> When activating the list change handling again, there will
* be no event fired in NatTable automatically. For bulk updates with
* deactivated internal handling it is therefore necessary to fire a custom
* event to trigger the NatTable refresh operation.
* </p>
*
* @since 1.6
*/
public void deactivate() {
this.active = false;
}
/**
* @return Whether this {@link DetailGlazedListsEventLayer} will propagate
* {@link ListEvent}s into NatTable or not.
*
* @since 1.6
*/
public boolean isActive() {
return this.active;
}
/**
* Change the underlying {@link EventList} this layer is listening to.
*
* @param newEventList
* the {@link EventList} to listen on.
*/
public void setEventList(EventList<T> newEventList) {
this.eventList.removeListEventListener(this);
this.eventList = newEventList;
this.eventList.addListEventListener(this);
}
@Override
public int getColumnPositionByIndex(int columnIndex) {
return this.underlying.getColumnPositionByIndex(columnIndex);
}
@Override
public int getRowPositionByIndex(int rowIndex) {
return this.underlying.getRowPositionByIndex(rowIndex);
}
}