tornadofx.property.DirtyState Maven / Gradle / Ivy
package tornadofx.property;
import javafx.beans.property.Property;
import javafx.beans.property.ReadOnlyBooleanWrapper;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.beans.value.WritableValue;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* Track dirty states for a collection of properties, with undo feature to rollback changes.
*
* Create an instance of DirtyState with a reference to the bean that holds the properties
* you want to track changes in. All puplic properties are automatically tracked, unless you
* supply an explicit list of properties to track.
*
*
* // Track all properties in customer
* DirtyState dirtyState = new DirtyState(customer);
*
* // Track only username and password
* DirtyState dirtyState = new DirtyState(customer,
* customer.usernameProperty(), customer.passwordProperty());
*
*
* The list of currently dirty properties can be obtained via {@code DirtyState#getUnmodifiableDirtyProperties()}.
*
* The {@code DirtyState} itself is a {@code ReadOnlyProperty} you can use to track if any of the properties are dirty. A good
* use case for this is for example conditionally enabling of a save button:
*
*
* // Disable save button until anything is changed
* saveButton.disableProperty().bind(dirtyState.not())
*
*
* To roll back any changes, call {@code DirtyState#reset()}.
*
* To reset dirty state and clear the rollback buffer, call {@code DirtyState#undo()}.
*
*
* // Undo changes
* undoButton.setOnAction(event -> dirtyState.reset());
*
* // Show undo button when changes are performed
* undoButton.visibleProperty().bind(dirtyState);
*
*
* It's possible to change the tracked properties after the tracker is created. Let's say you want to
* listen to all properties but the id property of a customer object:
*
*
* // Track all but the id property
* DirtyState dirtyState = new DirtyState(customer);
* dirtyState.getProperties().remove(customer.idProperty());
*
*/
public class DirtyState extends ReadOnlyBooleanWrapper {
private final ObservableList properties = FXCollections.observableArrayList();
private final ObservableList dirtyProperties = FXCollections.observableArrayList();
private final ObservableList unmodifiableDirtyProperties = FXCollections.unmodifiableObservableList(dirtyProperties);
private final Map initialValues = new HashMap<>();
private DirtyListener dirtyListener = new DirtyListener();
public DirtyState(Object bean) {
this(bean, true);
}
public DirtyState(Object bean, boolean addDeclaredProperties) {
super(bean, "dirty", false);
monitorChanges();
if (addDeclaredProperties)
addDeclaredProperties();
}
public DirtyState(Object bean, List properties) {
super(bean, "dirty", false);
monitorChanges();
getProperties().addAll(properties);
}
private void addDeclaredProperties() {
Object bean = getBean();
if (bean == null) return;
for (Method method : bean.getClass().getDeclaredMethods()) {
if (method.getName().endsWith("Property") && Property.class.isAssignableFrom(method.getReturnType())
&& !DirtyState.class.isAssignableFrom(method.getReturnType())) {
try {
Property property = (Property) method.invoke(bean);
if (property != null)
properties.add(property);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
}
private void monitorChanges() {
properties.addListener((ListChangeListener) change -> {
while (change.next()) {
if (change.wasAdded()) change.getAddedSubList().forEach(p -> p.addListener(dirtyListener));
if (change.wasRemoved()) change.getRemoved().forEach(p -> p.removeListener(dirtyListener));
}
});
}
private class DirtyListener implements ChangeListener {
@SuppressWarnings("SuspiciousMethodCalls")
public void changed(ObservableValue property, Object oldValue, Object newValue) {
if (dirtyProperties.contains(property)) {
// Remove dirty state if newValue equals inititalValue
if (Objects.equals(initialValues.get(property), newValue))
dirtyProperties.remove(property);
// If no other properties dirty, remove dirty state
if (dirtyProperties.isEmpty() && isDirty())
setValue(false);
} else {
// Configure initital value and add to dirty property list
initialValues.put((Property) property, oldValue);
dirtyProperties.add((Property) property);
// Only trigger dirty state change if previously clean
if (!isDirty())
setValue(true);
}
}
}
public void reset() {
dirtyProperties.clear();
initialValues.clear();
setValue(false);
}
public void undo() {
initialValues.forEach(WritableValue::setValue);
setValue(false);
}
public Boolean isDirty() {
return getValue();
}
public ObservableList getProperties() {
return properties;
}
public ObservableList getUnmodifiableDirtyProperties() {
return unmodifiableDirtyProperties;
}
}