diff --git a/viewpanel/src/com/spineview/ObjectPanel.java b/viewpanel/src/com/spineview/ObjectPanel.java index b6216a5..e8adfd7 100644 --- a/viewpanel/src/com/spineview/ObjectPanel.java +++ b/viewpanel/src/com/spineview/ObjectPanel.java @@ -43,6 +43,10 @@ public final class ObjectPanel { final java.util.Deque log = new java.util.ArrayDeque(); boolean rendering; boolean logBlockStarted; + Object inspected; // 当前 Inspect 的对象 + java.lang.reflect.Field fieldTarget; // 被点选的字段(可写值) + Object fieldOwner; + java.util.List inspectFields = new ArrayList(); List objects = new ArrayList(); Object target; } @@ -95,9 +99,10 @@ public final class ObjectPanel { Object delete = button(loader, skin, "Delete"); Object rename = button(loader, skin, "Rename"); Object inspect = button(loader, skin, "Inspect"); + Object setValue = button(loader, skin, "Set Value"); // 按钮直接放进根表格:两行按钮 + 一行滚动列表(与阶段四验证过的结构一致) - Object[] top = {refresh, newBone, newSlot, newBBox, newPath, delete, rename, inspect}; + Object[] top = {refresh, newBone, newSlot, newBBox, newPath, delete, rename, inspect, setValue}; for (int i = 0; i < top.length; i++) { Object cell = call(root, Obf.TABLE_ADD, new Class[] {actorClass}, cellClass, top[i]); layoutCell(cell, Obf.CELL_LEFT); @@ -142,6 +147,12 @@ public final class ObjectPanel { create(state, "Path"); } }); + wire(loader, setValue, new Runnable() { + @Override + public void run() { + applyFieldValue(state); + } + }); wire(loader, inspect, new Runnable() { @Override public void run() { @@ -177,6 +188,7 @@ public final class ObjectPanel { } }); + startSelectionWatcher(state); System.err.println("[panel] 增删改查面板已建好"); refreshList(state); return root; @@ -343,9 +355,14 @@ public final class ObjectPanel { return; } List lines = new ArrayList(); + final java.util.Map fieldRows = + new java.util.HashMap(); lines.add("selected: " + SceneObjects.kind(object) + " \"" + SceneObjects.nameOf(object) + "\" (" + object.getClass().getName() + ")"); - lines.add("--- fields (" + countFields(object) + ") ---"); + state.inspected = object; + state.fieldTarget = null; + lines.add("--- fields (" + countFields(object) + ") --- (点一行字段可改值)"); + final java.util.List fields = new ArrayList(); // {owner, field, valueText} for (Class type = object.getClass(); type != null && type != Object.class; type = type.getSuperclass()) { for (java.lang.reflect.Field field : type.getDeclaredFields()) { @@ -360,10 +377,14 @@ public final class ObjectPanel { } catch (Throwable error) { value = "<" + error.getClass().getSimpleName() + ">"; } + final int rowIndex = lines.size(); lines.add(" " + shortType(field.getType().getName()) + " " + field.getName() + " = " + value + " @" + type.getName()); + fieldRows.put(Integer.valueOf(rowIndex), new Object[] {object, field, value}); + fields.add(new Object[] {object, field, value}); } } + state.inspectFields = fields; lines.add("--- methods ---"); for (Method method : object.getClass().getMethods()) { if (method.getDeclaringClass() == Object.class) continue; @@ -375,13 +396,95 @@ public final class ObjectPanel { lines.add(" " + shortType(method.getReturnType().getName()) + " " + method.getName() + "(" + params + ")"); } - fillTextRows(state, lines); + fillRows(state, lines, fieldRows); setStatus(state, "Inspected " + SceneObjects.kind(object) + " \"" + SceneObjects.nameOf(object) + "\": " + (lines.size() - 2) + " entries"); } }); } + /** + * 把输入框里的值写进"被点选的字段":直接反射改模型字段 + 包一次撤销 + 重建树节点, + * 然后重新 Inspect 一遍(值会立刻显出来)——这就是"改一个值看哪个字段动"的实验入口。 + */ + private static void applyFieldValue(final State state) { + final java.lang.reflect.Field field = state.fieldTarget; + final Object owner = state.fieldOwner; + final String text = textFieldText(state.nameField); + if (field == null || owner == null) { + setStatus(state, "Set Value: click a field row first."); + return; + } + SceneActions.onEditorThread(state.loader, new Runnable() { + @Override + public void run() { + try { + field.setAccessible(true); + Class type = field.getType(); + Object value; + if (type == float.class) value = Float.valueOf(Float.parseFloat(text.trim())); + else if (type == int.class) value = Integer.valueOf(Integer.parseInt(text.trim())); + else if (type == boolean.class) value = Boolean.valueOf(Boolean.parseBoolean(text.trim())); + else if (type == double.class) value = Double.valueOf(Double.parseDouble(text.trim())); + else if (type == String.class) value = text; + else { + setStatus(state, "Set Value: unsupported type " + type.getName()); + return; + } + Object skeleton = SceneActions.skeletonOf(owner); + Class undo = Class.forName("un", true, state.loader); + java.lang.reflect.Method begin = null, commit = null; + for (Method m : undo.getDeclaredMethods()) { + if (!java.lang.reflect.Modifier.isStatic(m.getModifiers())) continue; + if (m.getParameterTypes().length != 0) continue; + if (m.getReturnType() != void.class) continue; + if (m.getName().equals("D")) begin = m; + if (m.getName().equals("B")) commit = m; + } + if (begin != null) begin.invoke(null); + field.set(owner, value); + if (commit != null) commit.invoke(null); + SceneActions.refresh(state.loader, skeleton); + setStatus(state, "Set " + field.getName() + " = " + text); + } catch (Throwable error) { + setStatus(state, "Set Value failed: " + error); + error.printStackTrace(System.err); + } + inspect(state); + } + }); + } + + /** 跟随编辑器里的选择:变了就自动重新 Inspect(做成"实时显示数据")。 */ + private static void startSelectionWatcher(final State state) { + Thread thread = new Thread(new Runnable() { + @Override + public void run() { + Object last = null; + while (true) { + try { + Object current = SceneObjects.selected(state.loader); + if (current != null && current != last) { + last = current; + inspect(state); + } + Thread.sleep(600); + } catch (InterruptedException ignored) { + return; + } catch (Throwable error) { + try { + Thread.sleep(600); + } catch (InterruptedException ignored) { + return; + } + } + } + } + }, "spineview-selection-watch"); + thread.setDaemon(true); + thread.start(); + } + private static int countFields(Object object) { int count = 0; for (Class type = object.getClass(); type != null && type != Object.class; @@ -418,6 +521,68 @@ public final class ObjectPanel { } } + /** + * 填列表区:普通行是只读文本;`clickable` 里的行做成可点按钮—— + * 点一下 = 选中该字段(把当前值读进输入框),配合 `Set Value` 就能改值做实验。 + */ + private static void fillRows(final State state, List lines, + java.util.Map clickable) { + try { + call(state.listTable, Obf.TABLE_CLEAR_CHILDREN, new Class[0], void.class); + state.logBlockStarted = false; + Class cellClass = Class.forName(Obf.CELL, true, state.loader); + Class actorClass = Class.forName(Obf.ACTOR, true, state.loader); + for (int i = 0; i < lines.size(); i++) { + final Object[] target = clickable.get(Integer.valueOf(i)); + Object row = button(state.loader, state.skin, lines.get(i)); + Object cell = call(state.listTable, Obf.TABLE_ADD, new Class[] {actorClass}, + cellClass, row); + layoutCell(cell, Obf.CELL_LEFT); + call(state.listTable, Obf.TABLE_ROW, new Class[0], cellClass); + if (target != null) { + final String valueText = String.valueOf(target[2]); + wire(state.loader, row, new Runnable() { + @Override + public void run() { + state.fieldOwner = target[0]; + state.fieldTarget = (java.lang.reflect.Field) target[1]; + setInputText(state, valueText); + setStatus(state, "Picked field " + state.fieldTarget.getName() + + " (value " + valueText + ") -- edit the name box, then Set Value"); + } + }); + } + } + state.rendering = false; + appendLogRows(state); + invalidateList(state); + } catch (Throwable error) { + System.err.println("[panel] 字段列表填充失败: " + error); + } + } + + /** 把文字写进面板顶部的输入框(改字段值用)。 */ + private static void setInputText(final State state, final String text) { + SceneActions.onEditorThread(state.loader, new Runnable() { + @Override + public void run() { + for (Method method : state.nameField.getClass().getMethods()) { + Class[] types = method.getParameterTypes(); + if (types.length != 1) continue; + if (method.getReturnType() != void.class) continue; + if (types[0] != String.class && types[0] != CharSequence.class) continue; + try { + method.setAccessible(true); + method.invoke(state.nameField, text); + return; + } catch (Throwable ignored) { + // 换下一个 + } + } + } + }); + } + /** 用纯文本行填列表区(行不可点)。 */ private static void fillTextRows(final State state, List lines) { try {