AlicizaX/Client/Assets/InputGlyph/InputGlyphDatabaseEditor.cs

641 lines
21 KiB
C#
Raw Normal View History

2025-12-05 19:04:53 +08:00
using System;
using UnityEditor;
using UnityEngine;
using UnityEngine.U2D;
using TMPro;
using System.Reflection;
using System.Collections.Generic;
using UnityEngine.InputSystem;
using InputGlyphsFramework;
using System.IO;
using System.Linq;
using UnityEngine.InputSystem.Layouts;
[CustomEditor(typeof(InputGlyphDatabase))]
public class InputGlyphDatabaseEditor : Editor
{
InputGlyphDatabase db;
int tabIndex = 0;
string[] tabNames = new string[] { "Keyboard", "Xbox", "PlayStation", "Other" };
void OnEnable()
{
db = target as InputGlyphDatabase;
}
public override void OnInspectorGUI()
{
serializedObject.Update();
if (db == null) return;
EditorGUILayout.Space();
tabIndex = GUILayout.Toolbar(tabIndex, tabNames, GUILayout.Height(24));
var curDevice = (InputDeviceWatcher.InputDeviceCategory)tabIndex;
if (db.tables == null) db.tables = new List<DeviceGlyphTable>();
EnsureTableFor(InputDeviceWatcher.InputDeviceCategory.Keyboard);
EnsureTableFor(InputDeviceWatcher.InputDeviceCategory.Xbox);
EnsureTableFor(InputDeviceWatcher.InputDeviceCategory.PlayStation);
EnsureTableFor(InputDeviceWatcher.InputDeviceCategory.Other);
var table = db.GetTable(curDevice);
if (table == null) return;
EditorGUILayout.BeginVertical("box");
EditorGUILayout.LabelField(curDevice.ToString(), EditorStyles.boldLabel);
table.tmpAsset = EditorGUILayout.ObjectField("TMP Sprite Asset", table.tmpAsset, typeof(TMP_SpriteAsset), false) as TMP_SpriteAsset;
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Parse TMP Asset"))
{
ParseTMPAssetIntoTable(table);
}
if (GUILayout.Button("Clear"))
{
table.entries.Clear();
EditorUtility.SetDirty(db);
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space();
if (table.entries != null)
{
for (int i = 0; i < table.entries.Count; ++i)
{
var e = table.entries[i];
if (e == null) continue;
EditorGUILayout.BeginHorizontal(GUILayout.Height(64));
if (e.Sprite != null)
{
Texture2D preview = AssetPreview.GetAssetPreview(e.Sprite);
if (preview == null) preview = AssetPreview.GetMiniThumbnail(e.Sprite);
if (preview != null) GUILayout.Label(preview, GUILayout.Width(64), GUILayout.Height(64));
else EditorGUILayout.ObjectField(e.Sprite, typeof(Sprite), false, GUILayout.Width(64), GUILayout.Height(64));
}
else
{
EditorGUILayout.ObjectField(null, typeof(Sprite), false, GUILayout.Width(64), GUILayout.Height(64));
}
EditorGUILayout.BeginVertical();
EditorGUILayout.LabelField(e.Sprite != null ? e.Sprite.name : "<missing>", EditorStyles.boldLabel);
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("ControlPath", GUILayout.Width(80));
EditorGUILayout.SelectableLabel(e.controlPath ?? string.Empty, GUILayout.Height(16), GUILayout.Width(300));
Rect btnRect = GUILayoutUtility.GetRect(60, 18, GUILayout.Width(60));
if (GUI.Button(btnRect, "Bind"))
{
ControlPathPickerPopup.ShowDropdown(btnRect, curDevice, (selectedPath) =>
{
if (!string.IsNullOrEmpty(selectedPath))
{
e.controlPath = selectedPath;
EditorUtility.SetDirty(db);
AssetDatabase.SaveAssets();
}
});
}
if (GUILayout.Button("Clear", GUILayout.Width(48)))
{
e.controlPath = string.Empty;
EditorUtility.SetDirty(db);
AssetDatabase.SaveAssets();
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.EndVertical();
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(6);
}
}
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Add Entry"))
{
table.entries.Add(new GlyphEntry { Sprite = null, controlPath = string.Empty });
EditorUtility.SetDirty(db);
}
if (GUILayout.Button("Remove Nulls"))
{
table.entries.RemoveAll(x => x == null || x.Sprite == null);
EditorUtility.SetDirty(db);
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.EndVertical();
EditorGUILayout.Space();
if (GUILayout.Button("Save Asset"))
{
EditorUtility.SetDirty(db);
AssetDatabase.SaveAssets();
}
serializedObject.ApplyModifiedProperties();
}
void EnsureTableFor(InputDeviceWatcher.InputDeviceCategory device)
{
if (db.GetTable(device) == null)
{
var t = new DeviceGlyphTable { deviceType = device, tmpAsset = null, entries = new List<GlyphEntry>() };
db.tables.Add(t);
EditorUtility.SetDirty(db);
}
}
void ParseTMPAssetIntoTable(DeviceGlyphTable table)
{
if (table == null || table.tmpAsset == null) return;
var asset = table.tmpAsset;
table.entries = new List<GlyphEntry>();
var chars = asset.spriteCharacterTable;
SpriteAtlas atlas = GetSpriteAtlasFromTMP(asset);
string assetPath = AssetDatabase.GetAssetPath(asset);
string assetFolder = Path.GetDirectoryName(assetPath);
for (int i = 0; i < chars.Count; ++i)
{
var ch = chars[i];
if (ch == null) continue;
var name = ch.name;
if (string.IsNullOrEmpty(name)) continue;
Sprite s = null;
try
{
var glyph = ch.glyph as TMP_SpriteGlyph;
if (glyph != null && glyph.sprite != null) s = glyph.sprite;
}
catch
{
}
if (s == null && atlas != null)
{
try
{
s = atlas.GetSprite(name);
}
catch
{
s = null;
}
if (s == null)
{
try
{
var m = typeof(SpriteAtlas).GetMethod("GetSprite", new System.Type[] { typeof(string) });
if (m != null) s = m.Invoke(atlas, new object[] { name }) as Sprite;
}
catch
{
}
}
}
if (s == null)
{
string[] scoped = AssetDatabase.FindAssets(name + " t:Sprite", new[] { assetFolder });
if (scoped != null && scoped.Length > 0)
{
foreach (var g in scoped)
{
var p = AssetDatabase.GUIDToAssetPath(g);
var sp = AssetDatabase.LoadAssetAtPath<Sprite>(p);
if (sp != null && sp.name == name)
{
s = sp;
break;
}
}
}
}
if (s == null)
{
string[] all = AssetDatabase.FindAssets(name + " t:Sprite");
if (all != null && all.Length > 0)
{
foreach (var g in all)
{
var p = AssetDatabase.GUIDToAssetPath(g);
var sp = AssetDatabase.LoadAssetAtPath<Sprite>(p);
if (sp != null && sp.name == name)
{
s = sp;
break;
}
}
}
}
table.entries.Add(new GlyphEntry { Sprite = s, controlPath = string.Empty });
}
EditorUtility.SetDirty(db);
AssetDatabase.SaveAssets();
}
SpriteAtlas GetSpriteAtlasFromTMP(TMP_SpriteAsset asset)
{
if (asset == null) return null;
var t = asset.GetType();
foreach (var f in t.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
{
if (typeof(SpriteAtlas).IsAssignableFrom(f.FieldType))
{
var val = f.GetValue(asset) as SpriteAtlas;
if (val != null) return val;
}
}
foreach (var p in t.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
{
if (typeof(SpriteAtlas).IsAssignableFrom(p.PropertyType))
{
try
{
var val = p.GetValue(asset, null) as SpriteAtlas;
if (val != null) return val;
}
catch
{
}
}
}
return null;
}
}
public class ControlPathPickerPopup : EditorWindow
{
InputDeviceWatcher.InputDeviceCategory device;
Action<string> onSelect;
string search = "";
Vector2 scroll;
Dictionary<string, List<string>> groups = new Dictionary<string, List<string>>();
Dictionary<string, List<string>> displayGroups = new Dictionary<string, List<string>>();
Dictionary<string, bool> foldouts = new Dictionary<string, bool>();
bool listening = false;
IDisposable anyButtonSubscription;
public static void ShowDropdown(Rect anchorRect, InputDeviceWatcher.InputDeviceCategory device, Action<string> onSelect)
{
var w = CreateInstance<ControlPathPickerPopup>();
w.device = device;
w.onSelect = onSelect;
w.titleContent = new GUIContent("Pick Control Path");
w.minSize = new Vector2(480, 420);
w.InitListsDynamic();
w.ShowAsDropDown(anchorRect, new Vector2(480, 420));
}
void OnEnable()
{
EditorApplication.update += EditorUpdate;
}
void OnDisable()
{
StopListening();
EditorApplication.update -= EditorUpdate;
}
void EditorUpdate()
{
if (listening) Repaint();
}
// -------------------- 动态构建列表 --------------------
void InitListsDynamic()
{
groups.Clear();
displayGroups.Clear();
foldouts.Clear();
// 首先插入一些基本的静态分组占位(如果需要)
var known = new[] { "Keyboard", "Gamepad", "Mouse", "Joystick", "Touchscreen", "XR Controller", "XR HMD", "Pointer", "Pen", "Other" };
foreach (var k in known)
{
groups[k] = new List<string>();
displayGroups[k] = new List<string>();
foldouts[k] = true;
}
// 列出所有已注册布局名称
IEnumerable<string> layoutNames;
try
{
layoutNames = InputSystem.ListLayouts();
}
catch
{
layoutNames = Enumerable.Empty<string>();
}
foreach (var layoutName in layoutNames)
{
InputControlLayout layout = null;
try
{
layout = InputSystem.LoadLayout(layoutName);
}
catch
{
layout = null;
}
if (layout == null) continue;
// 遍历 layout.controls (InputControlLayout.ControlItem)
try
{
var controls = layout.controls;
if (controls.Count == 0) continue;
foreach (var ci in controls)
{
// isModifyingExistingControl 表示这个 control 只是修改已有 control 的属性,不引入新的控制项,通常跳过
if (ci.isModifyingExistingControl) continue;
string controlName = ci.name.ToString();
if (string.IsNullOrEmpty(controlName)) continue;
string path = $"<{layoutName}>/{controlName}";
// 决定分组:根据 layoutName 或 controlName 做简单匹配
string group = DecideGroupForLayout(layoutName, controlName);
if (!groups.ContainsKey(group))
{
groups[group] = new List<string>();
displayGroups[group] = new List<string>();
foldouts[group] = true;
}
// 防重复
if (!groups[group].Contains(path))
{
groups[group].Add(path);
displayGroups[group].Add($"{controlName} ({path})");
}
// 试着添加常见的子项(例如 stick/up/down 等),如果 controlItem 标识这些为子控制可能需要额外处理
// 但大多数情况下 layout.controls 已包含独立条目如 leftStick/up
}
}
catch
{
/* 忽略个别布局异常 */
}
}
// 额外补充:把一些常用手动添加的路径放到 Gamepad / Keyboard 中,防止某些 layout 未定义具体按钮
AddFallbackCommonPaths();
}
string DecideGroupForLayout(string layoutName, string controlName)
{
var lname = layoutName.ToLowerInvariant();
if (lname.Contains("keyboard")) return "Keyboard";
if (lname.Contains("gamepad") || lname.Contains("xbox") || lname.Contains("dualshock") || lname.Contains("controller")) return "Gamepad";
if (lname.Contains("mouse")) return "Mouse";
if (lname.Contains("joystick")) return "Joystick";
if (lname.Contains("touchscreen")) return "Touchscreen";
if (lname.Contains("xr") || lname.Contains("hmd")) return "XR Controller";
return "Other";
}
void AddFallbackCommonPaths()
{
void addIfMissing(string grp, string path, string label = null)
{
if (!groups.ContainsKey(grp))
{
groups[grp] = new List<string>();
displayGroups[grp] = new List<string>();
foldouts[grp] = true;
}
if (!groups[grp].Contains(path))
{
groups[grp].Add(path);
displayGroups[grp].Add(label ?? path);
}
}
addIfMissing("Gamepad", "<Gamepad>/buttonSouth");
addIfMissing("Gamepad", "<Gamepad>/buttonEast");
addIfMissing("Gamepad", "<Gamepad>/buttonWest");
addIfMissing("Gamepad", "<Gamepad>/buttonNorth");
addIfMissing("Gamepad", "<Gamepad>/leftShoulder");
addIfMissing("Gamepad", "<Gamepad>/rightShoulder");
addIfMissing("Gamepad", "<Gamepad>/leftTrigger");
addIfMissing("Gamepad", "<Gamepad>/rightTrigger");
addIfMissing("Keyboard", "<Keyboard>/space");
addIfMissing("Keyboard", "<Keyboard>/enter");
addIfMissing("Keyboard", "<Keyboard>/escape");
addIfMissing("Mouse", "<Mouse>/leftButton");
}
// -------------------- UI --------------------
void OnGUI()
{
EditorGUILayout.BeginVertical();
EditorGUILayout.BeginHorizontal();
var prevColor = GUI.color;
search = EditorGUILayout.TextField(search, GUILayout.ExpandWidth(true));
if (!listening)
{
if (GUILayout.Button("Listen", GUILayout.Width(72)))
{
StartListening();
}
}
else
{
GUI.color = Color.yellow;
if (GUILayout.Button("Stop", GUILayout.Width(72)))
{
StopListening();
}
GUI.color = prevColor;
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space();
scroll = EditorGUILayout.BeginScrollView(scroll);
foreach (var kv in displayGroups.OrderBy(k => k.Key))
{
string cat = kv.Key;
if (!ShouldShowCategory(cat)) continue;
foldouts.TryGetValue(cat, out bool f);
f = EditorGUILayout.Foldout(f, cat, true);
foldouts[cat] = f;
if (!f) continue;
var displays = kv.Value;
var vals = groups[cat];
for (int i = 0; i < displays.Count; ++i)
{
var dsp = displays[i];
var val = vals[i];
if (!MatchSearch(dsp, val)) continue;
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Select", GUILayout.Width(64)))
{
onSelect?.Invoke(val);
Close();
return;
}
EditorGUILayout.LabelField(dsp, GUILayout.ExpandWidth(true));
EditorGUILayout.EndHorizontal();
}
EditorGUILayout.Space(6);
}
EditorGUILayout.EndScrollView();
EditorGUILayout.Space();
GUILayout.FlexibleSpace();
if (GUILayout.Button("Close")) Close();
EditorGUILayout.EndVertical();
}
bool ShouldShowCategory(string cat)
{
if (string.IsNullOrEmpty(search)) return true;
if (cat.IndexOf(search, StringComparison.OrdinalIgnoreCase) >= 0) return true;
var disp = displayGroups[cat];
for (int i = 0; i < disp.Count; ++i)
if (MatchSearch(disp[i], groups[cat][i]))
return true;
return false;
}
bool MatchSearch(string display, string value)
{
if (string.IsNullOrEmpty(search)) return true;
if (display.IndexOf(search, StringComparison.OrdinalIgnoreCase) >= 0) return true;
if (value.IndexOf(search, StringComparison.OrdinalIgnoreCase) >= 0) return true;
return false;
}
// -------------------- Listen (使用 IObserver<T> 订阅) --------------------
class InputControlObserver : IObserver<InputControl>
{
readonly Action<InputControl> onNext;
public InputControlObserver(Action<InputControl> onNext)
{
this.onNext = onNext;
}
public void OnCompleted()
{
}
public void OnError(Exception error)
{
}
public void OnNext(InputControl value)
{
try
{
onNext?.Invoke(value);
}
catch
{
}
}
}
void StartListening()
{
if (listening) return;
listening = true;
try
{
// 使用 IObserver<InputControl> 订阅(通用、可靠)
anyButtonSubscription = InputSystem.onAnyButtonPress.Subscribe(new InputControlObserver(control =>
{
if (control == null) return;
string path = control.path;
onSelect?.Invoke(path);
StopListening();
Close();
}));
}
catch (Exception)
{
// 某些版本/包装可能提供 Call/CallOnce 扩展方法Utilities尝试反射调用这些扩展
try
{
var t = typeof(InputSystem);
var prop = t.GetProperty("onAnyButtonPress");
if (prop != null)
{
var obs = prop.GetValue(null);
var callMethod = obs?.GetType().GetMethod("CallOnce") ?? obs?.GetType().GetMethod("Call");
if (callMethod != null)
{
// 尝试调用 CallOnce(Action<InputControl>)
callMethod.Invoke(obs, new object[]
{
new Action<InputControl>(c =>
{
if (c == null) return;
onSelect?.Invoke(c.path);
StopListening();
Close();
})
});
StopListening();
Close();
return;
}
}
}
catch
{
}
// 如果都失败,简单关闭监听状态
listening = false;
}
}
void StopListening()
{
if (!listening) return;
listening = false;
try
{
anyButtonSubscription?.Dispose();
anyButtonSubscription = null;
}
catch
{
anyButtonSubscription = null;
}
}
}