using System; using System.Collections.Generic; using UnityEditor; using UnityEngine; [CustomEditor(typeof(InputGlyphDatabase))] public class InputGlyphDatabaseEditor : Editor { SerializedProperty tablesProp; SerializedProperty placeholderSpriteProp; InputGlyphDatabase db; int tabIndex = 0; bool showAddField = false; string newTableName = ""; List searchStrings = new List(); List currentPages = new List(); // 每个表的临时字段,用于添加单个条目(目前仅支持 sprite) List newEntrySprites = 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; } EnsureDefaultTable("Keyboard"); EnsureDefaultTable("Xbox"); EnsureDefaultTable("PlayStation"); SyncEditorListsWithTables(); } public override void OnInspectorGUI() { serializedObject.Update(); if (db == null || tablesProp == null) return; 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 = ""; } int settingsIndex = tablesProp != null ? tablesProp.arraySize : 0; bool settingsSelected = (tabIndex == settingsIndex); if (GUILayout.Toggle(settingsSelected, "Settings", EditorStyles.toolbarButton, GUILayout.Width(90)) != settingsSelected) { tabIndex = (tabIndex == settingsIndex) ? 0 : settingsIndex; } EditorGUILayout.EndHorizontal(); 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 sheetProp = newTable.FindPropertyRelative("spriteSheetTexture"); if (sheetProp != null) sheetProp.objectReferenceValue = null; var entriesProp = newTable.FindPropertyRelative("entries"); if (entriesProp != null) entriesProp.arraySize = 0; serializedObject.ApplyModifiedProperties(); EditorUtility.SetDirty(db); SyncEditorListsWithTables(); showAddField = false; tabIndex = tablesProp.arraySize - 1; } } } if (GUILayout.Button("Cancel", EditorStyles.toolbarButton, GUILayout.Width(80))) { showAddField = false; newTableName = ""; } EditorGUILayout.EndHorizontal(); } EditorGUILayout.Space(6); 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); if (GUILayout.Toggle(selected, name, EditorStyles.toolbarButton, GUILayout.MinWidth(60))) { tabIndex = i; } 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); SyncEditorListsWithTables(); tabIndex = Mathf.Clamp(tabIndex, 0, Math.Max(0, tablesProp.arraySize - 1)); return; } } } EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(8); EditorGUILayout.BeginVertical("box"); if (tabIndex == tablesProp.arraySize) { // 设置 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 { if (tabIndex < 0 || tabIndex >= tablesProp.arraySize) { EditorGUILayout.HelpBox("Invalid table index.", MessageType.Error); } else { var tableProp = tablesProp.GetArrayElementAtIndex(tabIndex); EnsureEditorListsLength(); // 计算此表的 deviceName 和运行时索引(用于删除单个条目时) var nameProp = tableProp.FindPropertyRelative("deviceName"); string deviceName = nameProp != null ? nameProp.stringValue : ""; int runtimeTableIndex = MapSerializedTableToRuntimeIndex(deviceName); GUILayout.BeginHorizontal(); GUIStyle searchStyle = EditorStyles.toolbarSearchField ?? EditorStyles.textField; searchStrings[tabIndex] = GUILayout.TextField(searchStrings[tabIndex] ?? "", searchStyle); GUILayout.EndHorizontal(); EditorGUILayout.Space(6); var sheetProp = tableProp.FindPropertyRelative("spriteSheetTexture"); EditorGUILayout.BeginHorizontal(); GUILayout.Label("Sprite Sheet (Texture2D)", GUILayout.Width(140)); EditorGUILayout.PropertyField(sheetProp, GUIContent.none, GUILayout.ExpandWidth(true)); if (GUILayout.Button("Parse Sprite Sheet", GUILayout.Width(120))) { ParseSpriteSheetIntoTableSerialized(tableProp); } if (GUILayout.Button("Clear", GUILayout.Width(80))) { var entriesProp = tableProp.FindPropertyRelative("entries"); if (entriesProp != null) entriesProp.arraySize = 0; if (runtimeTableIndex >= 0 && db != null) { var table = db.tables[runtimeTableIndex]; if (table != null) table.entries.Clear(); } serializedObject.ApplyModifiedProperties(); EditorUtility.SetDirty(db); currentPages[tabIndex] = 0; } EditorGUILayout.EndHorizontal(); var platformProp = tableProp.FindPropertyRelative("platformIcons"); EditorGUILayout.PropertyField(platformProp, new GUIContent("Platforms Icons")); Sprite placeholder = platformProp.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 PlatformIcons.", MessageType.Info); } EditorGUILayout.Space(6); // ---- 新增:单个新增 Entry 的 UI(只支持 Sprite) ---- EditorGUILayout.BeginVertical("box"); EditorGUILayout.LabelField("Add Single Entry", EditorStyles.boldLabel); EditorGUILayout.BeginHorizontal(); GUILayout.Label("Sprite", GUILayout.Width(50)); newEntrySprites[tabIndex] = (Sprite)EditorGUILayout.ObjectField(newEntrySprites[tabIndex], typeof(Sprite), false, GUILayout.Width(80), GUILayout.Height(80)); GUILayout.FlexibleSpace(); EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(6); EditorGUILayout.BeginHorizontal(); GUILayout.FlexibleSpace(); if (GUILayout.Button("Add Entry", GUILayout.Width(110))) { if (newEntrySprites[tabIndex] == null) { EditorUtility.DisplayDialog("Missing Sprite", "Please assign a Sprite to add.", "OK"); } else { AddEntryToTableSerialized(tableProp, newEntrySprites[tabIndex]); // reset temp field newEntrySprites[tabIndex] = null; currentPages[tabIndex] = 0; } } EditorGUILayout.EndHorizontal(); EditorGUILayout.EndVertical(); // ---- end add-single-entry UI ---- EditorGUILayout.Space(6); var entries = tableProp.FindPropertyRelative("entries"); if (entries != null) { int total = entries.arraySize; 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); 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; // 显示一个条目,右侧带有小的删除按钮 using (new EditorGUILayout.HorizontalScope("box")) { // 左侧:预览列 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)); } } // 中间:操作列 EditorGUILayout.BeginVertical(); var actionProp = eProp.FindPropertyRelative("action"); EditorGUILayout.Space(2); EditorGUILayout.PropertyField(actionProp, GUIContent.none, GUILayout.ExpandWidth(true)); EditorGUILayout.EndVertical(); // 右侧:小的删除按钮 GUILayout.Space(6); if (GUILayout.Button("×", EditorStyles.toolbarButton, GUILayout.Width(24))) { string spriteName = null; var sProp = eProp.FindPropertyRelative("Sprite"); if (sProp != null) spriteName = (sProp.objectReferenceValue as Sprite)?.name; if (EditorUtility.DisplayDialog("Remove Entry?", $"Remove entry '{(string.IsNullOrEmpty(spriteName) ? "" : spriteName)}' from table '{deviceName}'?", "Remove", "Cancel")) { // 从序列化数组中移除 var entriesProp = tableProp.FindPropertyRelative("entries"); if (entriesProp != null && i >= 0 && i < entriesProp.arraySize) { entriesProp.DeleteArrayElementAtIndex(i); // 应用后从运行时移除以保持两者同步 serializedObject.ApplyModifiedProperties(); } // 从运行时列表中移除(db.tables) if (runtimeTableIndex >= 0 && db != null && db.tables != null && runtimeTableIndex < db.tables.Count) { var runtimeTable = db.tables[runtimeTableIndex]; if (runtimeTable != null && i >= 0 && i < runtimeTable.entries.Count) { runtimeTable.entries.RemoveAt(i); } } EditorUtility.SetDirty(db); AssetDatabase.SaveAssets(); // 重置分页并返回,避免继续迭代已变更的序列化数组 currentPages[tabIndex] = 0; return; } } } EditorGUILayout.Space(4); } if (matchedTotal == 0) { EditorGUILayout.HelpBox("No entries match the search.", MessageType.Info); } } } } EditorGUILayout.EndVertical(); EditorGUILayout.Space(6); serializedObject.ApplyModifiedProperties(); } 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 sheetProp = newTable.FindPropertyRelative("spriteSheetTexture"); if (sheetProp != null) sheetProp.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(); if (newEntrySprites == null) newEntrySprites = new List(); while (searchStrings.Count < count) searchStrings.Add(""); while (currentPages.Count < count) currentPages.Add(0); while (newEntrySprites.Count < count) newEntrySprites.Add(null); while (searchStrings.Count > count) searchStrings.RemoveAt(searchStrings.Count - 1); while (currentPages.Count > count) currentPages.RemoveAt(currentPages.Count - 1); while (newEntrySprites.Count > count) newEntrySprites.RemoveAt(newEntrySprites.Count - 1); } void EnsureEditorListsLength() { if (tablesProp == null) return; SyncEditorListsWithTables(); } // ----- 新增:把单个 Sprite 加入到序列化表和 runtime 表 ----- void AddEntryToTableSerialized(SerializedProperty tableProp, Sprite sprite) { if (tableProp == null) return; var entriesProp = tableProp.FindPropertyRelative("entries"); if (entriesProp == null) return; int insertIndex = entriesProp.arraySize; entriesProp.InsertArrayElementAtIndex(insertIndex); var newE = entriesProp.GetArrayElementAtIndex(insertIndex); if (newE != null) { var spriteProp = newE.FindPropertyRelative("Sprite"); var actionProp = newE.FindPropertyRelative("action"); if (spriteProp != null) spriteProp.objectReferenceValue = sprite; // 保持 action 序列化不变(大多数项目无法在此处直接序列化 InputAction) if (actionProp != null) { try { actionProp.objectReferenceValue = null; } catch { } } } serializedObject.ApplyModifiedProperties(); // 同时添加到运行时列表 var nameProp = tableProp.FindPropertyRelative("deviceName"); string deviceName = nameProp != null ? nameProp.stringValue : ""; int tableIndex = MapSerializedTableToRuntimeIndex(deviceName); if (tableIndex >= 0 && db != null && db.tables != null && tableIndex < db.tables.Count) { var tableObj = db.tables[tableIndex]; GlyphEntry e = new GlyphEntry(); e.Sprite = sprite; e.action = null; // runtime only: none provided here tableObj.entries.Add(e); } EditorUtility.SetDirty(db); AssetDatabase.SaveAssets(); } // ----- Parse Sprite Sheet (Texture2D with Multiple) ----- // 只用名称匹配覆盖 Sprite,不改变已有 action void ParseSpriteSheetIntoTableSerialized(SerializedProperty tableProp) { if (tableProp == null) return; var sheetProp = tableProp.FindPropertyRelative("spriteSheetTexture"); var tex = sheetProp != null ? sheetProp.objectReferenceValue as Texture2D : null; if (tex == null) { Debug.LogWarning("[InputGlyphDatabase] spriteSheetTexture is null for table."); return; } var nameProp = tableProp.FindPropertyRelative("deviceName"); string deviceName = nameProp != null ? nameProp.stringValue : ""; int tableIndex = MapSerializedTableToRuntimeIndex(deviceName); if (tableIndex < 0) { Debug.LogError($"[InputGlyphDatabase] Could not map serialized table '{deviceName}' to runtime db.tables."); return; } var tableObj = db.tables[tableIndex]; if (tableObj == null) { Debug.LogError($"[InputGlyphDatabase] Runtime table object is null for '{deviceName}'."); return; } string path = AssetDatabase.GetAssetPath(tex); if (string.IsNullOrEmpty(path)) { Debug.LogWarning("[InputGlyphDatabase] Could not get asset path for texture."); return; } var assets = AssetDatabase.LoadAllAssetsAtPath(path); if (assets == null || assets.Length == 0) { Debug.LogWarning("[InputGlyphDatabase] No sub-assets found at path: " + path); return; } // 收集 sprites(按照文件内顺序;你如果想按名字排序可以在这里加) List sprites = new List(); foreach (var a in assets) { if (a is Sprite sp) sprites.Add(sp); } var entriesProp = tableProp.FindPropertyRelative("entries"); if (entriesProp == null) { Debug.LogWarning("[InputGlyphDatabase] entries property not found on table."); return; } // 构建序列化表名 -> 索引 映射(忽略大小写) var serializedNameToIndex = new Dictionary(StringComparer.OrdinalIgnoreCase); for (int i = 0; i < entriesProp.arraySize; ++i) { var eProp = entriesProp.GetArrayElementAtIndex(i); if (eProp == null) continue; var sProp = eProp.FindPropertyRelative("Sprite"); var sRef = sProp != null ? sProp.objectReferenceValue as Sprite : null; if (sRef != null && !serializedNameToIndex.ContainsKey(sRef.name)) { serializedNameToIndex[sRef.name] = i; } } // runtime 名称 -> 索引 映射 var runtimeNameToIndex = new Dictionary(StringComparer.OrdinalIgnoreCase); for (int i = 0; i < tableObj.entries.Count; ++i) { var re = tableObj.entries[i]; if (re != null && re.Sprite != null) { var rn = re.Sprite.name; if (!runtimeNameToIndex.ContainsKey(rn)) runtimeNameToIndex[rn] = i; } } int replaced = 0, added = 0; foreach (var sp in sprites) { if (sp == null) continue; string nm = sp.name; // -- 序列化层:同名则替换 Sprite 引用(不触碰 action),否则新增元素并把 action 设为 null -- if (serializedNameToIndex.TryGetValue(nm, out int sIndex)) { var eProp = entriesProp.GetArrayElementAtIndex(sIndex); if (eProp != null) { var spriteProp = eProp.FindPropertyRelative("Sprite"); if (spriteProp != null) spriteProp.objectReferenceValue = sp; // 不修改 actionProp,保持原有 action(如果有的话) } replaced++; } else { int insertIndex = entriesProp.arraySize; entriesProp.InsertArrayElementAtIndex(insertIndex); var newE = entriesProp.GetArrayElementAtIndex(insertIndex); if (newE != null) { var spriteProp = newE.FindPropertyRelative("Sprite"); var actionProp = newE.FindPropertyRelative("action"); if (spriteProp != null) spriteProp.objectReferenceValue = sp; if (actionProp != null) actionProp.objectReferenceValue = null; // 新增项 action 为空 } serializedNameToIndex[nm] = insertIndex; added++; } // -- 运行时层:同名则替换 Sprite,否则新增 runtime entry(action 设 null,保持之前 runtime entry 的 action 不变) -- if (runtimeNameToIndex.TryGetValue(nm, out int rIndex)) { var runtimeEntry = tableObj.entries[rIndex]; if (runtimeEntry != null) runtimeEntry.Sprite = sp; } else { GlyphEntry ge = new GlyphEntry(); ge.Sprite = sp; ge.action = null; tableObj.entries.Add(ge); runtimeNameToIndex[nm] = tableObj.entries.Count - 1; } } // 应用并保存修改(序列化层与 runtime 层保持同步) EditorUtility.SetDirty(db); serializedObject.Update(); serializedObject.ApplyModifiedProperties(); AssetDatabase.SaveAssets(); Debug.Log($"[InputGlyphDatabase] Merged sprite sheet '{tex.name}' into table '{deviceName}'. spritesFound={sprites.Count}, replaced={replaced}, added={added}, totalEntries={tableObj.entries.Count}"); } int MapSerializedTableToRuntimeIndex(string deviceName) { if (db == null || db.tables == null) return -1; for (int ti = 0; ti < db.tables.Count; ++ti) { if (string.Equals(db.tables[ti].deviceName, deviceName, StringComparison.OrdinalIgnoreCase)) return ti; } return -1; } }