/******************************************************************************
* Copyright (c) 2016 itemis AG and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Alexander Nyßen (itemis AG) - initial API and implementation
*
*******************************************************************************/
package org.eclipse.gef.common.collections;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import javafx.beans.InvalidationListener;
import javafx.collections.ObservableSet;
import javafx.collections.SetChangeListener;
import javafx.collections.SetChangeListener.Change;
/**
* A utility class to support change notifications for an {@link ObservableSet}
* , replacing the JavaFX-internal {@code SetChangeListener} helper class.
*
* @author anyssen
*
* @param <E>
* The element type of the {@link ObservableSet}.
*/
public class SetListenerHelperEx<E> {
/**
* A simple implementation of an
* {@link javafx.collections.SetChangeListener.Change}.
*
* @author anyssen
*
* @param <E>
* The element type of the source {@link ObservableSet}.
*
*/
public static class AtomicChange<E> extends SetChangeListener.Change<E> {
private E removedElement;
private E addedElement;
/**
* Creates a new {@link SetListenerHelperEx.AtomicChange} that
* represents a change comprising a single elementary sub-change.
*
* @param source
* The source {@link ObservableSet} from which the change
* originated.
* @param removedElement
* The element that was removed by this change or
* <code>null</code> if no value was removed.
* @param addedElement
* The element that was added by this change or
* <code>null</code> if no value was added.
*/
public AtomicChange(ObservableSet<E> source, E removedElement,
E addedElement) {
super(source);
this.removedElement = removedElement;
this.addedElement = addedElement;
}
/**
* Creates a new {@link SetListenerHelperEx.AtomicChange} for the passed
* in source, based on the data provided in the passed-in change.
* <p>
* This is basically used to allow properties wrapping an
* {@link ObservableSet} to re-fire change events of their wrapped
* {@link ObservableSet} with themselves as source.
*
* @param source
* The new source {@link ObservableSet}.
* @param change
* The change to infer a new change from. It is expected that
* the change is in initial state. In either case it will be
* reset to initial state.
*/
public AtomicChange(ObservableSet<E> source,
SetChangeListener.Change<? extends E> change) {
super(source);
this.addedElement = change.getElementAdded();
this.removedElement = change.getElementRemoved();
}
@Override
public E getElementAdded() {
return addedElement;
}
@Override
public E getElementRemoved() {
return removedElement;
}
@Override
public String toString() {
if (wasAdded()) {
return "Added " + addedElement + ".";
} else {
return "Removed " + removedElement + ".";
}
}
@Override
public boolean wasAdded() {
return addedElement != null;
}
@Override
public boolean wasRemoved() {
return removedElement != null;
}
}
private List<InvalidationListener> invalidationListeners = null;
private boolean lockInvalidationListeners;
private boolean lockSetChangeListeners;
private List<SetChangeListener<? super E>> setChangeListeners = null;
private ObservableSet<E> source;
/**
* Constructs a new {@link SetListenerHelperEx} for the given source
* {@link ObservableSet}.
*
* @param source
* The {@link ObservableSet} to use as source in change
* notifications.
*/
public SetListenerHelperEx(ObservableSet<E> source) {
this.source = source;
}
/**
* Adds a new {@link InvalidationListener} to this
* {@link SetListenerHelperEx}. If the same listener is added more than
* once, it will be registered more than once and will receive multiple
* change events.
*
* @param listener
* The listener to add.
*/
public void addListener(InvalidationListener listener) {
if (invalidationListeners == null) {
invalidationListeners = new ArrayList<>();
}
// XXX: Prevent ConcurrentModificationExceptions (in case listeners are
// added during notifications); as we only create a new multi-set in the
// locked case, memory should not be waisted.
if (lockInvalidationListeners) {
invalidationListeners = new ArrayList<>(invalidationListeners);
}
invalidationListeners.add(listener);
}
/**
* Adds a new {@link SetChangeListener} to this {@link SetListenerHelperEx}.
* If the same listener is added more than once, it will be registered more
* than once and will receive multiple change events.
*
* @param listener
* The listener to add.
*/
public void addListener(SetChangeListener<? super E> listener) {
if (setChangeListeners == null) {
setChangeListeners = new ArrayList<>();
}
// XXX: Prevent ConcurrentModificationExceptions (in case listeners are
// added during notifications); as we only create a new multi-set in the
// locked case, memory should not be waisted.
if (lockSetChangeListeners) {
setChangeListeners = new ArrayList<>(setChangeListeners);
}
setChangeListeners.add(listener);
}
/**
* Notifies all attached {@link InvalidationListener}s and
* {@link SetChangeListener}s about the change.
*
* @param change
* The change to notify listeners about.
*/
public void fireValueChangedEvent(
SetChangeListener.Change<? extends E> change) {
notifyInvalidationListeners();
if (change != null) {
notifySetChangeListeners(change);
}
}
/**
* Returns the source {@link ObservableSet} this {@link SetListenerHelperEx}
* is bound to, which is used in change notifications.
*
* @return The source {@link ObservableSet}.
*/
protected ObservableSet<E> getSource() {
return source;
}
/**
* Notifies all registered {@link InvalidationListener}s.
*/
protected void notifyInvalidationListeners() {
if (invalidationListeners != null) {
try {
lockInvalidationListeners = true;
for (InvalidationListener l : invalidationListeners) {
try {
l.invalidated(source);
} catch (Exception e) {
Thread.currentThread().getUncaughtExceptionHandler()
.uncaughtException(Thread.currentThread(), e);
}
}
} finally {
lockInvalidationListeners = false;
}
}
}
/**
* Notifies the attached {@link SetChangeListener}s about the related
* change.
*
* @param change
* The applied change.
*/
protected void notifySetChangeListeners(Change<? extends E> change) {
if (setChangeListeners != null) {
try {
lockSetChangeListeners = true;
for (SetChangeListener<? super E> l : setChangeListeners) {
try {
l.onChanged(change);
} catch (Exception e) {
Thread.currentThread().getUncaughtExceptionHandler()
.uncaughtException(Thread.currentThread(), e);
}
}
} finally {
lockSetChangeListeners = false;
}
}
}
/**
* Removes the given {@link InvalidationListener} from this
* {@link SetListenerHelperEx}. If its was registered more than once,
* removes one occurrence.
*
* @param listener
* The listener to remove.
*/
public void removeListener(InvalidationListener listener) {
if (invalidationListeners == null) {
return;
}
// XXX: Prevent ConcurrentModificationExceptions (in case listeners are
// added during notifications); as we only create a new multi-set in the
// locked case, memory should not be waisted.
if (lockInvalidationListeners) {
invalidationListeners = new ArrayList<>(invalidationListeners);
}
// XXX: We have to ignore the hash code when removing listeners, as
// otherwise unbinding will be broken (JavaFX bindings violate the
// contract between equals() and hashCode(): JI-9028554); remove() may
// thus not be used.
for (Iterator<InvalidationListener> iterator = invalidationListeners
.iterator(); iterator.hasNext();) {
if (iterator.next().equals(listener)) {
iterator.remove();
break;
}
}
if (invalidationListeners.isEmpty()) {
invalidationListeners = null;
}
}
/**
* Removes the given {@link SetChangeListener} from this
* {@link SetListenerHelperEx}. If its was registered more than once,
* removes one occurrence.
*
* @param listener
* The listener to remove.
*/
public void removeListener(SetChangeListener<? super E> listener) {
if (setChangeListeners == null) {
return;
}
// XXX: Prevent ConcurrentModificationExceptions (in case listeners are
// added during notifications); as we only create a new multi-set in the
// locked case, memory should not be waisted.
if (lockSetChangeListeners) {
setChangeListeners = new ArrayList<>(setChangeListeners);
}
// XXX: We have to ignore the hash code when removing listeners, as
// otherwise unbinding will be broken (JavaFX bindings violate the
// contract between equals() and hashCode(): JI-9028554); remove() may
// thus not be used.
for (Iterator<SetChangeListener<? super E>> iterator = setChangeListeners
.iterator(); iterator.hasNext();) {
if (iterator.next().equals(listener)) {
iterator.remove();
break;
}
}
if (setChangeListeners.isEmpty()) {
setChangeListeners = null;
}
}
}