diff --git a/Platform/Apple/tools/jace/pom.xml b/Platform/Apple/tools/jace/pom.xml
index ab81f472..7b76897b 100644
--- a/Platform/Apple/tools/jace/pom.xml
+++ b/Platform/Apple/tools/jace/pom.xml
@@ -29,6 +29,14 @@
17
+
+
+ org.8bitbunch
+ lawlesslegends
+ 3.0-SNAPSHOT
+
+
+ jace.config.InvokableActionAnnotationProcessor
3.11.0
@@ -113,18 +121,6 @@
}
-
-
- org.reflections
- reflections
- 0.10.2
-
-
- module reflections {
- exports org.reflections;
- }
-
-
true
diff --git a/Platform/Apple/tools/jace/src/main/java/jace/JaceUIController.java b/Platform/Apple/tools/jace/src/main/java/jace/JaceUIController.java
index 7654da3e..feb008f6 100644
--- a/Platform/Apple/tools/jace/src/main/java/jace/JaceUIController.java
+++ b/Platform/Apple/tools/jace/src/main/java/jace/JaceUIController.java
@@ -13,6 +13,7 @@ import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
+import java.util.function.Function;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -259,8 +260,8 @@ public class JaceUIController {
private void connectButtons(Node n) {
if (n instanceof Button button) {
- Runnable action = Utility.getNamedInvokableAction(button.getText());
- button.setOnMouseClicked(evt -> action.run());
+ Function action = Utility.getNamedInvokableAction(button.getText());
+ button.setOnMouseClicked(evt -> action.apply(false));
} else if (n instanceof Parent parent) {
parent.getChildrenUnmodifiable().forEach(child -> connectButtons(child));
}
diff --git a/Platform/Apple/tools/jace/src/main/java/jace/config/Configuration.java b/Platform/Apple/tools/jace/src/main/java/jace/config/Configuration.java
index 1ae0dba8..2e160fb7 100644
--- a/Platform/Apple/tools/jace/src/main/java/jace/config/Configuration.java
+++ b/Platform/Apple/tools/jace/src/main/java/jace/config/Configuration.java
@@ -30,7 +30,6 @@ import java.io.ObjectStreamException;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.GenericArrayType;
-import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
@@ -64,15 +63,6 @@ import javafx.scene.image.ImageView;
*/
public class Configuration implements Reconfigurable {
- private static Method findAnyMethodByName(Class extends Reconfigurable> aClass, String m) {
- for (Method method : aClass.getMethods()) {
- if (method.getName().equals(m)) {
- return method;
- }
- }
- return null;
- }
-
static ConfigurableField getConfigurableFieldInfo(Reconfigurable subject, String settingName) {
Field f;
try {
@@ -88,15 +78,6 @@ public class Configuration implements Reconfigurable {
return (f != null && !f.shortName().equals("")) ? f.shortName() : longName;
}
- public static InvokableAction getInvokableActionInfo(Reconfigurable subject, String actionName) {
- for (Method m : subject.getClass().getMethods()) {
- if (m.getName().equals(actionName) && m.isAnnotationPresent(InvokableAction.class)) {
- return m.getAnnotation(InvokableAction.class);
- }
- }
- return null;
- }
-
public static Optional getChangedIcon() {
return Utility.loadIcon("icon_exclaim.gif").map(ImageView::new);
}
@@ -301,13 +282,19 @@ public class Configuration implements Reconfigurable {
return;
}
- for (Method m : node.subject.getClass().getMethods()) {
- if (!m.isAnnotationPresent(InvokableAction.class)) {
- continue;
+ InvokableActionRegistry registry = InvokableActionRegistry.getInstance();
+ registry.getStaticMethodNames(node.subject.getClass()).stream().forEach((name) -> {
+ InvokableAction action = registry.getStaticMethodInfo(name);
+ if (action != null) {
+ node.hotkeys.put(name, action.defaultKeyMapping());
}
- InvokableAction action = m.getDeclaredAnnotation(InvokableAction.class);
- node.hotkeys.put(m.getName(), action.defaultKeyMapping());
- }
+ });
+ registry.getInstanceMethodNames(node.subject.getClass()).stream().forEach((name) -> {
+ InvokableAction action = registry.getInstanceMethodInfo(name);
+ if (action != null) {
+ node.hotkeys.put(name, action.defaultKeyMapping());
+ }
+ });
for (Field f : node.subject.getClass().getFields()) {
// System.out.println("Evaluating field " + f.getName());
@@ -530,12 +517,18 @@ public class Configuration implements Reconfigurable {
private static void doApply(ConfigNode node) {
List removeList = new ArrayList<>();
Keyboard.unregisterAllHandlers(node.subject);
- node.hotkeys.keySet().stream().forEach((m) -> {
- Method method = findAnyMethodByName(node.subject.getClass(), m);
- if (method != null) {
- InvokableAction action = method.getAnnotation(InvokableAction.class);
- for (String code : node.hotkeys.get(m)) {
- Keyboard.registerInvokableAction(action, node.subject, method, code);
+ InvokableActionRegistry registry = InvokableActionRegistry.getInstance();
+ node.hotkeys.keySet().stream().forEach((name) -> {
+ InvokableAction action = registry.getStaticMethodInfo(name);
+ if (action != null) {
+ for (String code : node.hotkeys.get(name)) {
+ Keyboard.registerInvokableAction(action, name, registry.getStaticFunction(name), code);
+ }
+ }
+ action = registry.getInstanceMethodInfo(name);
+ if (action != null) {
+ for (String code : node.hotkeys.get(name)) {
+ Keyboard.registerInvokableAction(action, name, registry.getInstanceFunction(name), code);
}
}
});
diff --git a/Platform/Apple/tools/jace/src/main/java/jace/config/ConfigurationUIController.java b/Platform/Apple/tools/jace/src/main/java/jace/config/ConfigurationUIController.java
index f2edb882..c64ccc94 100644
--- a/Platform/Apple/tools/jace/src/main/java/jace/config/ConfigurationUIController.java
+++ b/Platform/Apple/tools/jace/src/main/java/jace/config/ConfigurationUIController.java
@@ -1,18 +1,5 @@
package jace.config;
-import jace.config.Configuration.ConfigNode;
-import javafx.beans.Observable;
-import javafx.beans.value.ObservableValue;
-import javafx.collections.FXCollections;
-import javafx.fxml.FXML;
-import javafx.scene.Node;
-import javafx.scene.control.*;
-import javafx.scene.input.MouseEvent;
-import javafx.scene.layout.HBox;
-import javafx.scene.layout.VBox;
-import javafx.scene.text.Text;
-import javafx.util.StringConverter;
-
import java.io.File;
import java.io.Serializable;
import java.lang.reflect.Field;
@@ -25,6 +12,26 @@ import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
+import jace.config.Configuration.ConfigNode;
+import javafx.beans.Observable;
+import javafx.beans.value.ObservableValue;
+import javafx.collections.FXCollections;
+import javafx.fxml.FXML;
+import javafx.scene.Node;
+import javafx.scene.control.CheckBox;
+import javafx.scene.control.ChoiceBox;
+import javafx.scene.control.Label;
+import javafx.scene.control.ScrollPane;
+import javafx.scene.control.SplitPane;
+import javafx.scene.control.TextField;
+import javafx.scene.control.TreeItem;
+import javafx.scene.control.TreeView;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.VBox;
+import javafx.scene.text.Text;
+import javafx.util.StringConverter;
+
public class ConfigurationUIController {
public static final String DELIMITER = "~!~";
@@ -177,7 +184,8 @@ public class ConfigurationUIController {
}
private Node buildKeyShortcutRow(ConfigNode node, String actionName, String[] values) {
- InvokableAction actionInfo = Configuration.getInvokableActionInfo(node.subject, actionName);
+ InvokableActionRegistry registry = InvokableActionRegistry.getInstance();
+ InvokableAction actionInfo = registry.getInstanceMethodInfo(actionName);
if (actionInfo == null) {
return null;
}
diff --git a/Platform/Apple/tools/jace/src/main/java/jace/config/InvokableActionAnnotationProcessor.java b/Platform/Apple/tools/jace/src/main/java/jace/config/InvokableActionAnnotationProcessor.java
new file mode 100644
index 00000000..b8fd74a7
--- /dev/null
+++ b/Platform/Apple/tools/jace/src/main/java/jace/config/InvokableActionAnnotationProcessor.java
@@ -0,0 +1,168 @@
+package jace.config;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.nio.file.Files;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import javax.annotation.processing.AbstractProcessor;
+import javax.annotation.processing.Messager;
+import javax.annotation.processing.ProcessingEnvironment;
+import javax.annotation.processing.RoundEnvironment;
+import javax.annotation.processing.SupportedAnnotationTypes;
+import javax.annotation.processing.SupportedSourceVersion;
+import javax.lang.model.SourceVersion;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.TypeElement;
+
+// Compile-time annotation processor which creates a registry of all static methods annotated with @InvokableAction.
+@SupportedSourceVersion(SourceVersion.RELEASE_17)
+@SupportedAnnotationTypes("jace.config.InvokableAction")
+public class InvokableActionAnnotationProcessor extends AbstractProcessor {
+ Messager messager;
+ Map staticMethods = new HashMap<>();
+ Map instanceMethods = new HashMap<>();
+
+ @Override
+ public synchronized void init(ProcessingEnvironment processingEnv) {
+ super.init(processingEnv);
+ this.messager = processingEnv.getMessager();
+ messager.printMessage(javax.tools.Diagnostic.Kind.NOTE, "InvokableActionAnnotationProcessor init()");
+ }
+
+ @Override
+ public boolean process(Set extends TypeElement> annotations, RoundEnvironment roundEnv) {
+ messager.printMessage(javax.tools.Diagnostic.Kind.NOTE, "InvokableActionAnnotationProcessor process()");
+
+ // Get list of methods annotated with @InvokableAction.
+ Set extends Element> elements = roundEnv.getElementsAnnotatedWith(InvokableAction.class);
+ for (Element element : elements) {
+ if (element.getModifiers().contains(javax.lang.model.element.Modifier.STATIC)) {
+ // If the annotation method is static, add it to the static method registry.
+ trackStaticMethod(element);
+ } else {
+ // For non-static methods, track in a separate registry.
+ trackInstanceMethod(element);
+ }
+ try {
+ // Write class that contains static and instance methods.
+ writeRegistryClass();
+ } catch (IOException ex) {
+ messager.printMessage(javax.tools.Diagnostic.Kind.ERROR, "Error writing InvokableActionRegistry.java: " + ex.getMessage());
+ }
+ }
+ return true;
+ }
+
+ private void trackStaticMethod(Element element) {
+ // Store the method in the static method registry.
+ staticMethods.put(element.getAnnotation(InvokableAction.class), (ExecutableElement) element);
+ }
+
+ private void trackInstanceMethod(Element element) {
+ // Store the method in the instance method registry.
+ instanceMethods.put(element.getAnnotation(InvokableAction.class), (ExecutableElement) element);
+ }
+
+ private String serializeArrayOfStrings(String... strings) {
+ return Arrays.stream(strings).map(s -> "\"" + s + "\"").collect(Collectors.joining(","));
+ }
+
+ private void serializeInvokableAction(InvokableAction annotation, String variableName, PrintWriter writer) {
+ writer.append("""
+ %s = createInvokableAction("%s", "%s", "%s", "%s", %s, %s, new String[] {%s});
+ """.formatted(
+ variableName,
+ annotation.name(),
+ annotation.category(),
+ annotation.description(),
+ annotation.alternatives(),
+ annotation.consumeKeyEvent(),
+ annotation.notifyOnRelease(),
+ serializeArrayOfStrings(annotation.defaultKeyMapping())
+ ));
+ }
+
+ // Write the registry class.
+ private void writeRegistryClass() throws IOException {
+ Files.createDirectories(new File("target/generated-sources/jace/config").toPath());
+ try (PrintWriter writer = new PrintWriter(new FileWriter("target/generated-sources/jace/config/InvokableActionRegistryImpl.java"))) {
+ writer.write("""
+package jace.config;
+
+import java.util.logging.Level;
+
+public class InvokableActionRegistryImpl extends InvokableActionRegistry {
+ @Override
+ public void init() {
+ InvokableAction annotation;
+""");
+ for (Map.Entry entry : staticMethods.entrySet()) {
+ InvokableAction annotation = entry.getKey();
+ ExecutableElement method = entry.getValue();
+ String packageName = method.getEnclosingElement().getEnclosingElement().toString();
+ String className = method.getEnclosingElement().getSimpleName().toString();
+ String fqnClassName = packageName + "." + className;
+ serializeInvokableAction(annotation, "annotation", writer);
+ boolean takesBoolenParameter = method.getParameters().size() == 1 && method.getParameters().get(0).asType().toString().equalsIgnoreCase("boolean");
+ boolean returnsBoolean = method.getReturnType().toString().equalsIgnoreCase("boolean");
+ writer.write("""
+ putStaticAction(annotation.name(), %s.class, annotation, (b) -> {
+ try {
+ %s %s.%s(%s);
+ } catch (Exception ex) {
+ logger.log(Level.SEVERE, "Error invoking %s", ex);
+ %s
+ }
+ });
+ """.formatted(
+ fqnClassName,
+ returnsBoolean ? "return " : "",
+ fqnClassName,
+ method.getSimpleName(),
+ takesBoolenParameter ? "b" : "",
+ fqnClassName + "." + method.getSimpleName(),
+ returnsBoolean ? "return false;" : ""
+ ));
+ }
+
+ // Now for the instance methods, do the same except use a biconsumer which takes the instance as well as the boolean parameter.
+ for (Map.Entry entry : instanceMethods.entrySet()) {
+ InvokableAction annotation = entry.getKey();
+ ExecutableElement method = entry.getValue();
+ String packageName = method.getEnclosingElement().getEnclosingElement().toString();
+ String className = method.getEnclosingElement().getSimpleName().toString();
+ String fqnClassName = packageName + "." + className;
+ serializeInvokableAction(annotation, "annotation", writer);
+ boolean takesBoolenParameter = method.getParameters().size() == 1 && method.getParameters().get(0).asType().toString().equalsIgnoreCase("boolean");
+ boolean returnsBoolean = method.getReturnType().toString().equalsIgnoreCase("boolean");
+ writer.write("""
+ putInstanceAction(annotation.name(), %s.class, annotation, (o, b) -> {
+ try {
+ %s ((%s) o).%s(%s);
+ } catch (Exception ex) {
+ logger.log(Level.SEVERE, "Error invoking %s", ex);
+ %s
+ }
+ });
+ """.formatted(
+ fqnClassName,
+ returnsBoolean ? "return " : "",
+ fqnClassName,
+ method.getSimpleName(),
+ takesBoolenParameter ? "b" : "",
+ fqnClassName + "." + method.getSimpleName(),
+ returnsBoolean ? "return false;" : ""
+ ));
+ }
+ writer.write("}\n}");
+ }
+ }
+}
\ No newline at end of file
diff --git a/Platform/Apple/tools/jace/src/main/java/jace/config/InvokableActionRegistry.java b/Platform/Apple/tools/jace/src/main/java/jace/config/InvokableActionRegistry.java
new file mode 100644
index 00000000..c2fe9ff6
--- /dev/null
+++ b/Platform/Apple/tools/jace/src/main/java/jace/config/InvokableActionRegistry.java
@@ -0,0 +1,148 @@
+package jace.config;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.logging.Logger;
+
+public abstract class InvokableActionRegistry {
+ protected static final Logger logger = Logger.getLogger(InvokableActionRegistry.class.getName());
+ private final Map> staticMethodNames = new HashMap<>();
+ private final Map staticMethodInfo = new HashMap<>();
+ private final Map> staticMethodCallers = new HashMap<>();
+ private final Map> instanceMethodNames = new HashMap<>();
+ private final Map instanceMethodInfo = new HashMap<>();
+ private final Map> instanceMethodCallers = new HashMap<>();
+
+ protected static InvokableActionRegistry instance;
+
+ public static InvokableActionRegistry getInstance() {
+ if (instance == null) {
+ instance = new InvokableActionRegistryImpl();
+ instance.init();
+ }
+ return instance;
+ }
+
+
+ abstract public void init();
+
+ final public void putStaticAction(String name, Class c, InvokableAction action, Consumer caller) {
+ putStaticAction(name, c, action, (b) -> {
+ caller.accept(b);
+ return false;
+ });
+ }
+
+ final public void putStaticAction(String name, Class c, InvokableAction action, Function caller) {
+ staticMethodInfo.put(name, action);
+ staticMethodCallers.put(name, caller);
+ staticMethodNames.computeIfAbsent(c, k -> new TreeSet<>()).add(name);
+ }
+
+ public void putInstanceAction(String name, Class c, InvokableAction action, BiConsumer