package lighthouse.threading; import javafx.beans.WeakListener; import javafx.collections.*; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.Set; /** * Utility functions that mirror changes from one list into another list. JavaFX already provides this functionality * of course under the name "content binding"; a mirror is a content binding that relays changes into other threads * first. Thus you can have an ObservableList which is updated in one thread, but still bound to directly in the UI * thread, without needing to worry about cross-thread interference. */ public class ObservableMirrors { /** * Creates an unmodifiable list that asynchronously follows changes in mirrored, with changes applied using * the given executor. This should only be called on the thread that owns the list to be mirrored, as the contents * will be read. */ public static <T> ObservableList<T> mirrorList(ObservableList<T> mirrored, AffinityExecutor runChangesIn) { ObservableList<T> result = FXCollections.observableArrayList(); result.setAll(mirrored); mirrored.addListener(new ListMirror<T>(result, runChangesIn)); return FXCollections.unmodifiableObservableList(result); } private static class ListMirror<E> implements ListChangeListener<E>, WeakListener { private final WeakReference<ObservableList<E>> targetList; private final AffinityExecutor runChangesIn; public ListMirror(ObservableList<E> list, AffinityExecutor runChangesIn) { this.targetList = new WeakReference<>(list); this.runChangesIn = runChangesIn; } @Override public void onChanged(Change<? extends E> change) { final List<E> list = targetList.get(); if (list == null) { change.getList().removeListener(this); } else { // If we're already in the right thread this will just run the change immediately, as per normal. // Change objects are not thread safe. They may be reused by listeners following this one. However, // we cheat here and exploit knowledge of the implementation: a change is basically immutable and // self contained except for the iteration state. So we synchronize on the change and reset it at the // start to ensure we can iterate over it safely. Note that set changes actually are immutable and // so don't need this. LinkedList<List<? extends E>> sublists = new LinkedList<>(); while (change.next()) { if (change.wasPermutated()) { sublists.add(new ArrayList<>(change.getList().subList(change.getFrom(), change.getTo()))); } else if (change.wasAdded()) { sublists.add(new ArrayList<>(change.getAddedSubList())); } } runChangesIn.executeASAP(() -> { synchronized (change) { change.reset(); while (change.next()) { if (change.wasPermutated()) { list.subList(change.getFrom(), change.getTo()).clear(); list.addAll(change.getFrom(), sublists.pollFirst()); } else { if (change.wasReplaced() && change.getFrom() == change.getTo() - 1) { // Don't know how to manage multi-item replacements. list.set(change.getFrom(), sublists.pollFirst().get(0)); } else { if (change.wasRemoved()) { list.subList(change.getFrom(), change.getFrom() + change.getRemovedSize()).clear(); } if (change.wasAdded()) { list.addAll(change.getFrom(), sublists.pollFirst()); } } } } } }); } } @Override public boolean wasGarbageCollected() { return targetList.get() == null; } // Do we really need these? @Override public int hashCode() { final List<E> list = targetList.get(); return (list == null)? 0 : list.hashCode(); } @Override public boolean equals(Object obj) { if (this == obj) { return true; } final List<E> list1 = targetList.get(); if (list1 == null) { return false; } if (obj instanceof ListMirror) { final ListMirror<?> other = (ListMirror<?>) obj; final List<?> list2 = other.targetList.get(); return list1 == list2; } return false; } } public static <K, V> ObservableMap<K, V> mirrorMap(ObservableMap<K, V> mirrored, AffinityExecutor runChangesIn) { ObservableMap<K, V> result = FXCollections.observableHashMap(); result.putAll(mirrored); mirrored.addListener(new MapMirror<K, V>(result, runChangesIn)); return result; } private static class MapMirror<K, V> implements MapChangeListener<K, V>, WeakListener { private final WeakReference<ObservableMap<K, V>> targetMap; private final AffinityExecutor runChangesIn; public MapMirror(ObservableMap<K, V> targetMap, AffinityExecutor runChangesIn) { this.targetMap = new WeakReference<>(targetMap); this.runChangesIn = runChangesIn; } @Override public boolean wasGarbageCollected() { return targetMap.get() == null; } @Override public void onChanged(Change<? extends K, ? extends V> change) { final ObservableMap<K, V> map = targetMap.get(); if (map == null) { change.getMap().removeListener(this); } else { runChangesIn.executeASAP(() -> { if (change.wasAdded()) { map.put(change.getKey(), change.getValueAdded()); } else if (change.wasRemoved()) { map.remove(change.getKey()); } }); } } } /** * Creates an unmodifiable list that asynchronously follows changes in mirrored, with changes applied using * the given executor. This should only be called on the thread that owns the list to be mirrored, as the contents * will be read. */ public static <T> ObservableSet<T> mirrorSet(ObservableSet<T> mirrored, AffinityExecutor runChangesIn) { @SuppressWarnings("unchecked") ObservableSet<T> result = FXCollections.observableSet(); result.addAll(mirrored); mirrored.addListener(new SetMirror<T>(result, runChangesIn)); return FXCollections.unmodifiableObservableSet(result); } private static class SetMirror<E> implements SetChangeListener<E>, WeakListener { private final WeakReference<ObservableSet<E>> targetSet; private final AffinityExecutor runChangesIn; public SetMirror(ObservableSet<E> set, AffinityExecutor runChangesIn) { this.targetSet = new WeakReference<>(set); this.runChangesIn = runChangesIn; } @Override public void onChanged(final Change<? extends E> change) { final ObservableSet<E> set = targetSet.get(); if (set == null) { change.getSet().removeListener(this); } else { // If we're already in the right thread this will just run the change immediately, as per normal. runChangesIn.executeASAP(() -> { if (change.wasAdded()) set.add(change.getElementAdded()); if (change.wasRemoved()) set.remove(change.getElementRemoved()); }); } } @Override public boolean wasGarbageCollected() { return targetSet.get() == null; } @Override public int hashCode() { final ObservableSet<E> set = targetSet.get(); return (set == null)? 0 : set.hashCode(); } @Override public boolean equals(Object obj) { if (this == obj) { return true; } final Set<E> set1 = targetSet.get(); if (set1 == null) { return false; } if (obj instanceof SetMirror) { final SetMirror<?> other = (SetMirror<?>) obj; final Set<?> list2 = other.targetSet.get(); return set1 == list2; } return false; } } }