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 searchStrings = new List(); List currentPages = new List(); // 常量 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),但不再包含 Settings(Settings 已在上方 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 matchedIndices = new List(); 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 : "", 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(); if (currentPages == null) currentPages = new List(); 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(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(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; } }