| /******************************************************************************* |
| * 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); |
| } |
| } |