diff --git a/core/src/main/java/dev/quoije/umd/rco/object/BaseObject.java b/core/src/main/java/dev/quoije/umd/rco/object/BaseObject.java index 46b368b..84738cd 100644 --- a/core/src/main/java/dev/quoije/umd/rco/object/BaseObject.java +++ b/core/src/main/java/dev/quoije/umd/rco/object/BaseObject.java @@ -30,6 +30,7 @@ public abstract class BaseObject extends BaseNativeObject implements IDisplay { protected Controller controller; private String name; private BaseNativeObject parent; + private boolean displayed; private Field[] getSortedFields() { Field[] fields = getClass().getFields(); @@ -140,8 +141,17 @@ public abstract class BaseObject extends BaseNativeObject implements IDisplay { return name; } + /** True while this object is part of an opened page's display list. */ + public boolean isDisplayed() { + return displayed; + } + + public void setDisplayed(boolean displayed) { + this.displayed = displayed; + } + protected void trigger(EventType event) { - if (event.getEvent() != null && controller != null) { + if (event != null && event.getEvent() != null && controller != null) { controller.getInterpreter().interpretScript(getObject(), event.getEvent()); } } diff --git a/core/src/main/java/dev/quoije/umd/rco/object/ButtonObject.java b/core/src/main/java/dev/quoije/umd/rco/object/ButtonObject.java index 611e65b..dfaa183 100644 --- a/core/src/main/java/dev/quoije/umd/rco/object/ButtonObject.java +++ b/core/src/main/java/dev/quoije/umd/rco/object/ButtonObject.java @@ -7,6 +7,7 @@ import dev.quoije.umd.rco.type.ImageType; import dev.quoije.umd.rco.type.IntType; import dev.quoije.umd.rco.type.TextType; import dev.quoije.umd.rco.vsmx.objects.BaseNativeObject; +import dev.quoije.umd.rco.vsmx.objects.Controller; /** A button object. */ public class ButtonObject extends BasePositionObject { @@ -49,6 +50,14 @@ public class ButtonObject extends BasePositionObject { return focus.getImage(); } + @Override + public void setController(Controller controller) { + super.setController(controller); + if (controller != null) { + controller.registerFocusable(this); + } + } + @Override public void setFocus() { trigger(onFocusIn); @@ -68,21 +77,37 @@ public class ButtonObject extends BasePositionObject { @Override public void onUp() { - trigger(onFocusUp); + if (onFocusUp != null && onFocusUp.getEvent() != null) { + trigger(onFocusUp); + } else if (controller != null) { + controller.moveFocus(this, 0, 1); + } } @Override public void onDown() { - trigger(onFocusDown); + if (onFocusDown != null && onFocusDown.getEvent() != null) { + trigger(onFocusDown); + } else if (controller != null) { + controller.moveFocus(this, 0, -1); + } } @Override public void onLeft() { - trigger(onFocusLeft); + if (onFocusLeft != null && onFocusLeft.getEvent() != null) { + trigger(onFocusLeft); + } else if (controller != null) { + controller.moveFocus(this, -1, 0); + } } @Override public void onRight() { - trigger(onFocusRight); + if (onFocusRight != null && onFocusRight.getEvent() != null) { + trigger(onFocusRight); + } else if (controller != null) { + controller.moveFocus(this, 1, 0); + } } } diff --git a/core/src/main/java/dev/quoije/umd/rco/object/PageObject.java b/core/src/main/java/dev/quoije/umd/rco/object/PageObject.java index 0f6959e..0b5b46b 100644 --- a/core/src/main/java/dev/quoije/umd/rco/object/PageObject.java +++ b/core/src/main/java/dev/quoije/umd/rco/object/PageObject.java @@ -35,6 +35,7 @@ public class PageObject extends BaseObject { VsmxBaseObject children = object.getPropertyValue(Resource.childrenName); for (String childName : children.getPropertyNames()) { VsmxBaseObject child = children.getPropertyValue(childName); + setDisplayed(child, true); display.add(child); display(child); } @@ -47,11 +48,18 @@ public class PageObject extends BaseObject { VsmxBaseObject children = object.getPropertyValue(Resource.childrenName); for (String childName : children.getPropertyNames()) { VsmxBaseObject child = children.getPropertyValue(childName); + setDisplayed(child, false); display.remove(child); hide(child); } } + private static void setDisplayed(VsmxBaseObject object, boolean displayed) { + if (object instanceof VsmxNativeObject nativeObject && nativeObject.getObject() instanceof BaseObject baseObject) { + baseObject.setDisplayed(displayed); + } + } + private BasePositionObject getFirstButton(VsmxBaseObject object) { if (!object.hasPropertyValue(Resource.childrenName)) { return null; diff --git a/core/src/main/java/dev/quoije/umd/rco/object/UButtonObject.java b/core/src/main/java/dev/quoije/umd/rco/object/UButtonObject.java index 08869ca..5833746 100644 --- a/core/src/main/java/dev/quoije/umd/rco/object/UButtonObject.java +++ b/core/src/main/java/dev/quoije/umd/rco/object/UButtonObject.java @@ -6,6 +6,7 @@ import dev.quoije.umd.rco.type.EventType; import dev.quoije.umd.rco.type.ImageType; import dev.quoije.umd.rco.type.IntType; import dev.quoije.umd.rco.vsmx.objects.BaseNativeObject; +import dev.quoije.umd.rco.vsmx.objects.Controller; /** A user button object (navigation with onLeft/onRight/onUp/onDown events). */ public class UButtonObject extends BasePositionObject { @@ -39,6 +40,14 @@ public class UButtonObject extends BasePositionObject { onDisplayUpdated(); } + @Override + public void setController(Controller controller) { + super.setController(controller); + if (controller != null) { + controller.registerFocusable(this); + } + } + @Override public void setFocus() { trigger(onFocusIn); @@ -58,21 +67,37 @@ public class UButtonObject extends BasePositionObject { @Override public void onUp() { - trigger(onUp); + if (onUp != null && onUp.getEvent() != null) { + trigger(onUp); + } else if (controller != null) { + controller.moveFocus(this, 0, 1); + } } @Override public void onDown() { - trigger(onDown); + if (onDown != null && onDown.getEvent() != null) { + trigger(onDown); + } else if (controller != null) { + controller.moveFocus(this, 0, -1); + } } @Override public void onLeft() { - trigger(onLeft); + if (onLeft != null && onLeft.getEvent() != null) { + trigger(onLeft); + } else if (controller != null) { + controller.moveFocus(this, -1, 0); + } } @Override public void onRight() { - trigger(onRight); + if (onRight != null && onRight.getEvent() != null) { + trigger(onRight); + } else if (controller != null) { + controller.moveFocus(this, 1, 0); + } } } diff --git a/core/src/main/java/dev/quoije/umd/rco/vsmx/interpreter/VsmxInterpreter.java b/core/src/main/java/dev/quoije/umd/rco/vsmx/interpreter/VsmxInterpreter.java index af68d87..72294b8 100644 --- a/core/src/main/java/dev/quoije/umd/rco/vsmx/interpreter/VsmxInterpreter.java +++ b/core/src/main/java/dev/quoije/umd/rco/vsmx/interpreter/VsmxInterpreter.java @@ -498,6 +498,19 @@ public final class VsmxInterpreter { /** Interprets a function body in a fresh call frame. */ public synchronized void interpretFunction(VsmxFunction function, VsmxBaseObject object, VsmxBaseObject[] arguments) { + // This entry point can be invoked re-entrantly (FireEventAnim, timers, + // movie callbacks all run from inside an already-running script), so the + // outer execution state must be preserved and restored. Without this, + // the inner RETURN would set {@code exit} and truncate the outer script. + int savedPc = pc; + boolean savedExit = exit; + Deque savedStack = stack; + Deque savedCallStates = callStates; + VsmxCallState savedCallState = callState; + + callStates = new ArrayDeque<>(); + stack = null; + callState = null; pushCallState(object, function.getLocalVars(), false, true); for (int i = 1; i <= function.getArgs(); i++) { if (arguments == null || i > arguments.length) { @@ -508,6 +521,12 @@ public final class VsmxInterpreter { } pc = function.getStartLine(); interpret(); + + pc = savedPc; + exit = savedExit; + stack = savedStack; + callStates = savedCallStates; + callState = savedCallState; } /** Interprets a script event handler by name (e.g. "script:/main/onPush"). */ diff --git a/core/src/main/java/dev/quoije/umd/rco/vsmx/objects/Controller.java b/core/src/main/java/dev/quoije/umd/rco/vsmx/objects/Controller.java index 759c311..f0027ef 100644 --- a/core/src/main/java/dev/quoije/umd/rco/vsmx/objects/Controller.java +++ b/core/src/main/java/dev/quoije/umd/rco/vsmx/objects/Controller.java @@ -10,6 +10,9 @@ import dev.quoije.umd.rco.vsmx.interpreter.VsmxNativeObject; import dev.quoije.umd.rco.vsmx.interpreter.VsmxNull; import dev.quoije.umd.rco.vsmx.interpreter.VsmxString; +import java.util.ArrayList; +import java.util.List; + /** The controller native object: input focus and resource management. */ public class Controller extends BaseNativeObject { @@ -20,6 +23,7 @@ public class Controller extends BaseNativeObject { private VsmxBaseObject userData; private String resource; private BasePositionObject focus; + private final List focusables = new ArrayList<>(); public static VsmxNativeObject create(VsmxInterpreter interpreter, MenuHost menuHost, String resource) { Controller controller = new Controller(interpreter, menuHost); @@ -79,6 +83,63 @@ public class Controller extends BaseNativeObject { return focus; } + /** Registers a focusable object so default (geometric) focus traversal can reach it. */ + public void registerFocusable(BasePositionObject object) { + synchronized (focusables) { + if (!focusables.contains(object)) { + focusables.add(object); + } + } + } + + /** + * Moves focus to the nearest visible, focusable object in the given direction + * when a button has no script handler for that direction. {@code dx}/{@code dy} + * use the framebuffer coordinate system (+X right, +Y up). + */ + public void moveFocus(BasePositionObject from, int dx, int dy) { + int fromX = from.getX() + from.getWidth() / 2; + int fromY = from.getY() + from.getHeight() / 2; + + BasePositionObject best = null; + double bestScore = Double.MAX_VALUE; + synchronized (focusables) { + for (BasePositionObject candidate : focusables) { + if (candidate == from || !candidate.isDisplayed() || candidate.getAlpha() <= 0f + || candidate.getImage() == null) { + continue; + } + int candidateX = candidate.getX() + candidate.getWidth() / 2; + int candidateY = candidate.getY() + candidate.getHeight() / 2; + int offsetX = candidateX - fromX; + int offsetY = candidateY - fromY; + if (dy < 0 && offsetY >= 0) { + continue; + } + if (dy > 0 && offsetY <= 0) { + continue; + } + if (dx < 0 && offsetX >= 0) { + continue; + } + if (dx > 0 && offsetX <= 0) { + continue; + } + double primary = dx != 0 ? java.lang.Math.abs(offsetX) : java.lang.Math.abs(offsetY); + double secondary = dx != 0 ? java.lang.Math.abs(offsetY) : java.lang.Math.abs(offsetX); + double score = primary * 10 + secondary; + if (score < bestScore) { + bestScore = score; + best = candidate; + } + } + } + + if (best != null) { + best.setFocus(); + } + } + public void onUp() { if (focus != null) { focus.onUp();