Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: AutoProperty construct to reduce boilerplate code for new syntaxes #46

Merged
merged 5 commits into from
Mar 7, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion core/src/main/java/dk/gtz/graphedit/util/RetryUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public static <T> T tryTimes(int maxAttempts, int sleepMillis, Supplier<T> f) {
try {
return f.get();
} catch(Exception e) {
logger.warn("'{}' {}/{} attempts left", e.getMessage(), attempts+1, maxAttempts);
logger.trace("'{}' {}/{} attempts left", e.getMessage(), attempts+1, maxAttempts);
sleep(sleepMillis);
}
}
Expand Down
16 changes: 12 additions & 4 deletions core/src/main/java/dk/gtz/graphedit/view/ModelEditorToolbar.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public class ModelEditorToolbar extends ToolBar {
private final IToolbox toolbox;
private final ObjectProperty<ITool> selectedTool;
private final ViewModelProjectResource resource;
private ComboBox<String> syntaxSelector;

/**
* Create a new instance
Expand All @@ -47,8 +48,15 @@ public ModelEditorToolbar(IToolbox toolbox, ObjectProperty<ITool> selectedTool,
* @return Builder-pattern style reference to this
*/
public ModelEditorToolbar withSyntaxSelector() {
if(resource.metadata().containsKey("graphedit_syntax"))
addSyntaxSelector();
if(resource.metadata().containsKey("graphedit_syntax")) {
var factories = DI.get(SyntaxFactoryCollection.class);
syntaxSelector = addSyntaxSelector(factories);
factories.addChangeListener(e -> {
getItems().remove(syntaxSelector);
syntaxSelector = addSyntaxSelector(factories);
});
}

addSeparator();
return this;
}
Expand All @@ -74,8 +82,7 @@ private void setupContent() {
}
}

private void addSyntaxSelector() {
var factories = DI.get(SyntaxFactoryCollection.class);
private ComboBox<String> addSyntaxSelector(SyntaxFactoryCollection factories) {
ObservableList<String> list = FXCollections.observableArrayList();
for(var factory : factories.entrySet())
list.add(factory.getKey());
Expand All @@ -98,6 +105,7 @@ protected void onChanged(String oldValue, String newValue) {
}
};
cmb.getSelectionModel().selectedItemProperty().addListener(listener);
return cmb;
}

private void addButton(ITool tool) {
Expand Down
71 changes: 41 additions & 30 deletions core/src/main/java/dk/gtz/graphedit/view/StatusBarController.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@

import java.util.List;

import dk.gtz.graphedit.spi.ILanguageServer;
import dk.gtz.graphedit.viewmodel.LanguageServerCollection;
import dk.yalibs.yadi.DI;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.MapChangeListener;
import javafx.geometry.Insets;
import javafx.scene.control.Label;
import javafx.scene.input.MouseEvent;
Expand Down Expand Up @@ -57,6 +59,8 @@ public StatusBarController() {
// debugMouseHover();
}

// NOTE: kept for future use
@SuppressWarnings("unused")
private void debugMouseHover() {
Platform.runLater(() -> {
var w = DI.get(Window.class);
Expand All @@ -78,37 +82,44 @@ private void initializeLabels() {

private void initializeLSPs() {
var lsps = DI.get(LanguageServerCollection.class);
for(var server : lsps.values()) {
server.addProgressCallback(p -> {
Platform.runLater(() -> {
lspLabel.setText(p.title()+":");
messageLabel.setText(p.message());
});
switch(p.type()) {
case PROGRESS:
case BEGIN:
if(!spinnerThread.isAlive())
startSpinnerThread();
break;
case END:
if(spinnerThread.isAlive())
spinnerThread.interrupt();
Platform.runLater(() -> spinnerString.set("\u2714")); // checkmark unicode character
break;
case END_FAIL:
if(spinnerThread.isAlive())
spinnerThread.interrupt();
Platform.runLater(() -> spinnerString.set("\u2717")); // x mark unicode character
break;
default:
if(spinnerThread.isAlive())
spinnerThread.interrupt();
Platform.runLater(() -> spinnerString.set("?"));
break;

}
for(var lsp : lsps.values())
addSpinner(lsp);
lsps.addListener((MapChangeListener<String,ILanguageServer>)e -> {
if(e.wasAdded())
addSpinner(e.getValueAdded());
});
}

private void addSpinner(ILanguageServer server) {
server.addProgressCallback(p -> {
Platform.runLater(() -> {
lspLabel.setText(p.title()+":");
messageLabel.setText(p.message());
});
}
switch(p.type()) {
case PROGRESS:
case BEGIN:
if(!spinnerThread.isAlive())
startSpinnerThread();
break;
case END:
if(spinnerThread.isAlive())
spinnerThread.interrupt();
Platform.runLater(() -> spinnerString.set("\u2714")); // checkmark unicode character
break;
case END_FAIL:
if(spinnerThread.isAlive())
spinnerThread.interrupt();
Platform.runLater(() -> spinnerString.set("\u2717")); // x mark unicode character
break;
default:
if(spinnerThread.isAlive())
spinnerThread.interrupt();
Platform.runLater(() -> spinnerString.set("?"));
break;

}
});
}

private void startSpinnerThread() {
Expand Down
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);
sillydan1 marked this conversation as resolved.
Show resolved Hide resolved
}
}
}

@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);
sillydan1 marked this conversation as resolved.
Show resolved Hide resolved
}
}
}

@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);
sillydan1 marked this conversation as resolved.
Show resolved Hide resolved
}
}
}

@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);
sillydan1 marked this conversation as resolved.
Show resolved Hide resolved
}
}
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);
sillydan1 marked this conversation as resolved.
Show resolved Hide resolved
}
}
}

@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);
sillydan1 marked this conversation as resolved.
Show resolved Hide resolved
}
}
}

@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);
sillydan1 marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
}
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 {}
sillydan1 marked this conversation as resolved.
Show resolved Hide resolved
Loading