package org.jabref.gui.util;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.BooleanPropertyBase;
import javafx.beans.property.ListProperty;
import javafx.beans.property.Property;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.css.PseudoClass;
import javafx.scene.Node;
/**
* Helper methods for javafx binding.
* Some methods are taken from https://bugs.openjdk.java.net/browse/JDK-8134679
*/
public class BindingsHelper {
private BindingsHelper() {
}
public static <T> BooleanBinding any(ObservableList<T> source, Predicate<T> predicate) {
return Bindings.createBooleanBinding(() -> source.stream().anyMatch(predicate), source);
}
public static <T> BooleanBinding all(ObservableList<T> source, Predicate<T> predicate) {
// Stream.allMatch() (in contrast to Stream.anyMatch() returns 'true' for empty streams, so this has to be checked explicitly.
return Bindings.createBooleanBinding(() -> !source.isEmpty() && source.stream().allMatch(predicate), source);
}
public static void includePseudoClassWhen(Node node, PseudoClass pseudoClass, ObservableValue<? extends Boolean> condition) {
BooleanProperty pseudoClassState = new BooleanPropertyBase(false) {
@Override
protected void invalidated() {
node.pseudoClassStateChanged(pseudoClass, get());
}
@Override
public Object getBean() {
return node;
}
@Override
public String getName() {
return pseudoClass.getPseudoClassName();
}
};
pseudoClassState.bind(condition);
}
/**
* Binds propertA bidirectional to propertyB using the provided map functions to convert between them.
*/
public static <A, B> void bindBidirectional(Property<A> propertyA, Property<B> propertyB, Function<A, B> mapAtoB, Function<B, A> mapBtoA) {
Consumer<B> updateA = newValueB -> propertyA.setValue(mapBtoA.apply(newValueB));
Consumer<A> updateB = newValueA -> propertyB.setValue(mapAtoB.apply(newValueA));
bindBidirectional(propertyA, propertyB, updateA, updateB);
}
/**
* Binds propertA bidirectional to propertyB while using updateB to update propertyB when propertyA changed.
*/
public static <A> void bindBidirectional(Property<A> propertyA, ObservableValue<A> propertyB, Consumer<A> updateB) {
bindBidirectional(propertyA, propertyB, propertyA::setValue, updateB);
}
/**
* Binds propertA bidirectional to propertyB using updateB to update propertyB when propertyA changed and similar
* for updateA.
*/
public static <A, B> void bindBidirectional(ObservableValue<A> propertyA, ObservableValue<B> propertyB, Consumer<B> updateA, Consumer<A> updateB) {
final BidirectionalBinding<A, B> binding = new BidirectionalBinding<>(propertyA, propertyB, updateA, updateB);
// use updateB as initial source
updateA.accept(propertyB.getValue());
propertyA.addListener(binding.getChangeListenerA());
propertyB.addListener(binding.getChangeListenerB());
}
public static <A, B> void bindContentBidirectional(ListProperty<A> listProperty, Property<B> property, Function<List<A>, B> mapToB, Function<B, List<A>> mapToList) {
final BidirectionalListBinding<A, B> binding = new BidirectionalListBinding<>(listProperty, property, mapToB, mapToList);
// use property as initial source
listProperty.setAll(mapToList.apply(property.getValue()));
listProperty.addListener(binding);
property.addListener(binding);
}
private static class BidirectionalBinding<A, B> {
private final ObservableValue<A> propertyA;
private final Consumer<B> updateA;
private final Consumer<A> updateB;
private boolean updating = false;
public BidirectionalBinding(ObservableValue<A> propertyA, ObservableValue<B> propertyB, Consumer<B> updateA, Consumer<A> updateB) {
this.propertyA = propertyA;
this.updateA = updateA;
this.updateB = updateB;
}
public ChangeListener<? super A> getChangeListenerA() {
return this::changedA;
}
public ChangeListener<? super B> getChangeListenerB() {
return this::changedB;
}
public void changedA(ObservableValue<? extends A> observable, A oldValue, A newValue) {
updateLocked(updateB, oldValue, newValue);
}
public void changedB(ObservableValue<? extends B> observable, B oldValue, B newValue) {
updateLocked(updateA, oldValue, newValue);
}
private <T> void updateLocked(Consumer<T> update, T oldValue, T newValue) {
if (!updating) {
try {
updating = true;
update.accept(newValue);
} finally {
updating = false;
}
}
}
}
private static class BidirectionalListBinding<A, B> implements ListChangeListener<A>, ChangeListener<B> {
private final ListProperty<A> listProperty;
private final Property<B> property;
private final Function<List<A>, B> mapToB;
private final Function<B, List<A>> mapToList;
private boolean updating = false;
public BidirectionalListBinding(ListProperty<A> listProperty, Property<B> property, Function<List<A>, B> mapToB, Function<B, List<A>> mapToList) {
this.listProperty = listProperty;
this.property = property;
this.mapToB = mapToB;
this.mapToList = mapToList;
}
@Override
public void changed(ObservableValue<? extends B> observable, B oldValue, B newValue) {
if (!updating) {
try {
updating = true;
listProperty.setAll(mapToList.apply(newValue));
} finally {
updating = false;
}
}
}
@Override
public void onChanged(Change<? extends A> c) {
if (!updating) {
try {
updating = true;
property.setValue(mapToB.apply(listProperty.getValue()));
} finally {
updating = false;
}
}
}
}
}