Files
Spine_lab/viewpanel/src/com/spineview/ViewInstaller.java
T
tianmo e197a6883a Spine View 启动自动打开(等价 Views 菜单勾选),不用每次手点
- ViewInstaller.showView():面板 Actor 填好后调 Fx.D(us.spineView) —— 菜单 'Tree View' 的动作体就是
  Fx.D(us.Tree)(og.java:23),是编辑器自己的'显示/停靠视图'入口。
- 为什么之前每次都要手点:视图可见状态写在 views.json,只有正常退出才落盘;而我们一直用 SIGTERM
  杀编辑器进程,状态没写回去。启动时主动开一次就绕过了这个。
- 列表区仍按设计需要点一次 Refresh(面板是编辑器 UI 搭好之前就建的,那时场景树还是空的)。
2026-10-16 15:36:42 +08:00

293 lines
12 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package com.spineview;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
/**
* 被 ViewPatcher 插进 tD.<init> 的调用目标 —— 必须由**编辑器的类加载器**加载,
* 所以运行时要把 viewpanel/build/classes 放进 URLClassLoader 的 URLs。
*
* 它的活:为新的 "Spine View" 视图建一个面板 Actor,塞进 us.SPINEVIEW.D。
* 之后编辑器的停靠/折叠框架就会像对待其它视图一样对待它。
*/
public final class ViewInstaller {
private static final String SKIN = "com.badlogic.gdx.scenes.scene2d.ui.Skin";
private static final String ACTOR = "dl";
private static final String LABEL = "com.badlogic.gdx.scenes.scene2d.ui.Label";
private static final String TEXT_BUTTON = "com.badlogic.gdx.scenes.scene2d.ui.TextButton";
private static final String SCROLL_PANE = "com.badlogic.gdx.scenes.scene2d.ui.ScrollPane";
private ViewInstaller() {
}
/** 判定实验用:只打印一行,不做任何别的事。 */
public static void ping() {
System.err.println("[view] ping 被调用");
}
public static void install() {
// 这是从 tD 的构造函数中途调进来的:这时候编辑器自己的资源(皮肤贴图走 obt: 协议)
// 还在读,我们如果在同一个调用里 Class.forName 触发别的类的初始化,会把流搞坏。
// 所以这里只起个后台线程,等构造函数跑完、界面搭好之后再真正安装。
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException ignored) {
return;
}
doInstall();
}
}, "spineview-view-installer");
thread.setDaemon(true);
thread.start();
}
private static void doInstall() {
try {
ClassLoader loader = ViewInstaller.class.getClassLoader();
installForLoader(loader);
} catch (Throwable error) {
System.err.println("[view] 安装失败");
error.printStackTrace(System.err);
}
}
/**
* 从编辑器外面调用也可以:只要把编辑器那个 URLClassLoader 传进来。
*
* 视图常量已经由 view-agent 注册进 us 枚举,这里只负责把面板 Actor 填进
* us.spineView.D —— 之后编辑器的停靠框架就会像对待其它视图一样对待它。
*/
public static boolean installForLoader(ClassLoader loader) {
try {
Class<?> viewEnum = Class.forName("us", true, loader);
Object view = viewEnum.getMethod("valueOf", String.class).invoke(null, "spineView");
Object skin = findSkin(loader);
if (skin == null) {
System.err.println("[view] 找不到 Skin");
return false;
}
Object panel = buildPanel(loader, skin);
for (Field field : viewEnum.getDeclaredFields()) {
if (!field.getType().getName().equals(ACTOR)) continue;
field.setAccessible(true);
field.set(view, panel);
System.err.println("[view] 已把 Spine View 面板挂到视图 " + view);
showView(loader, view);
return true;
}
System.err.println("[view] 视图枚举里找不到 Actor 字段");
} catch (Throwable error) {
System.err.println("[view] 安装失败(稍后重试): " + error);
}
return false;
}
/**
* 像 Views 菜单那样把视图"打开"一次,这样面板不用每次手动点开。
*
* 菜单里 "Tree View" 的动作体就一句:`Fx.D(us.Tree)`(`og.java:23`),
* `Fx` 是编辑器里那个静态 Actor 锚点类,`D(Object)` 就是"显示/停靠这个视图"。
* 面板的可见状态本来就会写进 `views.json`,但只有正常退出才会写;我们一直用
* SIGTERM 杀进程,所以每次都得手动点 —— 这里启动时主动开一次即可。
*/
private static void showView(ClassLoader loader, Object view) {
try {
Class<?> fx = Class.forName("Fx", true, loader);
for (Method method : fx.getDeclaredMethods()) {
if (!java.lang.reflect.Modifier.isStatic(method.getModifiers())) continue;
Class<?>[] parameters = method.getParameterTypes();
if (parameters.length != 1) continue;
if (!parameters[0].isAssignableFrom(view.getClass())) continue;
method.setAccessible(true);
method.invoke(null, view);
System.err.println("[view] 已自动打开 Spine View(等价于 Views 菜单里勾选)");
return;
}
System.err.println("[view] Fx 上没找到打开视图的方法,跳过自动打开");
} catch (Throwable error) {
System.err.println("[view] 自动打开视图失败: " + error);
}
}
/** Skin 挂在 OG 的静态字段上(字节码里 OG 有 LSkin;D)。 */
private static Object findSkin(ClassLoader loader) throws Exception {
Class<?> holder = Class.forName("OG", true, loader);
for (Field field : holder.getDeclaredFields()) {
if (!Modifier.isStatic(field.getModifiers())) continue;
if (!field.getType().getName().equals(SKIN)) continue;
field.setAccessible(true);
Object value = field.get(null);
if (value != null) return value;
}
return null;
}
/**
* 编辑器的停靠框架会按惯例去找 "icon-tab" + 视图名 的图标,找不到就直接抛异常把界面带崩。
* 所以要在它建面板之前,把我们的图标别名到已有视图(Tree)的图标上。
* 皮肤是在建面板之前填好的,这里用极短间隔轮询去抢这个时间窗。
*/
public static boolean aliasIcon(ClassLoader loader) {
try {
Class<?> skinClass = Class.forName(SKIN, true, loader);
// libGDX 的 Skin 是"按类型分桶"存的:add(name, obj) 用 obj.getClass() 当桶,
// 而 getDrawable(name) 是去 Drawable.class 这个桶里查 —— 两者对不上就会"找不到"。
// 所以要用三参数的 add(name, obj, Class),并且把该 drawable 的所有接口/父类都注册一遍。
Method addTyped = null;
for (Method method : skinClass.getMethods()) {
Class<?>[] types = method.getParameterTypes();
if (types.length == 3 && types[0] == String.class && types[1] == Object.class
&& types[2] == Class.class && method.getReturnType() == void.class) {
addTyped = method;
break;
}
}
if (addTyped == null) return false;
// 停靠框架会按状态取不同的图标名;后缀没对应的就退回基础图标,
// 反正图标是复用的,不新建任何图片资源。
String[] suffixes = { "", "-over", "-down", "-up", "-checked", "-unchecked",
"-disabled", "-inactive", "-active", "-on", "-off", "-selected", "-open",
"-closed", "-hover", "-pressed" };
for (int attempt = 0; attempt < 30000; attempt++) {
Object skin = findSkin(loader);
if (skin != null) {
boolean any = false;
Object base = getDrawable(skinClass, skin, "icon-tabTree");
if (base == null) continue;
for (String suffix : suffixes) {
Object icon = getDrawable(skinClass, skin, "icon-tabTree" + suffix);
if (icon == null) icon = base;
for (Class<?> type : bucketTypes(icon.getClass())) {
try {
addTyped.invoke(skin, "icon-tabSpineView" + suffix, icon, type);
} catch (Throwable ignored) {
// 某些类型不接受就跳过
}
}
any = true;
}
if (any) {
System.err.println("[view] 已给 SpineView 注册图标别名");
return true;
}
}
Thread.sleep(2);
}
} catch (Throwable error) {
System.err.println("[view] 图标别名失败: " + error);
}
return false;
}
/** drawable 的所有接口 + 父类 —— 把可能当桶用的类型都列出来。 */
private static java.util.List<Class<?>> bucketTypes(Class<?> type) {
java.util.List<Class<?>> list = new java.util.ArrayList<Class<?>>();
for (Class<?> current = type; current != null && current != Object.class;
current = current.getSuperclass()) {
list.add(current);
for (Class<?> iface : current.getInterfaces()) {
if (!list.contains(iface)) list.add(iface);
}
}
return list;
}
/**
* Skin 里有一堆只有一个 String 参数的方法(取字体、取颜色、取 drawable…),名字都被混淆了。
* 这里挨个试,哪个能用 "icon-tabTree" 取到非空对象、又不会抛异常,就是 getDrawable。
*/
private static Object getDrawable(Class<?> skinClass, Object skin, String name) {
for (Method method : skinClass.getMethods()) {
Class<?>[] types = method.getParameterTypes();
if (types.length != 1 || types[0] != String.class) continue;
if (method.getReturnType() == void.class) continue;
if (method.getReturnType() == String.class) continue;
try {
Object value = method.invoke(skin, name);
if (value != null) return value;
} catch (Throwable ignored) {
// 取错类型会抛,继续试下一个
}
}
return null;
}
/**
* 面板内容:第一行按钮(靠左),第二行是能滚动的文字区(撑满剩下的空间)。
*
* 布局调用全部按"名字 + 参数类型 + 返回类型"定位 —— 这套载荷把同名方法塞了一堆
* (Table 上 `I` 既有无参返回 Cell 的 row(),也有无参返回 Table 的 top()),
* 少判一个条件就会调到别的方法上。名字出处见 {@link Obf}。
*/
private static Object buildPanel(ClassLoader loader, Object skin) throws Exception {
// 面板内容(增删改查)在 ObjectPanel 里,这里只负责把它建出来。
return ObjectPanel.build(loader, skin);
}
/**
* 在 Cell 上调一串"无参、返回 Cell"的布局方法(fill/expand/top…,名字见 {@link Obf})。
* 单个调用失败只记一行日志,不让整个面板装不上。
*/
private static void layoutCell(Object cell, String... names) {
for (String name : names) {
try {
Method method = cell.getClass().getMethod(name);
method.setAccessible(true);
method.invoke(cell);
} catch (Throwable error) {
System.err.println("[view] 单元格布局 " + name + " 调用失败: " + error);
}
}
}
/** 调用 Table 上的实例方法:名字 + 参数类型 + 返回类型一起判(同名方法太多)。 */
private static Object call(Object target, String name, Class<?>[] parameterTypes,
Class<?> returnType, Object... arguments) throws Exception {
for (Method method : target.getClass().getMethods()) {
if (!method.getName().equals(name)) continue;
if (!java.util.Arrays.equals(method.getParameterTypes(), parameterTypes)) continue;
if (returnType != null && method.getReturnType() != returnType) continue;
method.setAccessible(true);
return method.invoke(target, arguments);
}
throw new NoSuchMethodException(target.getClass().getName() + "." + name
+ " 参数=" + java.util.Arrays.toString(parameterTypes));
}
private static Method findInstanceMethod(Class<?> owner, int parameterCount,
Class<?> parameterType) throws NoSuchMethodException {
for (Method method : owner.getMethods()) {
if (Modifier.isStatic(method.getModifiers())) continue;
Class<?>[] types = method.getParameterTypes();
if (types.length != parameterCount) continue;
boolean match = true;
for (Class<?> type : types) {
if (parameterType != null && type != parameterType) {
match = false;
break;
}
}
if (match) return method;
}
throw new NoSuchMethodException(owner.getName() + " 参数=" + parameterCount);
}
/** Group.addActor(Actor):一个参数、能容纳该 actor、返回 void。 */
private static void addChild(Object group, Object actor) throws Exception {
for (Method method : group.getClass().getMethods()) {
Class<?>[] types = method.getParameterTypes();
if (types.length != 1) continue;
if (!types[0].isAssignableFrom(actor.getClass())) continue;
if (method.getReturnType() != void.class) continue;
method.invoke(group, actor);
return;
}
throw new NoSuchMethodException("找不到接纳 " + actor.getClass().getName() + " 的方法");
}
}