Skip to content

Commit

Permalink
feat: add autoproperty abstract base class to help with boilerplate
Browse files Browse the repository at this point in the history
This is not breaking, as you can still simply override the functions
  • Loading branch information
sillydan1 committed Mar 4, 2024
1 parent 9ff17c0 commit 4e8a53c
Show file tree
Hide file tree
Showing 11 changed files with 260 additions and 549 deletions.
213 changes: 213 additions & 0 deletions core/src/main/java/dk/gtz/graphedit/viewmodel/AutoProperty.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
package dk.gtz.graphedit.viewmodel;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javafx.beans.InvalidationListener;
import javafx.beans.property.Property;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.beans.value.WritableObjectValue;

public abstract class AutoProperty<T extends Property<T>> implements Property<T> {
private static Logger logger = LoggerFactory.getLogger(AutoProperty.class);
private T value;
private List<Field> fields;
private final Map<Field, ChangeListener<? super Object>> changeListeners;
private final Map<Field, InvalidationListener> invalidationListeners;

protected AutoProperty() {
this.changeListeners = new HashMap<>();
this.invalidationListeners = new HashMap<>();
}

protected void loadFields(Class<?> clazz, T value) {
this.value = value;
fields = new ArrayList<>();
for(var field : clazz.getFields())
if(List.of(field.getAnnotations())
.stream()
.anyMatch(a -> a
.annotationType()
.getSimpleName()
.equals(Autolisten.class.getSimpleName())))
fields.add(field);
}

@Override
public void addListener(ChangeListener<? super T> listener) {
for(var field : fields) {
var isObservable = ObservableValue.class.isAssignableFrom(field.getType());
if(!isObservable)
continue;
try {
var observable = (ObservableValue<?>)field.get(value);
if(!changeListeners.containsKey(field))
changeListeners.put(field, (e,o,n) -> listener.changed(value, value, value));
observable.addListener(changeListeners.get(field));
} catch (IllegalAccessException e) {
logger.error("error adding listener '{}': {}", field.getName(), e.getMessage(), e);
}
}
}

@Override
public void removeListener(ChangeListener<? super T> listener) {
for(var field : fields) {
var isObservable = ObservableValue.class.isAssignableFrom(field.getType());
if(!isObservable)
continue;
try {
var observable = (ObservableValue<?>)field.get(value);
if(changeListeners.containsKey(field))
observable.addListener(changeListeners.get(field));
else // BUG: This probably doesnt work, but it doesn't hurt to try
observable.addListener((e,o,n) -> listener.changed(value, value, value));
} catch (IllegalAccessException e) {
logger.error("error adding listener '{}': {}", field.getName(), e.getMessage(), e);
}
}
}

@Override
public void addListener(InvalidationListener listener) {
for(var field : fields) {
if(!field.getType().isAssignableFrom(ObservableValue.class))
continue;
try {
var observable = (ObservableValue<?>)field.get(value);
if(!invalidationListeners.containsKey(field))
invalidationListeners.put(field, listener);
observable.addListener(invalidationListeners.get(field));
} catch (IllegalAccessException e) {
logger.error("error adding listener '{}': {}", field.getName(), e.getMessage(), e);
}
}
}

@Override
public void removeListener(InvalidationListener listener) {
for(var field : fields) {
var isObservable = ObservableValue.class.isAssignableFrom(field.getType());
if(!isObservable)
continue;
try {
var observable = (ObservableValue<?>)field.get(value);
if(invalidationListeners.containsKey(field))
observable.addListener(invalidationListeners.get(field));
else // BUG: This probably doesnt work, but it doesn't hurt to try
observable.addListener(listener);
} catch (IllegalAccessException e) {
logger.error("error adding listener '{}': {}", field.getName(), e.getMessage(), e);
}
}
}

@Override
@SuppressWarnings({ "unchecked", "rawtypes" })
public void bind(ObservableValue<? extends T> observable) {
for(var field : fields) {
if(!field.getType().isAssignableFrom(Property.class))
continue;
try {
var thisObservable = (Property)field.get(value);
var thatObservable = (Property)field.get(observable.getValue());
thisObservable.bind(thatObservable);
} catch (IllegalAccessException e) {
logger.error("error binding '{}': {}", field.getName(), e.getMessage(), e);
}
}
}

@Override
public void unbind() {
for(var field : fields) {
var isProperty = Property.class.isAssignableFrom(field.getType());
if(!isProperty)
continue;
try {
((Property<?>)field.get(value)).unbind();
} catch (IllegalAccessException e) {
logger.error("error binding '{}': {}", field.getName(), e.getMessage(), e);
}
}
}

@Override
public boolean isBound() {
for(var field : fields) {
var isProperty = Property.class.isAssignableFrom(field.getType());
if(!isProperty)
continue;
try {
if(((Property<?>)field.get(value)).isBound())
return true;
} catch (IllegalAccessException e) {
logger.error("error listener '{}': {}", field.getName(), e.getMessage(), e);
}
}
return false;
}

@Override
@SuppressWarnings({ "rawtypes", "unchecked" })
public void bindBidirectional(Property<T> other) {
for(var field : fields) {
var isProperty = Property.class.isAssignableFrom(field.getType());
if(!isProperty)
continue;
try {
var thisObservable = (Property)field.get(value);
var thatObservable = (Property)field.get(other);
thisObservable.bindBidirectional(thatObservable);
} catch (IllegalAccessException e) {
logger.error("error adding listener '{}': {}", field.getName(), e.getMessage(), e);
}
}
}

@Override
@SuppressWarnings({ "unchecked", "rawtypes" })
public void unbindBidirectional(Property<T> other) {
for(var field : fields) {
var isProperty = Property.class.isAssignableFrom(field.getType());
if(!isProperty)
continue;
try {
var thisObservable = (Property)field.get(value);
var thatObservable = (Property)field.get(other);
thisObservable.unbindBidirectional(thatObservable);
} catch (IllegalAccessException e) {
logger.error("error adding listener '{}': {}", field.getName(), e.getMessage(), e);
}
}
}

@Override
public Object getBean() {
return null;
}

@Override
@SuppressWarnings({ "rawtypes", "unchecked" })
public void setValue(T value) {
for(var field : fields) {
var isWritable = WritableObjectValue.class.isAssignableFrom(field.getType());
if(!isWritable)
continue;
try {
var thisObservable = (WritableObjectValue)field.get(this.value);
var thatObservable = (WritableObjectValue)field.get(value);
thisObservable.set(thatObservable.get());
} catch (IllegalAccessException e) {
logger.error("error adding listener '{}': {}", field.getName(), e.getMessage(), e);
}
}
}
}
7 changes: 7 additions & 0 deletions core/src/main/java/dk/gtz/graphedit/viewmodel/Autolisten.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package dk.gtz.graphedit.viewmodel;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
public @interface Autolisten {}
78 changes: 7 additions & 71 deletions core/src/main/java/dk/gtz/graphedit/viewmodel/ViewModelEdge.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,18 @@
import java.util.UUID;

import dk.gtz.graphedit.model.ModelEdge;
import javafx.beans.InvalidationListener;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.Node;

/**
* The ViewModel representation of a graph edge.
* Edges connects vertices with other vertices.
*/
public class ViewModelEdge implements IInspectable, ISelectable, IHoverable, IFocusable, Property<ViewModelEdge> {
public class ViewModelEdge extends AutoProperty<ViewModelEdge> implements IInspectable, ISelectable, IHoverable, IFocusable {
private final UUID uuid;
private final BooleanProperty isSelected;
private final List<Runnable> focusEventHandlers;
Expand All @@ -28,12 +25,14 @@ public class ViewModelEdge implements IInspectable, ISelectable, IHoverable, IFo
/**
* Property pointing to the source vertex id
*/
protected final ObjectProperty<UUID> source;
@Autolisten
public final ObjectProperty<UUID> source;

/**
* Property pointing to the target vertex id
*/
protected final ObjectProperty<UUID> target;
@Autolisten
public final ObjectProperty<UUID> target;

/**
* Constructs a new view model edge instance
Expand All @@ -42,6 +41,8 @@ public class ViewModelEdge implements IInspectable, ISelectable, IHoverable, IFo
* @param target the syntactic element where the edge targets
*/
public ViewModelEdge(UUID uuid, ObjectProperty<UUID> source, ObjectProperty<UUID> target) {
super();
loadFields(getClass(), this);
this.uuid = uuid;
this.source = source;
this.target = target;
Expand Down Expand Up @@ -157,81 +158,16 @@ public List<InspectableProperty> getInspectableObjects() {
new InspectableProperty("Target ID", target));
}

@Override
public Object getBean() {
return null;
}

@Override
public String getName() {
return "";
}

@Override
public void addListener(ChangeListener<? super ViewModelEdge> listener) {
source.addListener((e,o,n) -> listener.changed(this,this,this));
target.addListener((e,o,n) -> listener.changed(this,this,this));
// NOTE: isSelected changes does not constitute a "changed" event
}

@Override
public void removeListener(ChangeListener<? super ViewModelEdge> listener) {
source.removeListener((e,o,n) -> listener.changed(this,this,this));
target.removeListener((e,o,n) -> listener.changed(this,this,this));
}

@Override
public ViewModelEdge getValue() {
return this;
}

@Override
public void addListener(InvalidationListener listener) {
source.addListener(listener);
target.addListener(listener);
}

@Override
public void removeListener(InvalidationListener listener) {
source.removeListener(listener);
target.removeListener(listener);
}

@Override
public void setValue(ViewModelEdge value) {
source.set(value.source().get());
target.set(value.target().get());
}

@Override
public void bind(ObservableValue<? extends ViewModelEdge> observable) {
source.bind(observable.getValue().source());
target.bind(observable.getValue().target());
}

@Override
public void unbind() {
source.unbind();
target.unbind();
}

@Override
public boolean isBound() {
return source.isBound() || target.isBound();
}

@Override
public void bindBidirectional(Property<ViewModelEdge> other) {
source.bindBidirectional(other.getValue().source());
target.bindBidirectional(other.getValue().target());
}

@Override
public void unbindBidirectional(Property<ViewModelEdge> other) {
source.unbindBidirectional(other.getValue().source());
target.unbindBidirectional(other.getValue().target());
}

@Override
public void hover(Node hoverDisplay) {
hoverElement.set(hoverDisplay);
Expand Down
Loading

0 comments on commit 4e8a53c

Please sign in to comment.