From 843cd5e38e29b3d239d41d1559d88de236c98e35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E6=80=9D=E6=B5=B7?= <1464576565@qq.com> Date: Thu, 19 Mar 2026 13:59:42 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0InputGlyph=E7=BC=96=E8=BE=91?= =?UTF-8?q?=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Editor/InputGlyphDatabaseEditor.cs | 1448 ++++++++++------- .../InputGlyph/InputActionReader.cs | 1 - .../InputGlyph/InputGlyphDatabase.cs | 21 +- Client/UserSettings/Layouts/default-2022.dwlt | 116 +- 4 files changed, 927 insertions(+), 659 deletions(-) diff --git a/Client/Assets/Scripts/CustomeModule/InputGlyph/Editor/InputGlyphDatabaseEditor.cs b/Client/Assets/Scripts/CustomeModule/InputGlyph/Editor/InputGlyphDatabaseEditor.cs index 21ba557..c10b7fb 100644 --- a/Client/Assets/Scripts/CustomeModule/InputGlyph/Editor/InputGlyphDatabaseEditor.cs +++ b/Client/Assets/Scripts/CustomeModule/InputGlyph/Editor/InputGlyphDatabaseEditor.cs @@ -1,709 +1,961 @@ using System; using System.Collections.Generic; using UnityEditor; +using UnityEditorInternal; using UnityEngine; +using UnityEngine.InputSystem; [CustomEditor(typeof(InputGlyphDatabase))] -public class InputGlyphDatabaseEditor : Editor +public sealed class InputGlyphDatabaseEditor : Editor { - SerializedProperty tablesProp; - SerializedProperty placeholderSpriteProp; - InputGlyphDatabase db; + private const string TablesPropertyName = "tables"; + private const string PlaceholderSpritePropertyName = "placeholderSprite"; + private const string DeviceNamePropertyName = "deviceName"; + private const string SpriteSheetPropertyName = "spriteSheetTexture"; + private const string PlatformIconPropertyName = "platformIcons"; + private const string EntriesPropertyName = "entries"; + private const string EntrySpritePropertyName = "Sprite"; + private const string EntryActionPropertyName = "action"; + private const float PreviewSize = 52f; + private const float ListPreviewSize = 56f; + private const int MaxValidationIssuesToShow = 10; - int tabIndex = 0; - bool showAddField = false; - string newTableName = ""; + private static readonly string[] DefaultTableNames = { "Keyboard", "Xbox", "PlayStation", "Other" }; - List searchStrings = new List(); - List currentPages = new List(); - - // 每个表的临时字段,用于添加单个条目(目前仅支持 sprite) - List newEntrySprites = new List(); - - const int itemsPerPage = 10; - const int previewSize = 52; - - void OnEnable() + private sealed class TableEditorState { - db = target as InputGlyphDatabase; - tablesProp = serializedObject.FindProperty("tables"); - placeholderSpriteProp = serializedObject.FindProperty("placeholderSprite"); + public Sprite PendingSprite; + public bool ShowValidation = true; + public ReorderableList EntriesList; + public string EntriesPropertyPath; + } - if (tablesProp == null) - { - Debug.LogError("Could not find serialized property 'tables' on InputGlyphDatabase. Check field name."); - return; - } + private readonly List _tableStates = new(); - EnsureDefaultTable("Keyboard"); - EnsureDefaultTable("Xbox"); - EnsureDefaultTable("PlayStation"); + private InputGlyphDatabase _database; + private SerializedProperty _tablesProperty; + private SerializedProperty _placeholderSpriteProperty; + private int _selectedTab; + private bool _showAddTable; + private bool _showDatabaseValidation = true; + private string _newTableName = string.Empty; - SyncEditorListsWithTables(); + private bool IsSettingsSelected => _selectedTab >= TableCount; + private int TableCount => _database != null && _database.tables != null + ? _database.tables.Count + : (_tablesProperty != null ? _tablesProperty.arraySize : 0); + + private void OnEnable() + { + _database = target as InputGlyphDatabase; + _tablesProperty = serializedObject.FindProperty(TablesPropertyName); + _placeholderSpriteProperty = serializedObject.FindProperty(PlaceholderSpritePropertyName); + SyncTableStates(); + ClampSelectedTab(); } public override void OnInspectorGUI() { + if (_database == null || _tablesProperty == null) + { + DrawDefaultInspector(); + return; + } + serializedObject.Update(); - if (db == null || tablesProp == null) return; + SyncTableStates(); + ClampSelectedTab(); - EditorGUILayout.BeginHorizontal(EditorStyles.toolbar); - GUILayout.Space(4); - - if (GUILayout.Button("Save Asset", EditorStyles.toolbarButton)) + DrawToolbar(); + if (_showAddTable) { - serializedObject.ApplyModifiedProperties(); - EditorUtility.SetDirty(db); - AssetDatabase.SaveAssets(); + DrawAddTableBar(); } - GUILayout.FlexibleSpace(); + DrawMissingDefaultTablesNotice(); - if (GUILayout.Button(showAddField ? "Cancel +" : "+ Add Table", EditorStyles.toolbarButton, GUILayout.Width(110))) + EditorGUILayout.Space(6f); + DrawTabs(); + EditorGUILayout.Space(8f); + + using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox)) { - 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))) + if (IsSettingsSelected) { - 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)); + DrawSettingsPanel(); } else { - EditorGUILayout.HelpBox("No placeholder sprite assigned. If FindEntryByControlPath receives an empty path, it will return null.", MessageType.Info); + DrawTablePanel(_selectedTab); } } + + if (serializedObject.ApplyModifiedProperties()) + { + NotifyDatabaseChanged(); + } + } + + private void DrawToolbar() + { + using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar)) + { + if (GUILayout.Button("Save Asset", EditorStyles.toolbarButton, GUILayout.Width(90f))) + { + serializedObject.ApplyModifiedProperties(); + NotifyDatabaseChanged(true); + } + + if (HasMissingDefaultTables() && GUILayout.Button("Create Standard Tables", EditorStyles.toolbarButton, GUILayout.Width(140f))) + { + ApplyPendingInspectorChanges(); + CreateMissingDefaultTables(); + } + + GUILayout.FlexibleSpace(); + GUILayout.Label($"Tables: {TableCount}", EditorStyles.miniLabel); + + if (GUILayout.Button(_showAddTable ? "Cancel Add" : "+ Add Table", EditorStyles.toolbarButton, GUILayout.Width(90f))) + { + _showAddTable = !_showAddTable; + _newTableName = string.Empty; + GUI.FocusControl(null); + } + } + } + + private void DrawAddTableBar() + { + using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar)) + { + GUILayout.Label("Name", GUILayout.Width(40f)); + _newTableName = EditorGUILayout.TextField(_newTableName); + + using (new EditorGUI.DisabledScope(string.IsNullOrWhiteSpace(_newTableName))) + { + if (GUILayout.Button("Add", EditorStyles.toolbarButton, GUILayout.Width(70f))) + { + string trimmed = _newTableName.Trim(); + if (HasTable(trimmed)) + { + EditorUtility.DisplayDialog("Duplicate Table", $"A table named '{trimmed}' already exists.", "OK"); + } + else + { + ApplyPendingInspectorChanges(); + AddTable(trimmed); + _selectedTab = TableCount - 1; + _showAddTable = false; + _newTableName = string.Empty; + GUI.FocusControl(null); + } + } + } + } + } + + private void DrawMissingDefaultTablesNotice() + { + List missingTables = GetMissingDefaultTables(); + if (missingTables.Count == 0) + { + return; + } + + EditorGUILayout.HelpBox( + $"Recommended tables are missing: {string.Join(", ", missingTables)}. Glyph lookup still works, but missing categories may fall back to another table.", + MessageType.Info); + } + + private void DrawTabs() + { + using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar)) + { + for (int i = 0; i < TableCount; i++) + { + SerializedProperty tableProperty = _tablesProperty.GetArrayElementAtIndex(i); + string tableName = GetTableName(tableProperty, i); + bool selected = !IsSettingsSelected && _selectedTab == i; + + if (GUILayout.Toggle(selected, tableName, EditorStyles.toolbarButton, GUILayout.MinWidth(70f))) + { + _selectedTab = i; + } + + if (GUILayout.Button("X", EditorStyles.toolbarButton, GUILayout.Width(22f))) + { + if (EditorUtility.DisplayDialog("Delete Table", $"Delete table '{tableName}' and all of its entries?", "Delete", "Cancel")) + { + ApplyPendingInspectorChanges(); + RemoveTable(i); + GUIUtility.ExitGUI(); + } + } + } + + GUILayout.FlexibleSpace(); + + bool settingsSelected = IsSettingsSelected; + if (GUILayout.Toggle(settingsSelected, "Settings", EditorStyles.toolbarButton, GUILayout.Width(90f))) + { + _selectedTab = TableCount; + } + } + } + + private void DrawSettingsPanel() + { + EditorGUILayout.LabelField("Database Settings", EditorStyles.boldLabel); + EditorGUILayout.Space(4f); + EditorGUILayout.PropertyField(_placeholderSpriteProperty, new GUIContent("Placeholder Sprite")); + DrawSpritePreview(_placeholderSpriteProperty.objectReferenceValue as Sprite, "Preview"); + + EditorGUILayout.Space(8f); + DrawDatabaseValidationPanel(); + } + + private void DrawDatabaseValidationPanel() + { + List issues = CollectDatabaseValidationIssues(); + string title = issues.Count == 0 ? "Database Validation" : $"Database Validation ({issues.Count})"; + _showDatabaseValidation = EditorGUILayout.BeginFoldoutHeaderGroup(_showDatabaseValidation, title); + if (_showDatabaseValidation) + { + if (issues.Count == 0) + { + EditorGUILayout.HelpBox("No database-level issues found.", MessageType.Info); + } + else + { + DrawValidationList(issues, MessageType.Warning); + } + } + + EditorGUILayout.EndFoldoutHeaderGroup(); + } + + private void DrawTablePanel(int tableIndex) + { + if (tableIndex < 0 || tableIndex >= TableCount) + { + EditorGUILayout.HelpBox("Select a valid table.", MessageType.Info); + return; + } + + TableEditorState state = _tableStates[tableIndex]; + SerializedProperty tableProperty = _tablesProperty.GetArrayElementAtIndex(tableIndex); + SerializedProperty nameProperty = tableProperty.FindPropertyRelative(DeviceNamePropertyName); + SerializedProperty spriteSheetProperty = tableProperty.FindPropertyRelative(SpriteSheetPropertyName); + SerializedProperty platformIconProperty = tableProperty.FindPropertyRelative(PlatformIconPropertyName); + SerializedProperty entriesProperty = tableProperty.FindPropertyRelative(EntriesPropertyName); + + EditorGUILayout.LabelField("Table", EditorStyles.boldLabel); + EditorGUILayout.PropertyField(nameProperty, new GUIContent("Device Name")); + if (HasDuplicateTableName(nameProperty.stringValue, tableIndex)) + { + EditorGUILayout.HelpBox("Table names should be unique. Duplicate names can make lookups unpredictable.", MessageType.Warning); + } + + EditorGUILayout.Space(6f); + DrawInlinePropertyWithPreview(EditorGUILayout.GetControlRect(false, PreviewSize), new GUIContent("Platform Icon"), platformIconProperty, platformIconProperty.objectReferenceValue as Sprite); + + EditorGUILayout.Space(6f); + using (new EditorGUILayout.HorizontalScope()) + { + EditorGUILayout.PropertyField(spriteSheetProperty, new GUIContent("Sprite Sheet"), true); + + if (GUILayout.Button("Merge Sprite Sheet")) + { + ApplyPendingInspectorChanges(); + MergeSpriteSheet(tableIndex); + } + + using (new EditorGUI.DisabledScope(_database.tables[tableIndex].entries == null || _database.tables[tableIndex].entries.Count == 0)) + { + if (GUILayout.Button("Clear Entries")) + { + if (EditorUtility.DisplayDialog("Clear Entries", $"Remove all entries from '{nameProperty.stringValue}'?", "Clear", "Cancel")) + { + ApplyPendingInspectorChanges(); + ClearEntries(tableIndex); + } + } + } + } + + EditorGUILayout.Space(6f); + DrawQuickAddEntry(tableIndex, state); + + EditorGUILayout.Space(8f); + DrawTableValidationPanel(tableIndex, state); + + EditorGUILayout.Space(8f); + DrawEntriesList(tableIndex, entriesProperty); + } + + private void DrawQuickAddEntry(int tableIndex, TableEditorState state) + { + using (new EditorGUILayout.HorizontalScope(EditorStyles.helpBox)) + { + state.PendingSprite = (Sprite)EditorGUILayout.ObjectField("Sprite", state.PendingSprite, typeof(Sprite), false); + + using (new EditorGUI.DisabledScope(state.PendingSprite == null)) + { + if (GUILayout.Button("Add Entry", GUILayout.Width(90f))) + { + ApplyPendingInspectorChanges(); + AddEntry(tableIndex, state.PendingSprite); + state.PendingSprite = null; + } + } + } + } + + private void DrawTableValidationPanel(int tableIndex, TableEditorState state) + { + List issues = CollectTableValidationIssues(tableIndex); + string title = issues.Count == 0 ? "Validation" : $"Validation ({issues.Count})"; + state.ShowValidation = EditorGUILayout.BeginFoldoutHeaderGroup(state.ShowValidation, title); + if (state.ShowValidation) + { + if (issues.Count == 0) + { + EditorGUILayout.HelpBox("No table-level issues found.", MessageType.Info); + } + else + { + DrawValidationList(issues, MessageType.Warning); + } + } + + EditorGUILayout.EndFoldoutHeaderGroup(); + } + + private void DrawValidationList(List issues, MessageType messageType) + { + int visibleCount = Mathf.Min(MaxValidationIssuesToShow, issues.Count); + for (int i = 0; i < visibleCount; i++) + { + EditorGUILayout.HelpBox(issues[i], messageType); + } + + if (issues.Count > visibleCount) + { + EditorGUILayout.HelpBox($"{issues.Count - visibleCount} more issues are hidden to keep the inspector readable.", MessageType.None); + } + } + + private void DrawEntriesList(int tableIndex, SerializedProperty entriesProperty) + { + EditorGUILayout.LabelField("Entries", EditorStyles.boldLabel); + ReorderableList list = GetEntriesList(tableIndex, entriesProperty); + list.DoLayoutList(); + } + private ReorderableList GetEntriesList(int tableIndex, SerializedProperty entriesProperty) + { + TableEditorState state = _tableStates[tableIndex]; + if (state.EntriesList != null && state.EntriesPropertyPath == entriesProperty.propertyPath) + { + return state.EntriesList; + } + + ReorderableList list = new ReorderableList(serializedObject, entriesProperty, true, true, false, true); + state.EntriesList = list; + state.EntriesPropertyPath = entriesProperty.propertyPath; + + list.drawHeaderCallback = rect => + { + EditorGUI.LabelField(rect, $"Entries ({entriesProperty.arraySize}) - drag to reorder"); + }; + + list.elementHeightCallback = index => + { + if (index < 0 || index >= entriesProperty.arraySize) + { + return EditorGUIUtility.singleLineHeight + 8f; + } + + return GetEntryElementHeight(entriesProperty.GetArrayElementAtIndex(index)); + }; + + list.drawElementCallback = (rect, index, active, focused) => + { + if (index < 0 || index >= entriesProperty.arraySize) + { + return; + } + + DrawEntryElement(rect, entriesProperty.GetArrayElementAtIndex(index)); + }; + + list.onReorderCallback = _ => + { + ApplyPendingInspectorChanges(); + }; + + list.onRemoveCallback = currentList => + { + if (currentList.index < 0 || currentList.index >= entriesProperty.arraySize) + { + return; + } + + if (!EditorUtility.DisplayDialog("Remove Entry", "Remove the selected entry from the table?", "Remove", "Cancel")) + { + return; + } + + Undo.RecordObject(_database, "Remove glyph entry"); + entriesProperty.DeleteArrayElementAtIndex(currentList.index); + serializedObject.ApplyModifiedProperties(); + InvalidateEntriesList(tableIndex); + NotifyDatabaseChanged(); + }; + + return list; + } + + private float GetEntryElementHeight(SerializedProperty entryProperty) + { + SerializedProperty spriteProperty = entryProperty.FindPropertyRelative(EntrySpritePropertyName); + SerializedProperty actionProperty = entryProperty.FindPropertyRelative(EntryActionPropertyName); + float spriteHeight = EditorGUI.GetPropertyHeight(spriteProperty, true); + float actionHeight = EditorGUI.GetPropertyHeight(actionProperty, true); + float fieldHeight = spriteHeight + actionHeight + 10f; + return Mathf.Max(ListPreviewSize + 10f, fieldHeight + 8f); + } + + private void DrawEntryElement(Rect rect, SerializedProperty entryProperty) + { + SerializedProperty spriteProperty = entryProperty.FindPropertyRelative(EntrySpritePropertyName); + SerializedProperty actionProperty = entryProperty.FindPropertyRelative(EntryActionPropertyName); + Sprite sprite = spriteProperty.objectReferenceValue as Sprite; + + rect.y += 4f; + rect.height -= 8f; + + Rect previewRect = new Rect(rect.x, rect.y, ListPreviewSize, ListPreviewSize); + Rect fieldsRect = new Rect(rect.x + ListPreviewSize + 8f, rect.y, rect.width - ListPreviewSize - 44f, rect.height); + Rect pingRect = new Rect(rect.xMax - 30f, rect.y, 30f, EditorGUIUtility.singleLineHeight); + + DrawSpritePreview(previewRect, sprite); + + float currentY = fieldsRect.y; + float spriteHeight = EditorGUI.GetPropertyHeight(spriteProperty, true); + Rect spriteRect = new Rect(fieldsRect.x, currentY, fieldsRect.width, spriteHeight); + EditorGUI.PropertyField(spriteRect, spriteProperty, new GUIContent("Sprite"), true); + + currentY += spriteHeight + 4f; + float actionHeight = EditorGUI.GetPropertyHeight(actionProperty, true); + Rect actionRect = new Rect(fieldsRect.x, currentY, fieldsRect.width, actionHeight); + EditorGUI.PropertyField(actionRect, actionProperty, new GUIContent("Action"), true); + + using (new EditorGUI.DisabledScope(sprite == null)) + { + if (GUI.Button(pingRect, "Ping")) + { + EditorGUIUtility.PingObject(sprite); + } + } + } + + private void DrawSpritePreview(Sprite sprite, string label) + { + EditorGUILayout.LabelField(label, EditorStyles.miniBoldLabel); + Rect previewRect = GUILayoutUtility.GetRect(PreviewSize, PreviewSize, GUILayout.Width(PreviewSize), GUILayout.Height(PreviewSize)); + DrawSpritePreview(previewRect, sprite); + } + + private void DrawInlinePropertyWithPreview(Rect rect, GUIContent label, SerializedProperty property, Sprite previewSprite) + { + float previewWidth = PreviewSize; + float gap = 6f; + Rect fieldRect = new Rect(rect.x, rect.y, Mathf.Max(60f, rect.width - previewWidth - gap), rect.height); + Rect previewRect = new Rect(fieldRect.xMax + gap, rect.y, previewWidth, PreviewSize); + + EditorGUI.PropertyField(fieldRect, property, label, true); + if (Event.current.type == EventType.Repaint || Event.current.type == EventType.Layout) + { + DrawSpritePreview(previewRect, previewSprite); + } + } + + private void DrawSpritePreview(Rect rect, Sprite sprite) + { + if (sprite == null) + { + EditorGUI.HelpBox(rect, "None", MessageType.None); + return; + } + + Texture2D preview = AssetPreview.GetAssetPreview(sprite); + if (preview == null) + { + preview = AssetPreview.GetMiniThumbnail(sprite); + } + + if (preview != null) + { + GUI.DrawTexture(rect, preview, ScaleMode.ScaleToFit); + } else { - if (tabIndex < 0 || tabIndex >= tablesProp.arraySize) + EditorGUI.ObjectField(rect, sprite, typeof(Sprite), false); + } + } + + private List CollectDatabaseValidationIssues() + { + List issues = new(); + if (_database.tables == null || _database.tables.Count == 0) + { + issues.Add("The database has no tables. Runtime glyph lookup will always fall back to the placeholder sprite."); + return issues; + } + + List missingTables = GetMissingDefaultTables(); + if (missingTables.Count > 0) + { + issues.Add($"Recommended tables are missing: {string.Join(", ", missingTables)}."); + } + + HashSet seenNames = new(StringComparer.OrdinalIgnoreCase); + HashSet duplicateNames = new(StringComparer.OrdinalIgnoreCase); + for (int i = 0; i < _database.tables.Count; i++) + { + string tableName = _database.tables[i] != null ? _database.tables[i].deviceName : string.Empty; + if (string.IsNullOrWhiteSpace(tableName)) { - EditorGUILayout.HelpBox("Invalid table index.", MessageType.Error); + issues.Add($"Table {i + 1} has an empty device name."); + continue; } - else + + if (!seenNames.Add(tableName)) { - 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); - } - } + duplicateNames.Add(tableName); } } - EditorGUILayout.EndVertical(); - - EditorGUILayout.Space(6); - - serializedObject.ApplyModifiedProperties(); - } - - void EnsureDefaultTable(string name) - { - if (tablesProp == null) return; - for (int i = 0; i < tablesProp.arraySize; ++i) + foreach (string duplicateName in duplicateNames) { - var t = tablesProp.GetArrayElementAtIndex(i); - var nameProp = t.FindPropertyRelative("deviceName"); - if (nameProp != null && string.Equals(nameProp.stringValue, name, StringComparison.OrdinalIgnoreCase)) - return; + issues.Add($"Duplicate table name '{duplicateName}' detected."); } - 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); + return issues; } - void SyncEditorListsWithTables() + private List CollectTableValidationIssues(int tableIndex) { - 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) + List issues = new(); + if (!IsValidTableIndex(tableIndex)) { - var spriteProp = newE.FindPropertyRelative("Sprite"); - var actionProp = newE.FindPropertyRelative("action"); + issues.Add("The selected table is invalid."); + return issues; + } - if (spriteProp != null) spriteProp.objectReferenceValue = sprite; + DeviceGlyphTable table = _database.tables[tableIndex]; + if (table.entries == null || table.entries.Count == 0) + { + issues.Add("This table has no entries."); + return issues; + } - // 保持 action 序列化不变(大多数项目无法在此处直接序列化 InputAction) - if (actionProp != null) + int missingSpriteCount = 0; + int missingActionCount = 0; + HashSet seenSprites = new(StringComparer.OrdinalIgnoreCase); + HashSet duplicateSprites = new(StringComparer.OrdinalIgnoreCase); + Dictionary> bindingOwners = new(StringComparer.OrdinalIgnoreCase); + + for (int i = 0; i < table.entries.Count; i++) + { + GlyphEntry entry = table.entries[i]; + if (entry == null) { - try - { - actionProp.objectReferenceValue = null; - } - catch - { - } + issues.Add($"Entry {i + 1} is null."); + continue; + } + + if (entry.Sprite == null) + { + missingSpriteCount++; + } + else if (!seenSprites.Add(entry.Sprite.name)) + { + duplicateSprites.Add(entry.Sprite.name); + } + + if (entry.action == null) + { + missingActionCount++; + continue; + } + + string entryLabel = entry.Sprite != null ? entry.Sprite.name : $"Entry {i + 1}"; + for (int bindingIndex = 0; bindingIndex < entry.action.bindings.Count; bindingIndex++) + { + InputBinding binding = entry.action.bindings[bindingIndex]; + RegisterBindingOwner(bindingOwners, binding.path, entryLabel); + RegisterBindingOwner(bindingOwners, binding.effectivePath, entryLabel); } } - 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) + if (missingSpriteCount > 0) { - var tableObj = db.tables[tableIndex]; - GlyphEntry e = new GlyphEntry(); - e.Sprite = sprite; - e.action = null; // runtime only: none provided here - tableObj.entries.Add(e); + issues.Add($"{missingSpriteCount} entr{(missingSpriteCount == 1 ? "y has" : "ies have")} no sprite assigned."); } - EditorUtility.SetDirty(db); - AssetDatabase.SaveAssets(); + if (missingActionCount > 0) + { + issues.Add($"{missingActionCount} entr{(missingActionCount == 1 ? "y has" : "ies have")} no action assigned. Those entries will not participate in runtime path lookup."); + } + foreach (string spriteName in duplicateSprites) + { + issues.Add($"Duplicate sprite name '{spriteName}' found in this table."); + } + + foreach (KeyValuePair> pair in bindingOwners) + { + if (pair.Value.Count <= 1) + { + continue; + } + + issues.Add($"Binding '{pair.Key}' is mapped by multiple entries: {string.Join(", ", pair.Value)}. Runtime lookup keeps the first match."); + } + + return issues; } - -// ----- Parse Sprite Sheet (Texture2D with Multiple) ----- -// 只用名称匹配覆盖 Sprite,不改变已有 action - void ParseSpriteSheetIntoTableSerialized(SerializedProperty tableProp) + private void RegisterBindingOwner(Dictionary> bindingOwners, string controlPath, string ownerLabel) { - if (tableProp == null) return; - - var sheetProp = tableProp.FindPropertyRelative("spriteSheetTexture"); - var tex = sheetProp != null ? sheetProp.objectReferenceValue as Texture2D : null; - if (tex == null) + string normalizedPath = InputGlyphDatabase.EditorNormalizeControlPath(controlPath); + if (string.IsNullOrEmpty(normalizedPath)) { - 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) + if (!bindingOwners.TryGetValue(normalizedPath, out List owners)) + { + owners = new List(); + bindingOwners.Add(normalizedPath, owners); + } + + if (!owners.Contains(ownerLabel)) + { + owners.Add(ownerLabel); + } + } + + private void AddEntry(int tableIndex, Sprite sprite) + { + if (sprite == null || !IsValidTableIndex(tableIndex)) { - Debug.LogError($"[InputGlyphDatabase] Could not map serialized table '{deviceName}' to runtime db.tables."); return; } - var tableObj = db.tables[tableIndex]; - if (tableObj == null) + Undo.RecordObject(_database, "Add glyph entry"); + DeviceGlyphTable table = _database.tables[tableIndex]; + table.entries ??= new List(); + table.entries.Add(new GlyphEntry { Sprite = sprite, action = null }); + serializedObject.Update(); + InvalidateEntriesList(tableIndex); + NotifyDatabaseChanged(); + } + + private void ClearEntries(int tableIndex) + { + if (!IsValidTableIndex(tableIndex)) { - Debug.LogError($"[InputGlyphDatabase] Runtime table object is null for '{deviceName}'."); return; } - string path = AssetDatabase.GetAssetPath(tex); + Undo.RecordObject(_database, "Clear glyph entries"); + DeviceGlyphTable table = _database.tables[tableIndex]; + table.entries ??= new List(); + table.entries.Clear(); + serializedObject.Update(); + InvalidateEntriesList(tableIndex); + NotifyDatabaseChanged(); + } + + private void AddTable(string tableName) + { + Undo.RecordObject(_database, "Add glyph table"); + _database.tables ??= new List(); + _database.tables.Add(new DeviceGlyphTable + { + deviceName = tableName, + spriteSheetTexture = null, + platformIcons = null, + entries = new List() + }); + SyncTableStates(); + serializedObject.Update(); + InvalidateAllEntriesLists(); + NotifyDatabaseChanged(); + } + + private void RemoveTable(int tableIndex) + { + if (!IsValidTableIndex(tableIndex)) + { + return; + } + + Undo.RecordObject(_database, "Remove glyph table"); + _database.tables.RemoveAt(tableIndex); + SyncTableStates(); + ClampSelectedTab(); + serializedObject.Update(); + InvalidateAllEntriesLists(); + NotifyDatabaseChanged(); + } + + private void CreateMissingDefaultTables() + { + List missingTables = GetMissingDefaultTables(); + if (missingTables.Count == 0) + { + return; + } + + Undo.RecordObject(_database, "Create standard glyph tables"); + _database.tables ??= new List(); + for (int i = 0; i < missingTables.Count; i++) + { + _database.tables.Add(new DeviceGlyphTable + { + deviceName = missingTables[i], + spriteSheetTexture = null, + platformIcons = null, + entries = new List() + }); + } + + SyncTableStates(); + serializedObject.Update(); + InvalidateAllEntriesLists(); + NotifyDatabaseChanged(); + } + + private void MergeSpriteSheet(int tableIndex) + { + if (!IsValidTableIndex(tableIndex)) + { + return; + } + + DeviceGlyphTable table = _database.tables[tableIndex]; + if (table.spriteSheetTexture == null) + { + EditorUtility.DisplayDialog("Missing Sprite Sheet", "Assign a sprite sheet texture first.", "OK"); + return; + } + + string path = AssetDatabase.GetAssetPath(table.spriteSheetTexture); if (string.IsNullOrEmpty(path)) { - Debug.LogWarning("[InputGlyphDatabase] Could not get asset path for texture."); + Debug.LogWarning("[InputGlyphDatabase] Could not resolve the sprite sheet asset path."); return; } - var assets = AssetDatabase.LoadAllAssetsAtPath(path); + UnityEngine.Object[] assets = AssetDatabase.LoadAllAssetsAtPath(path); if (assets == null || assets.Length == 0) { - Debug.LogWarning("[InputGlyphDatabase] No sub-assets found at path: " + path); + Debug.LogWarning($"[InputGlyphDatabase] No sub-assets found at '{path}'."); return; } - // 收集 sprites(按照文件内顺序;你如果想按名字排序可以在这里加) - List sprites = new List(); - foreach (var a in assets) + List sprites = new(); + for (int i = 0; i < assets.Length; i++) { - if (a is Sprite sp) sprites.Add(sp); + if (assets[i] is Sprite sprite) + { + sprites.Add(sprite); + } } - var entriesProp = tableProp.FindPropertyRelative("entries"); - if (entriesProp == null) + if (sprites.Count == 0) { - Debug.LogWarning("[InputGlyphDatabase] entries property not found on table."); + EditorUtility.DisplayDialog("No Sprites Found", "The selected texture does not contain any sprite sub-assets.", "OK"); return; } - // 构建序列化表名 -> 索引 映射(忽略大小写) - var serializedNameToIndex = new Dictionary(StringComparer.OrdinalIgnoreCase); - for (int i = 0; i < entriesProp.arraySize; ++i) + Undo.RecordObject(_database, "Merge glyph sprite sheet"); + table.entries ??= new List(); + + Dictionary entriesByName = new(StringComparer.OrdinalIgnoreCase); + for (int i = 0; i < table.entries.Count; 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)) + GlyphEntry entry = table.entries[i]; + if (entry?.Sprite == null) { - serializedNameToIndex[sRef.name] = i; + continue; + } + + if (!entriesByName.ContainsKey(entry.Sprite.name)) + { + entriesByName.Add(entry.Sprite.name, entry); } } - // runtime 名称 -> 索引 映射 - var runtimeNameToIndex = new Dictionary(StringComparer.OrdinalIgnoreCase); - for (int i = 0; i < tableObj.entries.Count; ++i) + int replaced = 0; + int added = 0; + for (int i = 0; i < sprites.Count; i++) { - var re = tableObj.entries[i]; - if (re != null && re.Sprite != null) + Sprite sprite = sprites[i]; + if (entriesByName.TryGetValue(sprite.name, out GlyphEntry entry)) { - 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) + if (entry.Sprite != sprite) { - var spriteProp = eProp.FindPropertyRelative("Sprite"); - if (spriteProp != null) spriteProp.objectReferenceValue = sp; - // 不修改 actionProp,保持原有 action(如果有的话) + entry.Sprite = sprite; + replaced++; } - - 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; + GlyphEntry newEntry = new GlyphEntry { Sprite = sprite, action = null }; + table.entries.Add(newEntry); + entriesByName.Add(sprite.name, newEntry); 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}"); + InvalidateEntriesList(tableIndex); + NotifyDatabaseChanged(); + Debug.Log($"[InputGlyphDatabase] Merged sprite sheet '{table.spriteSheetTexture.name}' into '{table.deviceName}'. sprites={sprites.Count}, replaced={replaced}, added={added}, total={table.entries.Count}"); } - int MapSerializedTableToRuntimeIndex(string deviceName) + private void ApplyPendingInspectorChanges() { - if (db == null || db.tables == null) return -1; - for (int ti = 0; ti < db.tables.Count; ++ti) + if (serializedObject.ApplyModifiedProperties()) { - if (string.Equals(db.tables[ti].deviceName, deviceName, StringComparison.OrdinalIgnoreCase)) - return ti; + NotifyDatabaseChanged(); + } + } + + private void NotifyDatabaseChanged(bool saveAssets = false) + { + _database.EditorRefreshCache(); + EditorUtility.SetDirty(_database); + if (saveAssets) + { + AssetDatabase.SaveAssets(); + } + } + private void SyncTableStates() + { + int count = TableCount; + if (_tableStates.Count == count) + { + return; } - return -1; + _tableStates.Clear(); + for (int i = 0; i < count; i++) + { + _tableStates.Add(new TableEditorState()); + } + } + + private void ClampSelectedTab() + { + int maxIndex = Mathf.Max(0, TableCount); + _selectedTab = Mathf.Clamp(_selectedTab, 0, maxIndex); + } + + private void InvalidateEntriesList(int tableIndex) + { + if (tableIndex < 0 || tableIndex >= _tableStates.Count) + { + return; + } + + _tableStates[tableIndex].EntriesList = null; + _tableStates[tableIndex].EntriesPropertyPath = null; + } + + private void InvalidateAllEntriesLists() + { + for (int i = 0; i < _tableStates.Count; i++) + { + _tableStates[i].EntriesList = null; + _tableStates[i].EntriesPropertyPath = null; + } + } + + private bool IsValidTableIndex(int tableIndex) + { + return _database != null + && _database.tables != null + && tableIndex >= 0 + && tableIndex < _database.tables.Count; + } + + private string GetTableName(SerializedProperty tableProperty, int fallbackIndex) + { + SerializedProperty nameProperty = tableProperty.FindPropertyRelative(DeviceNamePropertyName); + return string.IsNullOrWhiteSpace(nameProperty.stringValue) ? $"Table {fallbackIndex + 1}" : nameProperty.stringValue; + } + + private bool HasTable(string tableName) + { + if (string.IsNullOrWhiteSpace(tableName) || _database.tables == null) + { + return false; + } + + for (int i = 0; i < _database.tables.Count; i++) + { + if (string.Equals(_database.tables[i].deviceName, tableName, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + return false; + } + + private bool HasDuplicateTableName(string tableName, int selfIndex) + { + if (string.IsNullOrWhiteSpace(tableName) || _database.tables == null) + { + return false; + } + + for (int i = 0; i < _database.tables.Count; i++) + { + if (i == selfIndex) + { + continue; + } + + if (string.Equals(_database.tables[i].deviceName, tableName, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + return false; + } + + private bool HasMissingDefaultTables() + { + return GetMissingDefaultTables().Count > 0; + } + + private List GetMissingDefaultTables() + { + List missingTables = new(); + for (int i = 0; i < DefaultTableNames.Length; i++) + { + if (!HasTable(DefaultTableNames[i])) + { + missingTables.Add(DefaultTableNames[i]); + } + } + + return missingTables; } } diff --git a/Client/Assets/Scripts/CustomeModule/InputGlyph/InputActionReader.cs b/Client/Assets/Scripts/CustomeModule/InputGlyph/InputActionReader.cs index 3d6d416..3c97638 100644 --- a/Client/Assets/Scripts/CustomeModule/InputGlyph/InputActionReader.cs +++ b/Client/Assets/Scripts/CustomeModule/InputGlyph/InputActionReader.cs @@ -6,7 +6,6 @@ using UnityEngine.InputSystem; /// /// 输入读取工具。 /// 负责运行时输入轮询、单次触发和切换态管理, -/// 与 InputBindingManager 的绑定/重绑定职责分离。 /// public static class InputActionReader { diff --git a/Client/Assets/Scripts/CustomeModule/InputGlyph/InputGlyphDatabase.cs b/Client/Assets/Scripts/CustomeModule/InputGlyph/InputGlyphDatabase.cs index 7d4084e..0102c1f 100644 --- a/Client/Assets/Scripts/CustomeModule/InputGlyph/InputGlyphDatabase.cs +++ b/Client/Assets/Scripts/CustomeModule/InputGlyph/InputGlyphDatabase.cs @@ -72,7 +72,7 @@ public sealed class InputGlyphDatabase : ScriptableObject } EnsureCache(); - _tableCache.TryGetValue(deviceName.ToLowerInvariant(), out DeviceGlyphTable table); + _tableCache.TryGetValue(deviceName, out DeviceGlyphTable table); return table; } @@ -175,6 +175,11 @@ public sealed class InputGlyphDatabase : ScriptableObject InitializeLookup(InputDeviceWatcher.InputDeviceCategory.PlayStation); InitializeLookup(InputDeviceWatcher.InputDeviceCategory.Other); + if (tables == null) + { + return; + } + for (int i = 0; i < tables.Count; i++) { DeviceGlyphTable table = tables[i]; @@ -183,13 +188,25 @@ public sealed class InputGlyphDatabase : ScriptableObject continue; } - _tableCache[table.deviceName.ToLowerInvariant()] = table; + _tableCache[table.deviceName] = table; InputDeviceWatcher.InputDeviceCategory category = ParseCategory(table.deviceName); Dictionary map = _pathLookup[category]; RegisterEntries(table, map); } } +#if UNITY_EDITOR + public void EditorRefreshCache() + { + BuildCache(); + } + + public static string EditorNormalizeControlPath(string controlPath) + { + return NormalizeControlPath(controlPath); + } +#endif + private void InitializeLookup(InputDeviceWatcher.InputDeviceCategory category) { _pathLookup[category] = new Dictionary(StringComparer.OrdinalIgnoreCase); diff --git a/Client/UserSettings/Layouts/default-2022.dwlt b/Client/UserSettings/Layouts/default-2022.dwlt index 6096c58..13083e5 100644 --- a/Client/UserSettings/Layouts/default-2022.dwlt +++ b/Client/UserSettings/Layouts/default-2022.dwlt @@ -14,8 +14,8 @@ MonoBehaviour: m_EditorClassIdentifier: m_PixelRect: serializedVersion: 2 - x: 0 - y: 43 + x: 1920 + y: 48 width: 1920 height: 997 m_ShowMode: 4 @@ -41,10 +41,10 @@ MonoBehaviour: serializedVersion: 2 x: 0 y: 566 - width: 308 + width: 505 height: 381 - m_MinSize: {x: 51, y: 71} - m_MaxSize: {x: 4001, y: 4021} + m_MinSize: {x: 50, y: 50} + m_MaxSize: {x: 4000, y: 4000} m_ActualView: {fileID: 14} m_Panes: - {fileID: 14} @@ -70,7 +70,7 @@ MonoBehaviour: serializedVersion: 2 x: 0 y: 0 - width: 308 + width: 505 height: 947 m_MinSize: {x: 100, y: 100} m_MaxSize: {x: 8096, y: 16192} @@ -174,7 +174,7 @@ MonoBehaviour: m_MinSize: {x: 400, y: 100} m_MaxSize: {x: 32384, y: 16192} vertical: 0 - controlID: 144 + controlID: 104 draggingID: 0 --- !u!114 &8 MonoBehaviour: @@ -193,7 +193,7 @@ MonoBehaviour: serializedVersion: 2 x: 0 y: 0 - width: 308 + width: 505 height: 566 m_MinSize: {x: 201, y: 221} m_MaxSize: {x: 4001, y: 4021} @@ -219,9 +219,9 @@ MonoBehaviour: - {fileID: 11} m_Position: serializedVersion: 2 - x: 308 + x: 505 y: 0 - width: 387 + width: 223 height: 947 m_MinSize: {x: 100, y: 100} m_MaxSize: {x: 8096, y: 16192} @@ -245,10 +245,10 @@ MonoBehaviour: serializedVersion: 2 x: 0 y: 0 - width: 387 + width: 223 height: 409 - m_MinSize: {x: 202, y: 221} - m_MaxSize: {x: 4002, y: 4021} + m_MinSize: {x: 200, y: 200} + m_MaxSize: {x: 4000, y: 4000} m_ActualView: {fileID: 17} m_Panes: - {fileID: 17} @@ -271,10 +271,10 @@ MonoBehaviour: serializedVersion: 2 x: 0 y: 409 - width: 387 + width: 223 height: 538 - m_MinSize: {x: 102, y: 121} - m_MaxSize: {x: 4002, y: 4021} + m_MinSize: {x: 100, y: 100} + m_MaxSize: {x: 4000, y: 4000} m_ActualView: {fileID: 18} m_Panes: - {fileID: 18} @@ -295,9 +295,9 @@ MonoBehaviour: m_Children: [] m_Position: serializedVersion: 2 - x: 695 + x: 728 y: 0 - width: 607 + width: 574 height: 947 m_MinSize: {x: 232, y: 271} m_MaxSize: {x: 10002, y: 10021} @@ -352,9 +352,9 @@ MonoBehaviour: m_Tooltip: m_Pos: serializedVersion: 2 - x: 0 - y: 639 - width: 307 + x: 1920 + y: 644 + width: 504 height: 360 m_SerializedDataModeController: m_DataMode: 0 @@ -380,7 +380,7 @@ MonoBehaviour: m_UseMipMap: 0 m_VSyncEnabled: 0 m_Gizmos: 0 - m_Stats: 1 + m_Stats: 0 m_SelectedSizes: 03000000000000000000000000000000000000000000000000000000000000000000000000000000 m_ZoomArea: m_HRangeLocked: 0 @@ -408,23 +408,23 @@ MonoBehaviour: serializedVersion: 2 x: 0 y: 21 - width: 307 + width: 504 height: 339 - m_Scale: {x: 0.15989584, y: 0.15989585} - m_Translation: {x: 153.5, y: 169.5} + m_Scale: {x: 0.2625, y: 0.2625} + m_Translation: {x: 252, y: 169.5} m_MarginLeft: 0 m_MarginRight: 0 m_MarginTop: 0 m_MarginBottom: 0 m_LastShownAreaInsideMargins: serializedVersion: 2 - x: -960 - y: -1060.0651 - width: 1920 - height: 2120.1301 + x: -960.00006 + y: -645.7143 + width: 1920.0001 + height: 1291.4286 m_MinimalGUI: 1 - m_defaultScale: 0.15989584 - m_LastWindowPixelSize: {x: 307, y: 360} + m_defaultScale: 0.2625 + m_LastWindowPixelSize: {x: 504, y: 360} m_ClearInEditMode: 1 m_NoCameraWarning: 1 m_LowResolutionForAspectRatios: 01000000000000000000 @@ -520,9 +520,9 @@ MonoBehaviour: m_Tooltip: m_Pos: serializedVersion: 2 - x: 0 - y: 73 - width: 307 + x: 1920 + y: 78 + width: 504 height: 545 m_SerializedDataModeController: m_DataMode: 0 @@ -1066,7 +1066,7 @@ MonoBehaviour: m_Position: m_Target: {x: 1000, y: 1000, z: 100} speed: 2 - m_Value: {x: 1000, y: 1000, z: 100} + m_Value: {x: 0, y: 0, z: 0} m_RenderMode: 0 m_CameraMode: drawMode: 0 @@ -1104,7 +1104,7 @@ MonoBehaviour: m_Fade: m_Target: 0 speed: 2 - m_Value: 0 + m_Value: 1 m_Color: {r: 0.5, g: 0.5, b: 0.5, a: 0.4} m_Pivot: {x: 0, y: 0, z: 0} m_Size: {x: 1, y: 1} @@ -1114,11 +1114,11 @@ MonoBehaviour: m_Rotation: m_Target: {x: -0.21037178, y: -0.10913931, z: 0.02363893, w: -0.97122556} speed: 2 - m_Value: {x: -0.2103712, y: -0.10913901, z: 0.023638865, w: -0.9712229} + m_Value: {x: 0, y: 0, z: 0, w: 1} m_Size: m_Target: 0.7536363 speed: 2 - m_Value: 0.7536363 + m_Value: 10 m_Ortho: m_Target: 0 speed: 2 @@ -1163,9 +1163,9 @@ MonoBehaviour: m_Tooltip: m_Pos: serializedVersion: 2 - x: 308 - y: 73 - width: 385 + x: 2425 + y: 78 + width: 221 height: 388 m_SerializedDataModeController: m_DataMode: 0 @@ -1180,9 +1180,9 @@ MonoBehaviour: m_SceneHierarchy: m_TreeViewState: scrollPos: {x: 0, y: 0} - m_SelectedIDs: 02200000 + m_SelectedIDs: m_LastClickedID: 0 - m_ExpandedIDs: aebbfffff2d0fffffad0fffffcd0ffff26fbffff28fbffff + m_ExpandedIDs: ccf9ffff m_RenameOverlay: m_UserAcceptedRename: 0 m_Name: @@ -1226,9 +1226,9 @@ MonoBehaviour: m_Tooltip: m_Pos: serializedVersion: 2 - x: 308 - y: 482 - width: 385 + x: 2425 + y: 487 + width: 221 height: 517 m_SerializedDataModeController: m_DataMode: 0 @@ -1260,9 +1260,9 @@ MonoBehaviour: m_Tooltip: m_Pos: serializedVersion: 2 - x: 695 - y: 73 - width: 605 + x: 2648 + y: 78 + width: 572 height: 926 m_SerializedDataModeController: m_DataMode: 0 @@ -1285,7 +1285,7 @@ MonoBehaviour: m_SkipHidden: 0 m_SearchArea: 2 m_Folders: - - Assets/Scripts/Hotfix/GameLogic + - Packages/com.alicizax.unity.framework m_Globs: [] m_OriginalText: m_ImportLogFlags: 0 @@ -1301,7 +1301,7 @@ MonoBehaviour: scrollPos: {x: 0, y: 0} m_SelectedIDs: e48c0000 m_LastClickedID: 36068 - m_ExpandedIDs: 000000007e0200008c0d0000246c00008a6d00008c6d00008e6d0000906d0000926d0000946d0000966d0000986d00009a6d00009c6d00009e6d0000a06d0000a26d0000a46d0000a66d0000a86d0000aa6d0000ac6d0000ae6d0000b06d0000b26d0000b46d0000b66d0000b86d0000ba6d0000bc6d0000be6d0000c06d0000c26d0000c46d0000c66d0000c86d0000ca6d0000cc6d0000ce6d0000d06d0000d26d0000d46d0000d66d0000d86d0000da6d0000dc6d0000de6d0000e06d0000e26d0000e46d0000e66d0000e86d0000ea6d0000ec6d0000ee6d0000f06d0000f26d0000f46d0000f66d0000f86d0000fa6d0000fc6d0000fe6d0000006e0000 + m_ExpandedIDs: 000000007e020000020d0000ec6a0000ee6a0000f06a0000f26a0000f46a0000f66a0000f86a0000fa6a0000fc6a0000fe6a0000006b0000026b0000046b0000066b0000086b00000a6b00000c6b00000e6b0000106b0000126b0000146b0000166b0000186b00001a6b00001c6b00001e6b0000206b0000226b0000246b0000266b0000286b00002a6b00002c6b00002e6b0000306b0000326b0000346b0000366b0000386b00003a6b00003c6b00003e6b0000406b0000426b0000446b0000466b0000486b00004a6b00004c6b00004e6b0000506b0000526b0000546b0000566b0000586b00005a6b00005c6b00005e6b0000606b0000626b0000646b0000666b0000686b00006a6b00006c6b00006e6b0000706b0000726b0000746b0000766b0000786b0000 m_RenameOverlay: m_UserAcceptedRename: 0 m_Name: @@ -1329,21 +1329,21 @@ MonoBehaviour: scrollPos: {x: 0, y: 0} m_SelectedIDs: m_LastClickedID: 0 - m_ExpandedIDs: ffffffff000000007e0200008c0d00008a6d00008c6d00008e6d0000906d0000926d0000966d0000986d00009a6d00009c6d00009e6d0000a06d0000a26d0000a46d0000a66d0000a86d0000aa6d0000ac6d0000ae6d0000b06d0000b26d0000b46d0000b66d0000ba6d0000bc6d0000be6d0000c06d0000c26d0000c46d0000c66d0000c86d0000ca6d0000cc6d0000ce6d0000d06d0000d26d0000d46d0000d66d0000d86d0000da6d0000e46d0000e66d0000e86d0000ea6d0000ec6d0000ee6d0000f06d0000f26d0000f66d0000f86d0000fc6d0000fe6d0000006e0000be6f0000c06f0000187000001a7000001c7000006a700000207100003c7100007a7100007c7100007e71000080710000b2710000c472000060730000267400004a750000dc75000032770000 + m_ExpandedIDs: ffffffff000000007e020000020d0000ec6a0000ee6a0000f06a0000f26a0000f46a0000f66a0000f86a0000fc6a0000fe6a0000006b0000026b0000046b0000066b0000086b00000a6b00000c6b00000e6b0000106b0000126b0000146b0000166b0000186b00001a6b00001c6b00001e6b0000206b0000226b0000246b0000266b0000286b00002a6b00002c6b00002e6b0000306b0000326b0000346b0000386b00003c6b00003e6b0000406b0000426b0000446b0000466b00004a6b00004c6b0000506b0000526b0000566b00005a6b00005c6b00005e6b0000606b0000626b0000646b0000666b0000686b00006a6b00006c6b00006e6b0000706b0000726b0000746b0000766b0000786b00006c6d00006e6d0000706d0000766d0000c86d0000 m_RenameOverlay: m_UserAcceptedRename: 0 - m_Name: - m_OriginalName: + m_Name: ProcedureEntryState + m_OriginalName: ProcedureEntryState m_EditFieldRect: serializedVersion: 2 x: 0 y: 0 width: 0 height: 0 - m_UserData: 0 + m_UserData: 3678 m_IsWaitingForDelay: 0 m_IsRenaming: 0 - m_OriginalEventType: 11 + m_OriginalEventType: 0 m_IsRenamingFilename: 1 m_ClientGUIView: {fileID: 12} m_SearchString: @@ -1405,8 +1405,8 @@ MonoBehaviour: m_Tooltip: m_Pos: serializedVersion: 2 - x: 1302 - y: 73 + x: 3222 + y: 78 width: 617 height: 926 m_SerializedDataModeController: