blob: 7d186416fb81dc15d8705a13a78ce05fcd8b9c08 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2012, 2020 Original authors 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:
* Original authors and others - initial API and implementation
******************************************************************************/
package org.eclipse.nebula.widgets.nattable.extension.glazedlists;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.concurrent.ScheduledFuture;
import org.eclipse.nebula.widgets.nattable.command.DisposeResourcesCommand;
import org.eclipse.nebula.widgets.nattable.command.ILayerCommand;
import org.eclipse.nebula.widgets.nattable.layer.AbstractLayerTransform;
import org.eclipse.nebula.widgets.nattable.layer.IUniqueIndexLayer;
import org.eclipse.nebula.widgets.nattable.layer.event.ILayerEvent;
import org.eclipse.nebula.widgets.nattable.layer.event.PropertyUpdateEvent;
import org.eclipse.nebula.widgets.nattable.layer.event.RowStructuralRefreshEvent;
import org.eclipse.nebula.widgets.nattable.layer.event.VisualRefreshEvent;
import org.eclipse.nebula.widgets.nattable.util.Scheduler;
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>GlazedLists events - {@link ListEvent}
* <li>Bean updates - PropertyChangeEvent(s)
* </ol>
* GlazedLists events are conflated at a 100ms interval i.e a single
* {@link RowStructuralRefreshEvent} is fired for any number of GlazedLists
* events received during that interval.
* <p>
* PropertyChangeEvent(s) are propagated immediately as a
* {@link PropertyUpdateEvent}.
*
* @param <T>
* Type of the bean in the backing list.
*/
public class GlazedListsEventLayer<T>
extends AbstractLayerTransform
implements IUniqueIndexLayer, ListEventListener<T>, PropertyChangeListener {
private static final Scheduler scheduler = new Scheduler("GlazedListsEventLayer"); //$NON-NLS-1$
private final IUniqueIndexLayer underlying;
private final ScheduledFuture<?> future;
private EventList<T> eventList;
private boolean testMode = false;
private boolean structuralChangeEventsToProcess = false;
private boolean eventsToProcess = false;
private boolean terminated;
private boolean active = true;
public GlazedListsEventLayer(IUniqueIndexLayer underlyingLayer, EventList<T> eventList) {
super(underlyingLayer);
this.underlying = underlyingLayer;
this.eventList = eventList;
this.eventList.addListEventListener(this);
// Start the event conflation thread
this.future = scheduler.scheduleAtFixedRate(getEventNotifier(), 0L, 100L);
}
/**
*
* @return The {@link Runnable} that is triggered all 100ms to fire a
* NatTable refresh event.
*/
protected Runnable getEventNotifier() {
return new Runnable() {
@Override
public void run() {
if (GlazedListsEventLayer.this.eventsToProcess && GlazedListsEventLayer.this.active) {
ILayerEvent layerEvent;
if (GlazedListsEventLayer.this.structuralChangeEventsToProcess) {
layerEvent = new RowStructuralRefreshEvent(getUnderlyingLayer());
} else {
layerEvent = new VisualRefreshEvent(getUnderlyingLayer());
}
fireEventFromSWTDisplayThread(layerEvent);
GlazedListsEventLayer.this.eventsToProcess = false;
GlazedListsEventLayer.this.structuralChangeEventsToProcess = false;
}
}
};
}
// GlazedLists ListEventListener
@Override
public void listChanged(ListEvent<T> event) {
while (event.next()) {
int eventType = event.getType();
if (eventType == ListEvent.DELETE || eventType == ListEvent.INSERT) {
this.structuralChangeEventsToProcess = true;
}
}
this.eventsToProcess = true;
}
// PropertyChangeListener
@Override
@SuppressWarnings("unchecked")
public void propertyChange(PropertyChangeEvent event) {
// We can cast since we know that the EventList is of type T
PropertyUpdateEvent<T> updateEvent = new PropertyUpdateEvent<T>(
this,
(T) event.getSource(),
event.getPropertyName(),
event.getOldValue(),
event.getNewValue());
fireEventFromSWTDisplayThread(updateEvent);
}
/**
* Fires the given {@link ILayerEvent} on the SWT Display thread in case
* {@link #testMode} is <code>false</code>. Needed because the GlazedLists
* list change handling is done in a background thread, but NatTable event
* handling needs to be triggered in the UI thread to be able to trigger
* repainting.
*
* @param event
* The event to fire
*/
protected void fireEventFromSWTDisplayThread(final ILayerEvent event) {
if (!this.testMode && Display.getCurrent() == null) {
Display.getDefault().asyncExec(new Runnable() {
@Override
public void run() {
fireLayerEvent(event);
}
});
} else {
fireLayerEvent(event);
}
}
@Override
public boolean doCommand(ILayerCommand command) {
if (!this.terminated && command instanceof DisposeResourcesCommand) {
this.terminated = true;
scheduler.unschedule(this.future);
}
return super.doCommand(command);
}
/**
*
* @return <code>true</code> if this layer was terminated,
* <code>false</code> if it is still active.
*/
public boolean isDisposed() {
return this.terminated;
}
/**
* @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);
}
/**
* Activate the test mode, which is needed for unit testing. When enabling
* the test mode, the events are not fired in the UI thread.
*
* @param testMode
* <code>true</code> to enable the test mode, <code>false</code>
* for real mode.
*/
public void setTestMode(boolean testMode) {
this.testMode = testMode;
}
/**
* 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 GlazedListsEventLayer} is
* activated. You can deactivate it prior performing bulk updates and
* activate it again after the update is finished for a better event
* handling.
*/
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 GlazedListsEventLayer} is
* activated. You can deactivate it prior performing bulk updates and
* activate it again after the update is finished for a better event
* handling.
*/
public void deactivate() {
this.active = false;
}
/**
* @return Whether this {@link GlazedListsEventLayer} will propagate
* {@link ListEvent}s into NatTable or not.
*/
public boolean isActive() {
return this.active;
}
/**
* This method can be used to discard event processing.
* <p>
* It is useful in cases scenarios where list changes are tracked while the
* handling is deactivated. By default list changes are also tracked while
* the handling is deactivated, so automatically a refresh is triggered on
* activation. For cases where a custom event is fired for updates, it could
* make sense to discard the events to process to avoid that a full refresh
* event is triggered.
* </p>
*
* @since 1.6
*/
public void discardEventsToProcess() {
this.eventsToProcess = false;
this.structuralChangeEventsToProcess = false;
}
// Columns
@Override
public int getColumnPositionByIndex(int columnIndex) {
return this.underlying.getColumnPositionByIndex(columnIndex);
}
// Rows
@Override
public int getRowPositionByIndex(int rowIndex) {
return this.underlying.getRowPositionByIndex(rowIndex);
}
}