修鼠标滚轮(第3版):面板按钮动作后把编辑器的滚动焦点抢到列表上

- 诊断证据:给列表 ScrollPane 挂 EventListener 代理后,滚轮事件收到 0 条 → 滚轮被视口的 scroll focus 拿走;
- grabScrollFocus():只在 **Stage 自己声明**的方法里找(addActor 声明在 Group,不会误调);
  setter=『1 个参数是 Actor、返回 void』,getter=『无参返回 Actor』;调 setter 后看哪个 getter 变成面板,
  找到就写日志『滚动焦点已抢到面板:setter=.. getter=..』,找不到写『没找到 setScrollFocus』。
- 触发时机:每次点击面板按钮/字段行之后(路由器里统一调用)。
This commit is contained in:
tianmo committed 2026-10-16 16:22:19 +08:00
1 parent 995ec585f4
commit 4b05804479
1 file changed
+129
@@ -49,6 +49,7 @@ public final class ObjectPanel {
java.util.List<Object[]> inspectFields = new ArrayList<Object[]>();
List<Object> objects = new ArrayList<Object>();
Object target;
boolean scrollFocusLogged;
Class<?> tableClass;
Class<?> scrollClass;
Class<?> actorClass;
@@ -61,6 +62,7 @@ public final class ObjectPanel {
*/
public static Object build(ClassLoader loader, Object skin) throws Exception {
final State state = new State();
LAST_STATE = state;
state.loader = loader;
state.skin = skin;
@@ -85,6 +87,7 @@ public final class ObjectPanel {
Object scroll = scrollClass.getConstructor(actorClass, skinClass).newInstance(state.listTable, skin);
state.listScroll = scroll;
makeTouchable(scroll);
attachEventProbe(scroll, "listScroll");
state.status = label(loader, skin, "Ready. Press Refresh to list scene objects.");
Object logScroll = null;
state.nameField = fieldClass.getConstructor(String.class, skinClass)
@@ -548,6 +551,128 @@ public final class ObjectPanel {
* 不靠名字定位:`Actor.getTouchable()` 是"**无参 + 返回枚举**"的那个方法;枚举第 0 个常量
* 就是 `Touchable.enabled`(libGDX 的声明顺序 enabled/disabled/childrenOnly)。
*/
/**
* 只读诊断:给某个 Actor 挂一个事件监听(`dn` = EventListener 接口的 **动态代理**),
* 把收到的每个事件打出来。用来确认"鼠标滚轮到底有没有送到面板"。
*
* 为什么要代理:载荷里的监听器都是匿名内部类,反射 new 不出来;
* 但 `dn` 是接口,用 `java.lang.reflect.Proxy` 就能造一个。
*/
/**
* 把编辑器的**滚动焦点**抢到面板的列表上(鼠标滚轮才会滚列表)。
*
* 识别方式(不猜名字、也不会误调 addActor):
* - 只在 **Stage 自己声明的**方法里找(`uk`/`dt`/`lpt6` 这些层),
* `addActor` 声明在 Group 上、不在这一层,所以不会被误选;
* - 候选 setter = 「1 个参数是 Actor 类型、返回 void」;候选 getter = 「无参、返回 Actor 类型」;
* - 调 setter 后看哪个 getter 变成了我们的面板 —— 那就是 setScrollFocus 那一对,日志会写出来。
*/
static void grabScrollFocus(final State state) {
try {
Object stage = SceneObjects.global(state.loader, "uk");
if (stage == null) stage = SceneObjects.global(state.loader, "dt");
if (stage == null || state.listScroll == null) return;
Class<?> actorClass = state.actorClass;
java.util.List<Method> getters = new ArrayList<Method>();
java.util.List<Method> setters = new ArrayList<Method>();
for (Class<?> type = stage.getClass(); type != null && type != Object.class;
type = type.getSuperclass()) {
if (type.getName().equals(Obf.GROUP)) break; // 到 Group 就停(addActor 在这一层)
for (Method method : type.getDeclaredMethods()) {
Class<?>[] types = method.getParameterTypes();
if (types.length == 0 && actorClass.isAssignableFrom(method.getReturnType())) {
getters.add(method);
} else if (types.length == 1 && types[0].isAssignableFrom(actorClass)
&& method.getReturnType() == void.class) {
setters.add(method);
}
}
if (type.getName().equals("dt")) continue;
}
for (Method setter : setters) {
try {
setter.setAccessible(true);
setter.invoke(stage, state.listScroll);
} catch (Throwable ignored) {
continue;
}
for (Method getter : getters) {
try {
getter.setAccessible(true);
if (getter.invoke(stage) == state.listScroll) {
if (state.scrollFocusLogged) return;
state.scrollFocusLogged = true;
System.err.println("[panel] 滚动焦点已抢到面板:setter=" + setter.getName()
+ " getter=" + getter.getName());
return;
}
} catch (Throwable ignored) {
// 下一个
}
}
}
if (!state.scrollFocusLogged) {
state.scrollFocusLogged = true;
System.err.println("[panel] 没找到 setScrollFocus(候选 setter " + setters.size()
+ " 个 / getter " + getters.size() + " 个)");
}
} catch (Throwable error) {
System.err.println("[panel] 抢滚动焦点失败: " + error);
}
}
static void attachEventProbe(final Object actor, final String tag) {
try {
Class<?> listenerClass = Class.forName("dn", true, actor.getClass().getClassLoader());
if (!listenerClass.isInterface()) {
System.err.println("[probe] dn 不是接口,跳过");
return;
}
Object listener = java.lang.reflect.Proxy.newProxyInstance(
listenerClass.getClassLoader(), new Class<?>[] {listenerClass},
new java.lang.reflect.InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) {
if (method.getName().equals("handle") && args != null && args.length == 1) {
Object event = args[0];
String name = event == null ? "null" : event.getClass().getName();
String type = event == null ? "" : String.valueOf(readField(event, "D"));
System.err.println("[probe] " + tag + " event=" + name + " typeField=" + type);
}
return null;
}
});
for (Method method : actor.getClass().getMethods()) {
Class<?>[] types = method.getParameterTypes();
if (types.length != 1 || types[0] != listenerClass) continue;
method.setAccessible(true);
Object result = method.invoke(actor, listener);
System.err.println("[probe] 已给 " + tag + " 挂事件监听: " + method.getName() + " -> " + result);
return;
}
System.err.println("[probe] 找不到 addListener");
} catch (Throwable error) {
System.err.println("[probe] 挂事件监听失败: " + error);
}
}
/** 读一个字段(诊断用,读不到返回 "?")。 */
static Object readField(Object target, String name) {
for (Class<?> type = target.getClass(); type != null && type != Object.class;
type = type.getSuperclass()) {
for (java.lang.reflect.Field field : type.getDeclaredFields()) {
if (!field.getName().equals(name)) continue;
try {
field.setAccessible(true);
return field.get(target);
} catch (Throwable ignored) {
return "?";
}
}
}
return "?";
}
static void makeTouchable(Object actor) {
try {
Class<?> enumType = null;
@@ -774,6 +899,8 @@ public final class ObjectPanel {
private static volatile boolean routerStarted;
/** 验证用:记住面板上的按钮(标签 → 按钮),探针可以按标签"点"它们。 */
private static volatile ObjectPanel.State LAST_STATE;
private static final java.util.Map<String, Object> BUTTONS =
new java.util.LinkedHashMap<String, Object>();
@@ -883,6 +1010,8 @@ public final class ObjectPanel {
}
try {
((Runnable) route[1]).run();
Object st = LAST_STATE;
if (st instanceof State) grabScrollFocus((State) st);
} catch (Throwable error) {
System.err.println("[panel] 动作失败: " + error);
error.printStackTrace(System.err);