diff --git a/viewpanel/src/com/spineview/ObjectPanel.java b/viewpanel/src/com/spineview/ObjectPanel.java index 557f315..d64d684 100644 --- a/viewpanel/src/com/spineview/ObjectPanel.java +++ b/viewpanel/src/com/spineview/ObjectPanel.java @@ -49,6 +49,8 @@ public final class ObjectPanel { java.util.List inspectFields = new ArrayList(); List objects = new ArrayList(); Object target; + Object root; + Object content; boolean scrollFocusLogged; boolean geometryDumped; Class tableClass; @@ -131,14 +133,21 @@ public final class ObjectPanel { Object cell = call(root, Obf.TABLE_ADD, new Class[] {actorClass}, cellClass, buttons); layoutCell(cell, Obf.CELL_LEFT); call(root, Obf.TABLE_ROW, new Class[0], cellClass); - // 中段:列表(唯一 expand+fill 的行,吃掉剩余高度) - cell = call(root, Obf.TABLE_ADD, new Class[] {actorClass}, cellClass, scroll); - layoutCell(cell, Obf.CELL_TOP, Obf.CELL_EXPAND, Obf.CELL_FILL); - state.listCell = cell; - // 下段:日志(独立一行,自然高度贴底;不用任何未验证的尺寸方法) - call(root, Obf.TABLE_ROW, new Class[0], cellClass); - cell = call(root, Obf.TABLE_ADD, new Class[] {actorClass}, cellClass, state.status); - layoutCell(cell, Obf.CELL_LEFT); + // 中段+下段放进**同一个竖直容器**:列表在上面 expand+fill(一直拉到日志上沿), + // 日志紧跟其下一行 —— 结构上保证"列表下边和日志区紧贴",不依赖任何高度猜测。 + Object content = tableClass.getConstructor().newInstance(); + call(content, Obf.TABLE_TOP, new Class[0], tableClass); + Object contentCell = call(content, Obf.TABLE_ADD, new Class[] {actorClass}, cellClass, scroll); + layoutCell(contentCell, Obf.CELL_TOP, Obf.CELL_EXPAND, Obf.CELL_FILL); + state.listCell = contentCell; + state.content = content; + state.root = root; + call(content, Obf.TABLE_ROW, new Class[0], cellClass); + Object logCell = call(content, Obf.TABLE_ADD, new Class[] {actorClass}, cellClass, + state.status); + layoutCell(logCell, Obf.CELL_LEFT); + cell = call(root, Obf.TABLE_ADD, new Class[] {actorClass}, cellClass, content); + layoutCell(cell, Obf.CELL_EXPAND, Obf.CELL_FILL); wire(loader, setValue, new Runnable() { @Override @@ -182,6 +191,67 @@ public final class ObjectPanel { }); startSelectionWatcher(state); + // 一次性诊断:等布局稳定后把 ScrollPane / 日志 Label 的几何打出来(只读,不改行为) + Thread measure = new Thread(new Runnable() { + @Override + public void run() { + try { + Thread.sleep(2500); + } catch (InterruptedException ignored) { + return; + } + StringBuilder a = new StringBuilder("[panel] 列表 ScrollPane 几何: "); + for (Method m : state.listScroll.getClass().getMethods()) { + if (m.getParameterTypes().length != 0 || m.getReturnType() != float.class) continue; + try { + m.setAccessible(true); + a.append(m.getName()).append('=').append(m.invoke(state.listScroll)).append(' '); + } catch (Throwable ignored) { + // 跳过 + } + } + StringBuilder b = new StringBuilder("[panel] 日志 Label 几何: "); + for (Method m : state.status.getClass().getMethods()) { + if (m.getParameterTypes().length != 0 || m.getReturnType() != float.class) continue; + try { + m.setAccessible(true); + b.append(m.getName()).append('=').append(m.invoke(state.status)).append(' '); + } catch (Throwable ignored) { + // 跳过 + } + } + // 同一方法名在 root/content/pane 上各读一遍:真正的 getWidth/getHeight 会给出"面板那么大"的数 + for (Method m : state.listScroll.getClass().getMethods()) { + if (m.getParameterTypes().length != 0 || m.getReturnType() != float.class) continue; + String name = m.getName(); + StringBuilder row = new StringBuilder("[panel] " + name + ": "); + Object[] targets = {state.root, state.content, state.listScroll, state.status}; + String[] tags = {"root", "content", "pane", "log"}; + for (int i = 0; i < targets.length; i++) { + if (targets[i] == null) continue; + try { + Method same = null; + for (Method candidate : targets[i].getClass().getMethods()) { + if (candidate.getName().equals(name) + && candidate.getParameterTypes().length == 0 + && candidate.getReturnType() == float.class) { + same = candidate; + break; + } + } + if (same == null) continue; + same.setAccessible(true); + row.append(tags[i]).append('=').append(same.invoke(targets[i])).append(' '); + } catch (Throwable ignored) { + // 跳过 + } + } + System.err.println(row.toString()); + } + } + }, "spineview-measure"); + measure.setDaemon(true); + measure.start(); System.err.println("[panel] 增删改查面板已建好"); refreshList(state); return root; @@ -406,7 +476,9 @@ public final class ObjectPanel { value = "<" + error.getClass().getSimpleName() + ">"; } final int rowIndex = lines.size(); - lines.add(" " + shortType(field.getType().getName()) + " " + field.getName() + String shown = realName(type.getName(), field.getName(), field.getType()); + lines.add(" " + shortType(field.getType().getName()) + " " + shown + + (shown.equals(field.getName()) ? "" : "(" + field.getName() + ")") + " = " + value + " @" + type.getName()); fieldRows.put(Integer.valueOf(rowIndex), new Object[] {object, field, value}); fields.add(new Object[] {object, field, value}); @@ -421,7 +493,15 @@ public final class ObjectPanel { if (params.length() > 0) params.append(", "); params.append(shortType(parameter.getName())); } - lines.add(" " + shortType(method.getReturnType().getName()) + " " + method.getName() + StringBuilder descriptor = new StringBuilder("("); + for (Class parameter : method.getParameterTypes()) { + descriptor.append(descriptorOf(parameter)); + } + descriptor.append(')').append(descriptorOf(method.getReturnType())); + String methodShown = realName(method.getDeclaringClass().getName(), method.getName(), + descriptor.toString()); + lines.add(" " + shortType(method.getReturnType().getName()) + " " + methodShown + + (methodShown.equals(method.getName()) ? "" : "(" + method.getName() + ")") + "(" + params + ")"); } fillRows(state, lines, fieldRows); @@ -928,8 +1008,70 @@ public final class ObjectPanel { private static volatile boolean routerStarted; /** 验证用:记住面板上的按钮(标签 → 按钮),探针可以按标签"点"它们。 */ + /** + * 混淆名 → 真名 的映射表(读 tools/deobf/names.tsv)。key = owner#混淆名#描述符。 + * + * 为什么需要它:编辑器**运行时**的类和成员名不能改(它的 indy 靠加密串里的原名反射查找), + * 所以反射拿到的是 `C/L/I/S/D…`;面板显示时用这张表翻译成有意义的名字。 + */ + private static final java.util.Map NAMES = + new java.util.HashMap(); + + /** 上一次 build 的 state(按钮路由里用来在动作后抢滚动焦点)。 */ private static volatile ObjectPanel.State LAST_STATE; + private static void loadNames() { + if (!NAMES.isEmpty()) return; + try { + String classes = System.getProperty("spine.classes"); + if (classes == null) return; + java.io.File root = new java.io.File(classes).getParentFile().getParentFile(); + java.io.File table = new java.io.File(root, "tools/deobf/names.tsv"); + if (!table.isFile()) { + System.err.println("[panel] 没找到映射表: " + table); + return; + } + for (String line : java.nio.file.Files.readAllLines(table.toPath(), + java.nio.charset.StandardCharsets.UTF_8)) { + if (line.isEmpty() || line.startsWith("#")) continue; + String[] parts = line.split("\t"); + if (parts.length < 4) continue; + NAMES.put(parts[0] + "#" + parts[1] + "#" + parts[2], parts[3]); + } + System.err.println("[panel] 已载入映射表 " + NAMES.size() + " 条:" + table); + } catch (Throwable error) { + System.err.println("[panel] 载入映射表失败: " + error); + } + } + + /** 类型的 JVM 描述符(用于查映射表)。 */ + private static String descriptorOf(Class type) { + if (type == void.class) return "V"; + if (type == boolean.class) return "Z"; + if (type == byte.class) return "B"; + if (type == char.class) return "C"; + if (type == short.class) return "S"; + if (type == int.class) return "I"; + if (type == long.class) return "J"; + if (type == float.class) return "F"; + if (type == double.class) return "D"; + if (type.isArray()) return "[" + descriptorOf(type.getComponentType()); + return "L" + type.getName().replace('.', '/') + ";"; + } + + /** 把"声明类 + 混淆名 + 类型"翻成真名;查不到就返回原名。 */ + static String realName(String owner, String obfName, Class type) { + loadNames(); + String hit = NAMES.get(owner + "#" + obfName + "#" + descriptorOf(type)); + return hit == null ? obfName : hit; + } + + static String realName(String owner, String obfName, String descriptor) { + loadNames(); + String hit = NAMES.get(owner + "#" + obfName + "#" + descriptor); + return hit == null ? obfName : hit; + } + private static final java.util.Map BUTTONS = new java.util.LinkedHashMap();