Fix Scenes freeze and Languages up/down navigation
- VsmxInterpreter.interpretFunction: save/restore pc/exit/stack/callStates so re-entrant event firing (FireEventAnim, timers, movie callbacks) no longer truncates the outer script. This is what left ss_btn_mm without focus (Scenes appeared frozen: no selection, input ignored while audio continued). - Add default geometric focus traversal for buttons with no direction handler: Controller.moveFocus picks the nearest visible focusable object. The PSP does this natively; English/Francais buttons have no onUp/onDown events in the RCO and relied on it. - Track page visibility (BaseObject.displayed, set by PageObject.open/close) so traversal skips closed pages and collapsed (alpha 0) columns. - Make BaseObject.trigger null-safe.
This commit is contained in:
@@ -30,6 +30,7 @@ public abstract class BaseObject extends BaseNativeObject implements IDisplay {
|
|||||||
protected Controller controller;
|
protected Controller controller;
|
||||||
private String name;
|
private String name;
|
||||||
private BaseNativeObject parent;
|
private BaseNativeObject parent;
|
||||||
|
private boolean displayed;
|
||||||
|
|
||||||
private Field[] getSortedFields() {
|
private Field[] getSortedFields() {
|
||||||
Field[] fields = getClass().getFields();
|
Field[] fields = getClass().getFields();
|
||||||
@@ -140,8 +141,17 @@ public abstract class BaseObject extends BaseNativeObject implements IDisplay {
|
|||||||
return name;
|
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) {
|
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());
|
controller.getInterpreter().interpretScript(getObject(), event.getEvent());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import dev.quoije.umd.rco.type.ImageType;
|
|||||||
import dev.quoije.umd.rco.type.IntType;
|
import dev.quoije.umd.rco.type.IntType;
|
||||||
import dev.quoije.umd.rco.type.TextType;
|
import dev.quoije.umd.rco.type.TextType;
|
||||||
import dev.quoije.umd.rco.vsmx.objects.BaseNativeObject;
|
import dev.quoije.umd.rco.vsmx.objects.BaseNativeObject;
|
||||||
|
import dev.quoije.umd.rco.vsmx.objects.Controller;
|
||||||
|
|
||||||
/** A button object. */
|
/** A button object. */
|
||||||
public class ButtonObject extends BasePositionObject {
|
public class ButtonObject extends BasePositionObject {
|
||||||
@@ -49,6 +50,14 @@ public class ButtonObject extends BasePositionObject {
|
|||||||
return focus.getImage();
|
return focus.getImage();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setController(Controller controller) {
|
||||||
|
super.setController(controller);
|
||||||
|
if (controller != null) {
|
||||||
|
controller.registerFocusable(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setFocus() {
|
public void setFocus() {
|
||||||
trigger(onFocusIn);
|
trigger(onFocusIn);
|
||||||
@@ -68,21 +77,37 @@ public class ButtonObject extends BasePositionObject {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onUp() {
|
public void onUp() {
|
||||||
|
if (onFocusUp != null && onFocusUp.getEvent() != null) {
|
||||||
trigger(onFocusUp);
|
trigger(onFocusUp);
|
||||||
|
} else if (controller != null) {
|
||||||
|
controller.moveFocus(this, 0, 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDown() {
|
public void onDown() {
|
||||||
|
if (onFocusDown != null && onFocusDown.getEvent() != null) {
|
||||||
trigger(onFocusDown);
|
trigger(onFocusDown);
|
||||||
|
} else if (controller != null) {
|
||||||
|
controller.moveFocus(this, 0, -1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLeft() {
|
public void onLeft() {
|
||||||
|
if (onFocusLeft != null && onFocusLeft.getEvent() != null) {
|
||||||
trigger(onFocusLeft);
|
trigger(onFocusLeft);
|
||||||
|
} else if (controller != null) {
|
||||||
|
controller.moveFocus(this, -1, 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onRight() {
|
public void onRight() {
|
||||||
|
if (onFocusRight != null && onFocusRight.getEvent() != null) {
|
||||||
trigger(onFocusRight);
|
trigger(onFocusRight);
|
||||||
|
} else if (controller != null) {
|
||||||
|
controller.moveFocus(this, 1, 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ public class PageObject extends BaseObject {
|
|||||||
VsmxBaseObject children = object.getPropertyValue(Resource.childrenName);
|
VsmxBaseObject children = object.getPropertyValue(Resource.childrenName);
|
||||||
for (String childName : children.getPropertyNames()) {
|
for (String childName : children.getPropertyNames()) {
|
||||||
VsmxBaseObject child = children.getPropertyValue(childName);
|
VsmxBaseObject child = children.getPropertyValue(childName);
|
||||||
|
setDisplayed(child, true);
|
||||||
display.add(child);
|
display.add(child);
|
||||||
display(child);
|
display(child);
|
||||||
}
|
}
|
||||||
@@ -47,11 +48,18 @@ public class PageObject extends BaseObject {
|
|||||||
VsmxBaseObject children = object.getPropertyValue(Resource.childrenName);
|
VsmxBaseObject children = object.getPropertyValue(Resource.childrenName);
|
||||||
for (String childName : children.getPropertyNames()) {
|
for (String childName : children.getPropertyNames()) {
|
||||||
VsmxBaseObject child = children.getPropertyValue(childName);
|
VsmxBaseObject child = children.getPropertyValue(childName);
|
||||||
|
setDisplayed(child, false);
|
||||||
display.remove(child);
|
display.remove(child);
|
||||||
hide(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) {
|
private BasePositionObject getFirstButton(VsmxBaseObject object) {
|
||||||
if (!object.hasPropertyValue(Resource.childrenName)) {
|
if (!object.hasPropertyValue(Resource.childrenName)) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import dev.quoije.umd.rco.type.EventType;
|
|||||||
import dev.quoije.umd.rco.type.ImageType;
|
import dev.quoije.umd.rco.type.ImageType;
|
||||||
import dev.quoije.umd.rco.type.IntType;
|
import dev.quoije.umd.rco.type.IntType;
|
||||||
import dev.quoije.umd.rco.vsmx.objects.BaseNativeObject;
|
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). */
|
/** A user button object (navigation with onLeft/onRight/onUp/onDown events). */
|
||||||
public class UButtonObject extends BasePositionObject {
|
public class UButtonObject extends BasePositionObject {
|
||||||
@@ -39,6 +40,14 @@ public class UButtonObject extends BasePositionObject {
|
|||||||
onDisplayUpdated();
|
onDisplayUpdated();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setController(Controller controller) {
|
||||||
|
super.setController(controller);
|
||||||
|
if (controller != null) {
|
||||||
|
controller.registerFocusable(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setFocus() {
|
public void setFocus() {
|
||||||
trigger(onFocusIn);
|
trigger(onFocusIn);
|
||||||
@@ -58,21 +67,37 @@ public class UButtonObject extends BasePositionObject {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onUp() {
|
public void onUp() {
|
||||||
|
if (onUp != null && onUp.getEvent() != null) {
|
||||||
trigger(onUp);
|
trigger(onUp);
|
||||||
|
} else if (controller != null) {
|
||||||
|
controller.moveFocus(this, 0, 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDown() {
|
public void onDown() {
|
||||||
|
if (onDown != null && onDown.getEvent() != null) {
|
||||||
trigger(onDown);
|
trigger(onDown);
|
||||||
|
} else if (controller != null) {
|
||||||
|
controller.moveFocus(this, 0, -1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLeft() {
|
public void onLeft() {
|
||||||
|
if (onLeft != null && onLeft.getEvent() != null) {
|
||||||
trigger(onLeft);
|
trigger(onLeft);
|
||||||
|
} else if (controller != null) {
|
||||||
|
controller.moveFocus(this, -1, 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onRight() {
|
public void onRight() {
|
||||||
|
if (onRight != null && onRight.getEvent() != null) {
|
||||||
trigger(onRight);
|
trigger(onRight);
|
||||||
|
} else if (controller != null) {
|
||||||
|
controller.moveFocus(this, 1, 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -498,6 +498,19 @@ public final class VsmxInterpreter {
|
|||||||
|
|
||||||
/** Interprets a function body in a fresh call frame. */
|
/** Interprets a function body in a fresh call frame. */
|
||||||
public synchronized void interpretFunction(VsmxFunction function, VsmxBaseObject object, VsmxBaseObject[] arguments) {
|
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<VsmxBaseObject> savedStack = stack;
|
||||||
|
Deque<VsmxCallState> savedCallStates = callStates;
|
||||||
|
VsmxCallState savedCallState = callState;
|
||||||
|
|
||||||
|
callStates = new ArrayDeque<>();
|
||||||
|
stack = null;
|
||||||
|
callState = null;
|
||||||
pushCallState(object, function.getLocalVars(), false, true);
|
pushCallState(object, function.getLocalVars(), false, true);
|
||||||
for (int i = 1; i <= function.getArgs(); i++) {
|
for (int i = 1; i <= function.getArgs(); i++) {
|
||||||
if (arguments == null || i > arguments.length) {
|
if (arguments == null || i > arguments.length) {
|
||||||
@@ -508,6 +521,12 @@ public final class VsmxInterpreter {
|
|||||||
}
|
}
|
||||||
pc = function.getStartLine();
|
pc = function.getStartLine();
|
||||||
interpret();
|
interpret();
|
||||||
|
|
||||||
|
pc = savedPc;
|
||||||
|
exit = savedExit;
|
||||||
|
stack = savedStack;
|
||||||
|
callStates = savedCallStates;
|
||||||
|
callState = savedCallState;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Interprets a script event handler by name (e.g. "script:/main/onPush"). */
|
/** Interprets a script event handler by name (e.g. "script:/main/onPush"). */
|
||||||
|
|||||||
@@ -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.VsmxNull;
|
||||||
import dev.quoije.umd.rco.vsmx.interpreter.VsmxString;
|
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. */
|
/** The controller native object: input focus and resource management. */
|
||||||
public class Controller extends BaseNativeObject {
|
public class Controller extends BaseNativeObject {
|
||||||
|
|
||||||
@@ -20,6 +23,7 @@ public class Controller extends BaseNativeObject {
|
|||||||
private VsmxBaseObject userData;
|
private VsmxBaseObject userData;
|
||||||
private String resource;
|
private String resource;
|
||||||
private BasePositionObject focus;
|
private BasePositionObject focus;
|
||||||
|
private final List<BasePositionObject> focusables = new ArrayList<>();
|
||||||
|
|
||||||
public static VsmxNativeObject create(VsmxInterpreter interpreter, MenuHost menuHost, String resource) {
|
public static VsmxNativeObject create(VsmxInterpreter interpreter, MenuHost menuHost, String resource) {
|
||||||
Controller controller = new Controller(interpreter, menuHost);
|
Controller controller = new Controller(interpreter, menuHost);
|
||||||
@@ -79,6 +83,63 @@ public class Controller extends BaseNativeObject {
|
|||||||
return focus;
|
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() {
|
public void onUp() {
|
||||||
if (focus != null) {
|
if (focus != null) {
|
||||||
focus.onUp();
|
focus.onUp();
|
||||||
|
|||||||
Reference in New Issue
Block a user