diff --git a/viewpanel/src/com/spineview/ObjectPanel.java b/viewpanel/src/com/spineview/ObjectPanel.java index 0156897..2db3522 100644 --- a/viewpanel/src/com/spineview/ObjectPanel.java +++ b/viewpanel/src/com/spineview/ObjectPanel.java @@ -49,6 +49,7 @@ public final class ObjectPanel { java.util.List inspectFields = new ArrayList(); List objects = new ArrayList(); 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 getters = new ArrayList(); + java.util.List setters = new ArrayList(); + 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 BUTTONS = new java.util.LinkedHashMap(); @@ -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);