package com.spineview; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; /** * 被 ViewPatcher 插进 tD. 的调用目标 —— 必须由**编辑器的类加载器**加载, * 所以运行时要把 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> bucketTypes(Class type) { java.util.List> list = new java.util.ArrayList>(); 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() + " 的方法"); } }