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 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 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 annotations, RoundEnvironment roundEnv) { + messager.printMessage(javax.tools.Diagnostic.Kind.NOTE, "InvokableActionAnnotationProcessor process()"); + + // Get list of methods annotated with @InvokableAction. + Set 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 caller) { + putInstanceAction(name, c, action, (o, b) -> { + caller.accept(o, b); + return false; + }); + } + + public void putInstanceAction(String name, Class c, InvokableAction action, BiFunction caller) { + instanceMethodInfo.put(name, action); + instanceMethodCallers.put(name, caller); + instanceMethodNames.computeIfAbsent(c, k -> new TreeSet<>()).add(name); + } + + public Set getStaticMethodNames(Class c) { + // Build a set of all the method names for this class and all its superclasses. + Set result = new TreeSet<>(); + Class current = c; + while (current != null) { + result.addAll(staticMethodNames.getOrDefault(current, Collections.EMPTY_SET)); + current = current.getSuperclass(); + } + return result; + } + + public Set getInstanceMethodNames(Class c) { + // Build a set of all the method names for this class and all its superclasses. + Set result = new TreeSet<>(); + Class current = c; + while (current != null) { + result.addAll(instanceMethodNames.getOrDefault(current, Collections.EMPTY_SET)); + current = current.getSuperclass(); + } + return result; + } + + public InvokableAction getStaticMethodInfo(String name) { + return staticMethodInfo.get(name); + } + + public InvokableAction getInstanceMethodInfo(String name) { + return instanceMethodInfo.get(name); + } + + public Function getStaticFunction(String name) { + return staticMethodCallers.get(name); + } + + public BiFunction getInstanceFunction(String name) { + return instanceMethodCallers.get(name); + } + + public Set getAllStaticActions() { + return new HashSet<>(staticMethodInfo.values()); + } + + protected InvokableAction createInvokableAction(String name, String category, String description, String alternatives, boolean consumeKeyEvent, boolean notifyOnRelease, String[] defaultKeyMapping) { + return new InvokableAction() { + @Override + public String name() { + return name; + } + + @Override + public String category() { + return category; + } + + @Override + public String description() { + return description; + } + + @Override + public String alternatives() { + return alternatives; + } + + @Override + public boolean consumeKeyEvent() { + return consumeKeyEvent; + } + + @Override + public boolean notifyOnRelease() { + return notifyOnRelease; + } + + @Override + public String[] defaultKeyMapping() { + return defaultKeyMapping; + } + + @Override + public Class annotationType() { + return InvokableAction.class; + } + }; + } +} \ No newline at end of file diff --git a/Platform/Apple/tools/jace/src/main/java/jace/config/InvokableActionRegistryImpl.java b/Platform/Apple/tools/jace/src/main/java/jace/config/InvokableActionRegistryImpl.java new file mode 100644 index 00000000..5b8d3053 --- /dev/null +++ b/Platform/Apple/tools/jace/src/main/java/jace/config/InvokableActionRegistryImpl.java @@ -0,0 +1,233 @@ +package jace.config; + +import java.util.logging.Level; + +// NOTE: This is generated code. Do not edit. +public class InvokableActionRegistryImpl extends InvokableActionRegistry { + + @Override + public void init() { + InvokableAction annotation; + annotation = createInvokableAction("Resize window", "general", "Resize the screen to 1x/1.5x/2x/3x video size", "Aspect;Adjust screen;Adjust window size;Adjust aspect ratio;Fix screen;Fix window size;Fix aspect ratio;Correct aspect ratio;", true, false, new String[]{"ctrl+shift+a"}); + putStaticAction(annotation.name(), jace.EmulatorUILogic.class, annotation, (b) -> { + try { + jace.EmulatorUILogic.scaleIntegerRatio(); + } catch (Exception ex) { + logger.log(Level.SEVERE, "Error invoking jace.EmulatorUILogic.scaleIntegerRatio", ex); + } + }); + annotation = createInvokableAction("Rewind", "General", "Go back 1 second", "Timewarp", true, false, new String[]{"ctrl+shift+Open Bracket"}); + putStaticAction(annotation.name(), jace.state.StateManager.class, annotation, (b) -> { + try { + jace.state.StateManager.beKindRewind(); + } catch (Exception ex) { + logger.log(Level.SEVERE, "Error invoking jace.state.StateManager.beKindRewind", ex); + } + }); + annotation = createInvokableAction("Configuration", "general", "Edit emulator configuraion", "Reconfigure;Preferences;Settings;Config", true, false, new String[]{"f4", "ctrl+shift+c"}); + putStaticAction(annotation.name(), jace.EmulatorUILogic.class, annotation, (b) -> { + try { + jace.EmulatorUILogic.showConfig(); + } catch (Exception ex) { + logger.log(Level.SEVERE, "Error invoking jace.EmulatorUILogic.showConfig", ex); + } + }); + annotation = createInvokableAction("Load settings", "general", "Load all configuration settings previously saved", "load preferences;revert settings;revert preferences", true, false, new String[]{"meta+ctrl+r"}); + putStaticAction(annotation.name(), jace.config.Configuration.class, annotation, (b) -> { + try { + jace.config.Configuration.loadSettings(); + } catch (Exception ex) { + logger.log(Level.SEVERE, "Error invoking jace.config.Configuration.loadSettings", ex); + } + }); + annotation = createInvokableAction("About", "general", "Display about window", "info;credits", true, false, new String[]{"ctrl+shift+."}); + putStaticAction(annotation.name(), jace.EmulatorUILogic.class, annotation, (b) -> { + try { + jace.EmulatorUILogic.showAboutWindow(); + } catch (Exception ex) { + logger.log(Level.SEVERE, "Error invoking jace.EmulatorUILogic.showAboutWindow", ex); + } + }); + annotation = createInvokableAction("Record sound", "sound", "Toggles recording (saving) sound output to a file", "", true, false, new String[]{"ctrl+shift+w"}); + putStaticAction(annotation.name(), jace.apple2e.Speaker.class, annotation, (b) -> { + try { + jace.apple2e.Speaker.toggleFileOutput(); + } catch (Exception ex) { + logger.log(Level.SEVERE, "Error invoking jace.apple2e.Speaker.toggleFileOutput", ex); + } + }); + annotation = createInvokableAction("BRUN file", "file", "Loads a binary file in memory and executes it. File should end with #06xxxx, where xxxx is the start address in hex", "Execute program;Load binary;Load program;Load rom;Play single-load game", true, false, new String[]{"ctrl+shift+b"}); + putStaticAction(annotation.name(), jace.EmulatorUILogic.class, annotation, (b) -> { + try { + jace.EmulatorUILogic.runFile(); + } catch (Exception ex) { + logger.log(Level.SEVERE, "Error invoking jace.EmulatorUILogic.runFile", ex); + } + }); + annotation = createInvokableAction("Save Raw Screenshot", "general", "Save raw (RAM) format of visible screen", "screendump;raw screenshot", true, false, new String[]{"ctrl+shift+z"}); + putStaticAction(annotation.name(), jace.EmulatorUILogic.class, annotation, (b) -> { + try { + jace.EmulatorUILogic.saveScreenshotRaw(); + } catch (Exception ex) { + logger.log(Level.SEVERE, "Error invoking jace.EmulatorUILogic.saveScreenshotRaw", ex); + } + }); + annotation = createInvokableAction("Save settings", "general", "Save all configuration settings as defaults", "save preferences;save defaults", true, false, new String[]{"meta+ctrl+s"}); + putStaticAction(annotation.name(), jace.config.Configuration.class, annotation, (b) -> { + try { + jace.config.Configuration.saveSettings(); + } catch (Exception ex) { + logger.log(Level.SEVERE, "Error invoking jace.config.Configuration.saveSettings", ex); + } + }); + annotation = createInvokableAction("Toggle fullscreen", "general", "Activate/deactivate fullscreen mode", "fullscreen;maximize", true, false, new String[]{"ctrl+shift+f"}); + putStaticAction(annotation.name(), jace.EmulatorUILogic.class, annotation, (b) -> { + try { + jace.EmulatorUILogic.toggleFullscreen(); + } catch (Exception ex) { + logger.log(Level.SEVERE, "Error invoking jace.EmulatorUILogic.toggleFullscreen", ex); + } + }); + annotation = createInvokableAction("Toggle Debug", "debug", "Show/hide the debug panel", "Show Debug;Hide Debug;Inspect", true, false, new String[]{"ctrl+shift+d"}); + putStaticAction(annotation.name(), jace.EmulatorUILogic.class, annotation, (b) -> { + try { + jace.EmulatorUILogic.toggleDebugPanel(); + } catch (Exception ex) { + logger.log(Level.SEVERE, "Error invoking jace.EmulatorUILogic.toggleDebugPanel", ex); + } + }); + annotation = createInvokableAction("Refresh screen", "display", "Marks screen contents as changed, forcing full screen redraw", "redraw", true, false, new String[]{"ctrl+shift+r"}); + putStaticAction(annotation.name(), jace.core.Video.class, annotation, (b) -> { + try { + jace.core.Video.forceRefresh(); + } catch (Exception ex) { + logger.log(Level.SEVERE, "Error invoking jace.core.Video.forceRefresh", ex); + } + }); + annotation = createInvokableAction("Toggle video mode", "video", "", "Gfx mode;color;b&w;monochrome", true, false, new String[]{"ctrl+shift+g"}); + putStaticAction(annotation.name(), jace.apple2e.VideoNTSC.class, annotation, (b) -> { + try { + jace.apple2e.VideoNTSC.changeVideoMode(); + } catch (Exception ex) { + logger.log(Level.SEVERE, "Error invoking jace.apple2e.VideoNTSC.changeVideoMode", ex); + } + }); + annotation = createInvokableAction("Save Screenshot", "general", "Save image of visible screen", "Save image;save framebuffer;screenshot", true, false, new String[]{"ctrl+shift+s"}); + putStaticAction(annotation.name(), jace.EmulatorUILogic.class, annotation, (b) -> { + try { + jace.EmulatorUILogic.saveScreenshot(); + } catch (Exception ex) { + logger.log(Level.SEVERE, "Error invoking jace.EmulatorUILogic.saveScreenshot", ex); + } + }); + annotation = createInvokableAction("Paste clipboard", "Keyboard", "", "paste", true, false, new String[]{"Ctrl+Shift+V", "Shift+Insert"}); + putStaticAction(annotation.name(), jace.core.Keyboard.class, annotation, (b) -> { + try { + jace.core.Keyboard.pasteFromClipboard(); + } catch (Exception ex) { + logger.log(Level.SEVERE, "Error invoking jace.core.Keyboard.pasteFromClipboard", ex); + } + }); + annotation = createInvokableAction("Open IDE", "development", "Open new IDE window for Basic/Assembly/Plasma coding", "IDE;dev;development;acme;assembler;editor", true, false, new String[]{"ctrl+shift+i"}); + putStaticAction(annotation.name(), jace.EmulatorUILogic.class, annotation, (b) -> { + try { + jace.EmulatorUILogic.showIDE(); + } catch (Exception ex) { + logger.log(Level.SEVERE, "Error invoking jace.EmulatorUILogic.showIDE", ex); + } + }); + annotation = createInvokableAction("Up", "joystick", "", "", true, true, new String[]{"up"}); + putInstanceAction(annotation.name(), jace.hardware.Joystick.class, annotation, (o, b) -> { + try { + return ((jace.hardware.Joystick) o).joystickUp(b); + } catch (Exception ex) { + logger.log(Level.SEVERE, "Error invoking jace.hardware.Joystick.joystickUp", ex); + return false; + } + }); + annotation = createInvokableAction("Open Apple Key", "Keyboard", "", "OA", false, true, new String[]{"Alt"}); + putInstanceAction(annotation.name(), jace.core.Keyboard.class, annotation, (o, b) -> { + try { + ((jace.core.Keyboard) o).openApple(b); + } catch (Exception ex) { + logger.log(Level.SEVERE, "Error invoking jace.core.Keyboard.openApple", ex); + } + }); + annotation = createInvokableAction("Left", "joystick", "", "", true, true, new String[]{"left"}); + putInstanceAction(annotation.name(), jace.hardware.Joystick.class, annotation, (o, b) -> { + try { + return ((jace.hardware.Joystick) o).joystickLeft(b); + } catch (Exception ex) { + logger.log(Level.SEVERE, "Error invoking jace.hardware.Joystick.joystickLeft", ex); + return false; + } + }); + annotation = createInvokableAction("Right", "joystick", "", "", true, true, new String[]{"right"}); + putInstanceAction(annotation.name(), jace.hardware.Joystick.class, annotation, (o, b) -> { + try { + return ((jace.hardware.Joystick) o).joystickRight(b); + } catch (Exception ex) { + logger.log(Level.SEVERE, "Error invoking jace.hardware.Joystick.joystickRight", ex); + return false; + } + }); + annotation = createInvokableAction("Closed Apple Key", "Keyboard", "", "CA", false, true, new String[]{"Shortcut", "Meta", "Command"}); + putInstanceAction(annotation.name(), jace.core.Keyboard.class, annotation, (o, b) -> { + try { + ((jace.core.Keyboard) o).solidApple(b); + } catch (Exception ex) { + logger.log(Level.SEVERE, "Error invoking jace.core.Keyboard.solidApple", ex); + } + }); + annotation = createInvokableAction("Down", "joystick", "", "", true, true, new String[]{"down"}); + putInstanceAction(annotation.name(), jace.hardware.Joystick.class, annotation, (o, b) -> { + try { + return ((jace.hardware.Joystick) o).joystickDown(b); + } catch (Exception ex) { + logger.log(Level.SEVERE, "Error invoking jace.hardware.Joystick.joystickDown", ex); + return false; + } + }); + annotation = createInvokableAction("Pause", "General", "Stops the computer, allowing reconfiguration of core elements", "freeze;halt", true, false, new String[]{"meta+pause", "alt+pause"}); + putInstanceAction(annotation.name(), jace.core.Computer.class, annotation, (o, b) -> { + try { + return ((jace.core.Computer) o).pause(); + } catch (Exception ex) { + logger.log(Level.SEVERE, "Error invoking jace.core.Computer.pause", ex); + return false; + } + }); + annotation = createInvokableAction("Cold boot", "general", "Process startup sequence from power-up", "Full reset;reset emulator", true, false, new String[]{"Ctrl+Shift+Backspace", "Ctrl+Shift+Delete"}); + putInstanceAction(annotation.name(), jace.core.Computer.class, annotation, (o, b) -> { + try { + ((jace.core.Computer) o).invokeColdStart(); + } catch (Exception ex) { + logger.log(Level.SEVERE, "Error invoking jace.core.Computer.invokeColdStart", ex); + } + }); + annotation = createInvokableAction("Warm boot", "general", "Process user-initatiated reboot (ctrl+apple+reset)", "reboot;reset;three-finger-salute;restart", true, false, new String[]{"Ctrl+Ignore Alt+Ignore Meta+Backspace", "Ctrl+Ignore Alt+Ignore Meta+Delete"}); + putInstanceAction(annotation.name(), jace.core.Computer.class, annotation, (o, b) -> { + try { + ((jace.core.Computer) o).invokeWarmStart(); + } catch (Exception ex) { + logger.log(Level.SEVERE, "Error invoking jace.core.Computer.invokeWarmStart", ex); + } + }); + annotation = createInvokableAction("Toggle Cheats", "General", "", "cheat;Plug-in", true, false, new String[]{"ctrl+shift+m"}); + putInstanceAction(annotation.name(), jace.cheat.Cheats.class, annotation, (o, b) -> { + try { + ((jace.cheat.Cheats) o).toggleCheats(); + } catch (Exception ex) { + logger.log(Level.SEVERE, "Error invoking jace.cheat.Cheats.toggleCheats", ex); + } + }); + annotation = createInvokableAction("Resume", "General", "Resumes the computer if it was previously paused", "unpause;unfreeze;resume;play", true, false, new String[]{"meta+shift+pause", "alt+shift+pause"}); + putInstanceAction(annotation.name(), jace.core.Computer.class, annotation, (o, b) -> { + try { + ((jace.core.Computer) o).resume(); + } catch (Exception ex) { + logger.log(Level.SEVERE, "Error invoking jace.core.Computer.resume", ex); + } + }); + } +} diff --git a/Platform/Apple/tools/jace/src/main/java/jace/core/Keyboard.java b/Platform/Apple/tools/jace/src/main/java/jace/core/Keyboard.java index 0e4a816c..2609dfdc 100644 --- a/Platform/Apple/tools/jace/src/main/java/jace/core/Keyboard.java +++ b/Platform/Apple/tools/jace/src/main/java/jace/core/Keyboard.java @@ -20,13 +20,12 @@ package jace.core; import java.io.IOException; import java.io.StringReader; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.Function; import java.util.logging.Level; import java.util.logging.Logger; @@ -96,8 +95,14 @@ public class Keyboard implements Reconfigurable { private static final Map> keyHandlersByKey = new HashMap<>(); private static final Map> keyHandlersByOwner = new HashMap<>(); - public static void registerInvokableAction(InvokableAction action, Object owner, Method method, String code) { - boolean isStatic = Modifier.isStatic(method.getModifiers()); + /** + * + * @param action + * @param owner + * @param method + * @param code + */ + public static void registerInvokableAction(InvokableAction action, Object owner, Function method, String code) { registerKeyHandler(new KeyHandler(code) { @Override public boolean handleKeyUp(KeyEvent e) { @@ -105,41 +110,40 @@ public class Keyboard implements Reconfigurable { if (action == null || !action.notifyOnRelease()) { return false; } -// System.out.println("Key up: "+method.toString()); - Object returnValue = null; - try { - if (method.getParameterCount() > 0) { - returnValue = method.invoke(isStatic ? null : owner, false); - } else { - returnValue = method.invoke(isStatic ? null : owner); - } - } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) { - Logger.getLogger(Keyboard.class.getName()).log(Level.SEVERE, null, ex); - } - if (returnValue != null) { - return (Boolean) returnValue; - } - return action.consumeKeyEvent(); + return method.apply(false) || action.consumeKeyEvent(); } @Override public boolean handleKeyDown(KeyEvent e) { // System.out.println("Key down: "+method.toString()); Emulator.withComputer(c -> c.getKeyboard().shiftPressed = e.isShiftDown()); - Object returnValue = null; - try { - if (method.getParameterCount() > 0) { - returnValue = method.invoke(isStatic ? null : owner, true); - } else { - returnValue = method.invoke(isStatic ? null : owner); - } - } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) { - Logger.getLogger(Keyboard.class.getName()).log(Level.SEVERE, null, ex); + if (action == null) { + return false; } - if (returnValue != null) { - return (Boolean) returnValue; + return method.apply(true) || action.consumeKeyEvent(); + } + }, owner); + } + + public static void registerInvokableAction(InvokableAction action, Object owner, BiFunction method, String code) { + registerKeyHandler(new KeyHandler(code) { + @Override + public boolean handleKeyUp(KeyEvent e) { + Emulator.withComputer(c -> c.getKeyboard().shiftPressed = e.isShiftDown()); + if (action == null || !action.notifyOnRelease()) { + return false; } - return action != null ? action.consumeKeyEvent() : null; + return method.apply(owner, false) || action.consumeKeyEvent(); + } + + @Override + public boolean handleKeyDown(KeyEvent e) { +// System.out.println("Key down: "+method.toString()); + Emulator.withComputer(c -> c.getKeyboard().shiftPressed = e.isShiftDown()); + if (action == null) { + return false; + } + return method.apply(owner, true) || action.consumeKeyEvent(); } }, owner); } diff --git a/Platform/Apple/tools/jace/src/main/java/jace/core/Utility.java b/Platform/Apple/tools/jace/src/main/java/jace/core/Utility.java index 7ebf5999..80a0c107 100644 --- a/Platform/Apple/tools/jace/src/main/java/jace/core/Utility.java +++ b/Platform/Apple/tools/jace/src/main/java/jace/core/Utility.java @@ -20,8 +20,6 @@ package jace.core; import java.io.File; import java.io.InputStream; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -30,12 +28,10 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; -import java.util.logging.Level; -import java.util.logging.Logger; +import java.util.function.Function; -import jace.config.Configuration; import jace.config.InvokableAction; +import jace.config.InvokableActionRegistry; import javafx.application.Platform; import javafx.geometry.Pos; import javafx.scene.control.Alert; @@ -406,38 +402,15 @@ public class Utility { return null; } - static final Map allActions = new ConcurrentHashMap<>(); - - public static Map getAllInvokableActions() { - synchronized (allActions) { - if (allActions.isEmpty()) { - Configuration.BASE.getTreeAsStream().forEach((Configuration.ConfigNode node) -> { - for (Method m : node.subject.getClass().getMethods()) { - if (m.isAnnotationPresent(InvokableAction.class)) { - allActions.put(m.getAnnotation(InvokableAction.class), () -> { - try { - m.invoke(node.subject); - } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) { - Logger.getLogger(Utility.class.getName()).log(Level.SEVERE, null, ex); - } - }); - } - } - }); - } - } - return allActions; - } - - public static Runnable getNamedInvokableAction(String action) { - Map actions = getAllInvokableActions(); - List actionsList = new ArrayList(actions.keySet()); + public static Function getNamedInvokableAction(String action) { + InvokableActionRegistry registry = InvokableActionRegistry.getInstance(); + List actionsList = new ArrayList(registry.getAllStaticActions()); actionsList.sort((a, b) -> Integer.compare(getActionNameMatch(action, a), getActionNameMatch(action, b))); // for (InvokableAction a : actionsList) { // String actionName = a.alternatives() == null ? a.name() : (a.name() + ";" + a.alternatives()); // System.out.println("Score for " + action + " evaluating " + a.name() + ": " + getActionNameMatch(action, a)); // } - return actions.get(actionsList.get(0)); + return registry.getStaticFunction(actionsList.get(0).name()); } private static int getActionNameMatch(String str, InvokableAction action) { diff --git a/Platform/Apple/tools/jace/src/main/java/module-info.java b/Platform/Apple/tools/jace/src/main/java/module-info.java index abc16d56..c9cb640a 100644 --- a/Platform/Apple/tools/jace/src/main/java/module-info.java +++ b/Platform/Apple/tools/jace/src/main/java/module-info.java @@ -21,6 +21,7 @@ module lawlesslegends { requires java.desktop; requires java.datatransfer; requires java.scripting; + requires static java.compiler; requires javafx.fxmlEmpty; requires javafx.fxml; requires javafx.swing; @@ -46,5 +47,8 @@ module lawlesslegends { uses javax.sound.sampled.SourceDataLine; + provides javax.annotation.processing.Processor with jace.config.InvokableActionAnnotationProcessor; + exports jace; + exports jace.config; }