AlicizaX/Client/Assets/InputGlyph/InputGlyphDatabaseEditor.cs
2025-12-10 17:38:31 +08:00

535 lines
23 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 System.Collections.Generic;
using System.IO;
using System.Reflection;
using AlicizaX.InputGlyph;
using UnityEditor;
using UnityEngine;
using UnityEngine.U2D;
using TMPro;
[CustomEditor(typeof(InputGlyphDatabase))]
public class InputGlyphDatabaseEditor : Editor
{
SerializedProperty tablesProp;
SerializedProperty placeholderSpriteProp;
InputGlyphDatabase db;
// 动态标签索引范围0 .. tablesCount 为各表,最后一个 index = tablesCount 为 Settings
int tabIndex = 0;
// 添加表时使用的临时 UI 状态
bool showAddField = false;
string newTableName = "";
// 每个表的搜索字符串与分页状态editor 内存,不序列化)
List<string> searchStrings = new List<string>();
List<int> currentPages = new List<int>();
// 常量
const int itemsPerPage = 10;
// 缩小后的预览尺寸
const int previewSize = 52;
void OnEnable()
{
db = target as InputGlyphDatabase;
tablesProp = serializedObject.FindProperty("tables");
placeholderSpriteProp = serializedObject.FindProperty("placeholderSprite");
if (tablesProp == null)
{
Debug.LogError("Could not find serialized property 'tables' on InputGlyphDatabase. Check field name.");
return;
}
// 如果没有默认的 Keyboard/Xbox/PlayStation 三个表则确保创建(便于迁移)
EnsureDefaultTable("Keyboard");
EnsureDefaultTable("Xbox");
EnsureDefaultTable("PlayStation");
// 初始化 editor 状态列表,长度与 tablesProp 对应
SyncEditorListsWithTables();
}
public override void OnInspectorGUI()
{
serializedObject.Update();
if (db == null || tablesProp == null) return;
// 顶部工具栏(与 Save 按钮风格一致),同时放置 Settings 按钮
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
GUILayout.Space(4);
if (GUILayout.Button("Save Asset", EditorStyles.toolbarButton))
{
serializedObject.ApplyModifiedProperties();
EditorUtility.SetDirty(db);
AssetDatabase.SaveAssets();
}
GUILayout.FlexibleSpace();
// + 按钮:切换显示新增输入区域
if (GUILayout.Button(showAddField ? "Cancel +" : "+ Add Table", EditorStyles.toolbarButton, GUILayout.Width(110)))
{
showAddField = !showAddField;
newTableName = "";
}
// Settings 按钮也放在这个 toolbar 上(风格保持一致)
int settingsIndex = tablesProp != null ? tablesProp.arraySize : 0;
bool settingsSelected = (tabIndex == settingsIndex);
if (GUILayout.Toggle(settingsSelected, "Settings", EditorStyles.toolbarButton, GUILayout.Width(90)) != settingsSelected)
{
// 切换到 settings 页面或从 settings 切回第一个 table如果取消
tabIndex = (tabIndex == settingsIndex) ? 0 : settingsIndex;
}
EditorGUILayout.EndHorizontal();
// 如果正在新增,展示一个横向输入框(下方,同 toolbar 风格)
if (showAddField)
{
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
GUILayout.Label("Name:", GUILayout.Width(40));
newTableName = EditorGUILayout.TextField(newTableName);
if (GUILayout.Button("Add", EditorStyles.toolbarButton, GUILayout.Width(80)))
{
string trimmed = newTableName != null ? newTableName.Trim() : "";
if (string.IsNullOrEmpty(trimmed))
{
EditorUtility.DisplayDialog("Invalid Name", "Table name cannot be empty.", "OK");
}
else
{
// 唯一性检查(不区分大小写)
bool exists = false;
for (int i = 0; i < tablesProp.arraySize; ++i)
{
var t = tablesProp.GetArrayElementAtIndex(i);
var nameProp = t.FindPropertyRelative("deviceName");
if (nameProp != null && string.Equals(nameProp.stringValue, trimmed, StringComparison.OrdinalIgnoreCase))
{
exists = true; break;
}
}
if (exists)
{
EditorUtility.DisplayDialog("Duplicate", "A table with that name already exists.", "OK");
}
else
{
int newIndex = tablesProp.arraySize;
tablesProp.InsertArrayElementAtIndex(newIndex);
var newTable = tablesProp.GetArrayElementAtIndex(newIndex);
var nameProp = newTable.FindPropertyRelative("deviceName");
if (nameProp != null) nameProp.stringValue = trimmed;
var tmpAssetProp = newTable.FindPropertyRelative("tmpAsset");
if (tmpAssetProp != null) tmpAssetProp.objectReferenceValue = null;
var entriesProp = newTable.FindPropertyRelative("entries");
if (entriesProp != null) entriesProp.arraySize = 0;
serializedObject.ApplyModifiedProperties();
EditorUtility.SetDirty(db);
// 重新 sync editor lists
SyncEditorListsWithTables();
showAddField = false;
tabIndex = tablesProp.arraySize - 1; // 选择新建的 tab
}
}
}
if (GUILayout.Button("Cancel", EditorStyles.toolbarButton, GUILayout.Width(80)))
{
showAddField = false;
newTableName = "";
}
EditorGUILayout.EndHorizontal();
}
EditorGUILayout.Space(6);
// 绘制标签行(来自 tables 的 deviceName但不再包含 SettingsSettings 已在上方 toolbar
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
int tablesCount = tablesProp.arraySize;
for (int i = 0; i < tablesCount; ++i)
{
var t = tablesProp.GetArrayElementAtIndex(i);
var nameProp = t.FindPropertyRelative("deviceName");
string name = nameProp != null ? nameProp.stringValue : ("Table " + i);
bool selected = (tabIndex == i);
// 采用 toolbarButton 风格的 toggle
if (GUILayout.Toggle(selected, name, EditorStyles.toolbarButton, GUILayout.MinWidth(60)))
{
tabIndex = i;
}
// 每个表右侧加一个小删除按钮Settings 不在这里)
if (GUILayout.Button("×", EditorStyles.toolbarButton, GUILayout.Width(22)))
{
// 二次确认
if (EditorUtility.DisplayDialog("Delete Table?",
$"Delete table '{name}' and all its entries? This cannot be undone.", "Delete", "Cancel"))
{
tablesProp.DeleteArrayElementAtIndex(i);
serializedObject.ApplyModifiedProperties();
EditorUtility.SetDirty(db);
// 调整 tabIndex 与 editor 状态
SyncEditorListsWithTables();
tabIndex = Mathf.Clamp(tabIndex, 0, Math.Max(0, tablesProp.arraySize - 1));
return; // 直接返回防止继续绘制已修改的 serializedObject
}
}
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(8);
// 绘制选中页内容(如果是 Settings 或某个 table
EditorGUILayout.BeginVertical("box");
if (tabIndex == tablesProp.arraySize)
{
// Settings 页
EditorGUILayout.LabelField("Settings", EditorStyles.boldLabel);
EditorGUILayout.Space(4);
EditorGUILayout.PropertyField(placeholderSpriteProp, new GUIContent("Placeholder Sprite"));
Sprite placeholder = placeholderSpriteProp.objectReferenceValue as Sprite;
EditorGUILayout.Space(6);
EditorGUILayout.LabelField("Preview", EditorStyles.miniBoldLabel);
if (placeholder != null)
{
Texture2D preview = AssetPreview.GetAssetPreview(placeholder);
if (preview == null) preview = AssetPreview.GetMiniThumbnail(placeholder);
if (preview != null) GUILayout.Label(preview, GUILayout.Width(previewSize), GUILayout.Height(previewSize));
else EditorGUILayout.ObjectField(placeholder, typeof(Sprite), false, GUILayout.Width(previewSize), GUILayout.Height(previewSize));
}
else
{
EditorGUILayout.HelpBox("No placeholder sprite assigned. If FindEntryByControlPath receives an empty path, it will return null.", MessageType.Info);
}
}
else
{
// Table 页
if (tabIndex < 0 || tabIndex >= tablesProp.arraySize)
{
EditorGUILayout.HelpBox("Invalid table index.", MessageType.Error);
}
else
{
var tableProp = tablesProp.GetArrayElementAtIndex(tabIndex);
// 去掉顶部显示 table 名称的 label
// var nameProp = tableProp.FindPropertyRelative("deviceName");
// string tableName = nameProp != null ? nameProp.stringValue : $"Table {tabIndex}";
// EditorGUILayout.LabelField(tableName, EditorStyles.boldLabel);
// Ensure editor lists 长度一致
EnsureEditorListsLength();
// 搜索框:尽量使用 EditorStyles.toolbarSearchField去掉左侧标题和 clear 按钮)
GUILayout.BeginHorizontal();
GUIStyle searchStyle = EditorStyles.toolbarSearchField ?? EditorStyles.textField;
searchStrings[tabIndex] = GUILayout.TextField(searchStrings[tabIndex] ?? "", searchStyle);
GUILayout.EndHorizontal();
EditorGUILayout.Space(6);
// 将 TMP Sprite Asset 的选择框 与 Parse / Clear 按钮 水平显示
var tmpAssetProp = tableProp.FindPropertyRelative("tmpAsset");
EditorGUILayout.BeginHorizontal();
GUILayout.Label("TMP Sprite Asset", GUILayout.Width(140));
EditorGUILayout.PropertyField(tmpAssetProp, GUIContent.none, GUILayout.ExpandWidth(true));
if (GUILayout.Button("Parse TMP Asset", GUILayout.Width(120))) ParseTMPAssetIntoTableSerialized(tableProp);
if (GUILayout.Button("Clear", GUILayout.Width(80)))
{
var entriesProp = tableProp.FindPropertyRelative("entries");
if (entriesProp != null) entriesProp.arraySize = 0;
serializedObject.ApplyModifiedProperties();
EditorUtility.SetDirty(db);
currentPages[tabIndex] = 0;
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(6);
var entries = tableProp.FindPropertyRelative("entries");
if (entries != null)
{
int total = entries.arraySize;
// collect matched indices by searching sprite.name
List<int> matchedIndices = new List<int>();
string query = (searchStrings[tabIndex] ?? "").Trim();
for (int i = 0; i < total; ++i)
{
var eProp = entries.GetArrayElementAtIndex(i);
if (eProp == null) continue;
var spriteProp = eProp.FindPropertyRelative("Sprite");
Sprite s = spriteProp.objectReferenceValue as Sprite;
string name = s != null ? s.name : "";
if (string.IsNullOrEmpty(query) || name.IndexOf(query, StringComparison.OrdinalIgnoreCase) >= 0)
{
matchedIndices.Add(i);
}
}
int matchedTotal = matchedIndices.Count;
int totalPages = Mathf.Max(1, (matchedTotal + itemsPerPage - 1) / itemsPerPage);
currentPages[tabIndex] = Mathf.Clamp(currentPages[tabIndex], 0, totalPages - 1);
// pagination controls (toolbar 风格)
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("<<", EditorStyles.miniButtonLeft, GUILayout.Width(36))) { currentPages[tabIndex] = 0; }
if (GUILayout.Button("<", EditorStyles.miniButtonMid, GUILayout.Width(36))) { currentPages[tabIndex] = Mathf.Max(0, currentPages[tabIndex] - 1); }
GUILayout.FlexibleSpace();
EditorGUILayout.LabelField(string.Format("Page {0}/{1}", currentPages[tabIndex] + 1, totalPages), GUILayout.Width(120));
GUILayout.FlexibleSpace();
if (GUILayout.Button(">", EditorStyles.miniButtonMid, GUILayout.Width(36))) { currentPages[tabIndex] = Mathf.Min(totalPages - 1, currentPages[tabIndex] + 1); }
if (GUILayout.Button(">>", EditorStyles.miniButtonRight, GUILayout.Width(36))) { currentPages[tabIndex] = totalPages - 1; }
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space(4);
int start = currentPages[tabIndex] * itemsPerPage;
int end = Math.Min(start + itemsPerPage, matchedTotal);
for (int mi = start; mi < end; ++mi)
{
int i = matchedIndices[mi];
var eProp = entries.GetArrayElementAtIndex(i);
if (eProp == null) continue;
// 使用稍小的间距并减小 preview 大小
using (new EditorGUILayout.HorizontalScope("box"))
{
// 左列 sprite 预览(固定宽)
using (new EditorGUILayout.VerticalScope(GUILayout.Width(80)))
{
var spriteProp = eProp.FindPropertyRelative("Sprite");
Sprite s = spriteProp.objectReferenceValue as Sprite;
EditorGUILayout.LabelField(s != null ? s.name : "<missing>", EditorStyles.boldLabel);
if (s != null)
{
Texture2D preview = AssetPreview.GetAssetPreview(s);
if (preview == null) preview = AssetPreview.GetMiniThumbnail(s);
if (preview != null)
{
GUILayout.Label(preview, GUILayout.Width(previewSize), GUILayout.Height(previewSize));
}
else
{
EditorGUILayout.PropertyField(spriteProp, GUIContent.none, GUILayout.Width(previewSize), GUILayout.Height(previewSize));
}
}
else
{
EditorGUILayout.PropertyField(spriteProp, GUIContent.none, GUILayout.Width(previewSize), GUILayout.Height(previewSize));
}
}
// 右列 action 字段
EditorGUILayout.BeginVertical();
var actionProp = eProp.FindPropertyRelative("action");
EditorGUILayout.Space(2);
EditorGUILayout.PropertyField(actionProp, GUIContent.none, GUILayout.ExpandWidth(true));
EditorGUILayout.EndVertical();
}
EditorGUILayout.Space(4);
}
if (matchedTotal == 0)
{
EditorGUILayout.HelpBox("No entries match the search.", MessageType.Info);
}
}
}
}
EditorGUILayout.EndVertical();
EditorGUILayout.Space(6);
serializedObject.ApplyModifiedProperties();
}
// Ensure table with name exists (用于初次迁移)
void EnsureDefaultTable(string name)
{
if (tablesProp == null) return;
for (int i = 0; i < tablesProp.arraySize; ++i)
{
var t = tablesProp.GetArrayElementAtIndex(i);
var nameProp = t.FindPropertyRelative("deviceName");
if (nameProp != null && string.Equals(nameProp.stringValue, name, StringComparison.OrdinalIgnoreCase))
return;
}
int idx = tablesProp.arraySize;
tablesProp.InsertArrayElementAtIndex(idx);
var newTable = tablesProp.GetArrayElementAtIndex(idx);
var deviceNameProp = newTable.FindPropertyRelative("deviceName");
if (deviceNameProp != null) deviceNameProp.stringValue = name;
var tmpAssetProp = newTable.FindPropertyRelative("tmpAsset");
if (tmpAssetProp != null) tmpAssetProp.objectReferenceValue = null;
var entriesProp = newTable.FindPropertyRelative("entries");
if (entriesProp != null) entriesProp.arraySize = 0;
serializedObject.ApplyModifiedProperties();
EditorUtility.SetDirty(db);
}
void SyncEditorListsWithTables()
{
int count = tablesProp != null ? tablesProp.arraySize : 0;
if (searchStrings == null) searchStrings = new List<string>();
if (currentPages == null) currentPages = new List<int>();
while (searchStrings.Count < count) searchStrings.Add("");
while (currentPages.Count < count) currentPages.Add(0);
while (searchStrings.Count > count) searchStrings.RemoveAt(searchStrings.Count - 1);
while (currentPages.Count > count) currentPages.RemoveAt(currentPages.Count - 1);
}
void EnsureEditorListsLength()
{
if (tablesProp == null) return;
SyncEditorListsWithTables();
}
// Parse TMP Sprite Asset into table (保持你原有实现)
void ParseTMPAssetIntoTableSerialized(SerializedProperty tableProp)
{
if (tableProp == null) return;
var tmpAssetProp = tableProp.FindPropertyRelative("tmpAsset");
var asset = tmpAssetProp.objectReferenceValue as TMP_SpriteAsset;
if (asset == null) return;
var entriesProp = tableProp.FindPropertyRelative("entries");
if (entriesProp == null) return;
entriesProp.arraySize = 0;
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 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;
}
}
}
}
int newIndex = entriesProp.arraySize;
entriesProp.InsertArrayElementAtIndex(newIndex);
var entryProp = entriesProp.GetArrayElementAtIndex(newIndex);
var spriteProp = entryProp.FindPropertyRelative("Sprite");
if (spriteProp != null) spriteProp.objectReferenceValue = s;
}
serializedObject.ApplyModifiedProperties();
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;
}
}