AlicizaX/Client/Assets/InputGlyph/InputGlyphDatabaseEditor.cs
2025-12-05 19:04:53 +08:00

641 lines
21 KiB
C#
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.

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;
}
}
}