diff --git a/Editor/AlicizaX.UI.Extension.Editor.asmdef b/Editor/AlicizaX.UI.Extension.Editor.asmdef index f36373b..33ae151 100644 --- a/Editor/AlicizaX.UI.Extension.Editor.asmdef +++ b/Editor/AlicizaX.UI.Extension.Editor.asmdef @@ -6,7 +6,8 @@ "GUID:6055be8ebefd69e48b49212b09b47b2f", "GUID:760f1778adc613f49a4394fb41ff0bbc", "GUID:1619e00706139ce488ff80c0daeea8e7", - "GUID:fb064c8bf96bac94e90d2f39090daa94" + "GUID:fb064c8bf96bac94e90d2f39090daa94", + "GUID:75469ad4d38634e559750d17036d5f7c" ], "includePlatforms": [ "Editor" diff --git a/Runtime/InputGlyph/Editor.meta b/Editor/InputGlyph.meta similarity index 77% rename from Runtime/InputGlyph/Editor.meta rename to Editor/InputGlyph.meta index 3f1f5cc..14518c9 100644 --- a/Runtime/InputGlyph/Editor.meta +++ b/Editor/InputGlyph.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 241d5382b2b4f274596c73f14a40cb8d +guid: 43899fd2849604a49acbdbd7b3450fb6 folderAsset: yes DefaultImporter: externalObjects: {} diff --git a/Editor/InputGlyph/InputGlyphDatabaseEditor.cs b/Editor/InputGlyph/InputGlyphDatabaseEditor.cs new file mode 100644 index 0000000..70e221d --- /dev/null +++ b/Editor/InputGlyph/InputGlyphDatabaseEditor.cs @@ -0,0 +1,476 @@ +using UnityEditor; +using UnityEditor.Callbacks; +using UnityEngine; +using UnityEngine.InputSystem; + +[CustomEditor(typeof(InputGlyphDatabase))] +public sealed class InputGlyphDatabaseEditor : Editor +{ + public override void OnInspectorGUI() + { + } + + [OnOpenAsset(0)] + private static bool OpenAsset(int instanceID, int line) + { + InputGlyphDatabase database = EditorUtility.InstanceIDToObject(instanceID) as InputGlyphDatabase; + if (database == null) + { + return false; + } + + InputGlyphDatabaseWindow.OpenFromAsset(database); + return true; + } +} + +internal sealed class InputGlyphDatabaseWindow : EditorWindow +{ + 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 SidebarWidth = 220f; + private const float EntryMinHeight = 76f; + private const float PreviewSize = 42f; + private static readonly string[] DefaultTableNames = { "Keyboard", "Xbox", "PlayStation", "Other" }; + + private InputGlyphDatabase _database; + private SerializedObject _serializedDatabase; + private SerializedProperty _tablesProperty; + private SerializedProperty _placeholderSpriteProperty; + private Vector2 _tableScroll; + private Vector2 _entryScroll; + private int _selectedTable; + private string _search = string.Empty; + private GUIStyle _sidebarStyle; + private GUIStyle _selectedTableStyle; + private GUIStyle _tableStyle; + private GUIStyle _entryCardStyle; + private GUIStyle _headerStyle; + private GUIContent _addIcon; + private GUIContent _removeIcon; + private GUIContent _saveIcon; + private GUIContent _refreshIcon; + private GUIContent _searchIcon; + private GUIContent _settingsIcon; + + internal static void OpenFromAsset(InputGlyphDatabase database) + { + InputGlyphDatabaseWindow window = GetWindow(true, "Input Glyph Database", true); + window.SetDatabase(database); + window.minSize = new Vector2(940f, 560f); + window.Show(); + } + + private void OnEnable() + { + BuildStyles(); + } + + private void SetDatabase(InputGlyphDatabase database) + { + _database = database; + _serializedDatabase = new SerializedObject(_database); + _tablesProperty = _serializedDatabase.FindProperty(TablesPropertyName); + _placeholderSpriteProperty = _serializedDatabase.FindProperty(PlaceholderSpritePropertyName); + _selectedTable = Mathf.Clamp(_selectedTable, 0, Mathf.Max(0, _tablesProperty.arraySize - 1)); + titleContent = new GUIContent(database.name, EditorGUIUtility.IconContent("d_ScriptableObject Icon").image); + } + + private void OnGUI() + { + if (_database == null || _serializedDatabase == null) + { + EditorApplication.delayCall += CloseIfDatabaseMissing; + return; + } + + BuildStyles(); + _serializedDatabase.Update(); + DrawToolbar(); + + Rect contentRect = new Rect(0f, EditorGUIUtility.singleLineHeight + 8f, position.width, position.height - EditorGUIUtility.singleLineHeight - 8f); + Rect sidebarRect = new Rect(contentRect.x, contentRect.y, SidebarWidth, contentRect.height); + Rect mainRect = new Rect(sidebarRect.xMax + 1f, contentRect.y, contentRect.width - SidebarWidth - 1f, contentRect.height); + + DrawSidebar(sidebarRect); + DrawMainPanel(mainRect); + + if (_serializedDatabase.ApplyModifiedProperties()) + { + MarkDirty(); + } + } + + private void CloseIfDatabaseMissing() + { + if (this != null && _database == null) + { + Close(); + } + } + + private void DrawToolbar() + { + using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar)) + { + if (GUILayout.Button(_saveIcon, EditorStyles.toolbarButton, GUILayout.Width(28f))) + { + Save(); + } + + if (GUILayout.Button(_refreshIcon, EditorStyles.toolbarButton, GUILayout.Width(28f))) + { + _database.EditorRefreshCache(); + Repaint(); + } + + GUILayout.Space(8f); + GUILayout.Label(_searchIcon, GUILayout.Width(20f)); + _search = GUILayout.TextField(_search, EditorStyles.toolbarSearchField, GUILayout.Width(240f)); + if (!string.IsNullOrEmpty(_search) && GUILayout.Button("×", EditorStyles.toolbarButton, GUILayout.Width(22f))) + { + _search = string.Empty; + GUI.FocusControl(null); + } + + GUILayout.FlexibleSpace(); + DrawObjectName(); + } + } + + private void DrawSidebar(Rect rect) + { + GUI.Box(rect, GUIContent.none, _sidebarStyle); + GUILayout.BeginArea(new Rect(rect.x + 8f, rect.y + 8f, rect.width - 16f, rect.height - 16f)); + + using (new EditorGUILayout.HorizontalScope()) + { + GUILayout.Label("Tables", _headerStyle); + GUILayout.FlexibleSpace(); + if (GUILayout.Button(_addIcon, EditorStyles.iconButton, GUILayout.Width(24f), GUILayout.Height(22f))) + { + AddTable(NextTableName()); + } + } + + _tableScroll = EditorGUILayout.BeginScrollView(_tableScroll); + for (int i = 0; i < _tablesProperty.arraySize; i++) + { + SerializedProperty table = _tablesProperty.GetArrayElementAtIndex(i); + DrawTableButton(i, table); + } + EditorGUILayout.EndScrollView(); + + GUILayout.Space(6f); + using (new EditorGUILayout.HorizontalScope()) + { + if (GUILayout.Button(_settingsIcon, EditorStyles.iconButton, GUILayout.Width(28f), GUILayout.Height(24f))) + { + _selectedTable = -1; + } + + GUILayout.FlexibleSpace(); + if (GUILayout.Button(_addIcon, EditorStyles.iconButton, GUILayout.Width(28f), GUILayout.Height(24f))) + { + CreateMissingDefaultTables(); + } + } + + GUILayout.EndArea(); + } + + private void DrawTableButton(int index, SerializedProperty table) + { + SerializedProperty nameProperty = table.FindPropertyRelative(DeviceNamePropertyName); + SerializedProperty iconProperty = table.FindPropertyRelative(PlatformIconPropertyName); + string tableName = string.IsNullOrWhiteSpace(nameProperty.stringValue) ? "Table " + (index + 1) : nameProperty.stringValue; + bool selected = _selectedTable == index; + GUIStyle style = selected ? _selectedTableStyle : _tableStyle; + + Rect rect = GUILayoutUtility.GetRect(1f, 44f, GUILayout.ExpandWidth(true)); + if (GUI.Button(rect, GUIContent.none, style)) + { + _selectedTable = index; + GUI.FocusControl(null); + } + + Rect iconRect = new Rect(rect.x + 8f, rect.y + 6f, 32f, 32f); + DrawSprite(iconRect, iconProperty.objectReferenceValue as Sprite); + GUI.Label(new Rect(iconRect.xMax + 8f, rect.y + 6f, rect.width - 76f, 18f), tableName, EditorStyles.boldLabel); + GUI.Label(new Rect(iconRect.xMax + 8f, rect.y + 24f, rect.width - 76f, 16f), EntryCountLabel(table), EditorStyles.miniLabel); + + Rect deleteRect = new Rect(rect.xMax - 28f, rect.y + 10f, 22f, 22f); + if (GUI.Button(deleteRect, _removeIcon, EditorStyles.iconButton)) + { + RemoveTable(index); + GUIUtility.ExitGUI(); + } + } + + private void DrawMainPanel(Rect rect) + { + GUILayout.BeginArea(new Rect(rect.x + 18f, rect.y + 14f, rect.width - 36f, rect.height - 22f)); + if (_selectedTable < 0 || _selectedTable >= _tablesProperty.arraySize) + { + DrawSettings(); + } + else + { + DrawTable(_selectedTable); + } + GUILayout.EndArea(); + } + + private void DrawSettings() + { + GUILayout.Label("Database", _headerStyle); + EditorGUILayout.Space(8f); + EditorGUILayout.PropertyField(_placeholderSpriteProperty, GUIContent.none); + Rect previewRect = GUILayoutUtility.GetRect(PreviewSize, PreviewSize, GUILayout.Width(PreviewSize), GUILayout.Height(PreviewSize)); + DrawSprite(previewRect, _placeholderSpriteProperty.objectReferenceValue as Sprite); + } + + private void DrawTable(int tableIndex) + { + SerializedProperty table = _tablesProperty.GetArrayElementAtIndex(tableIndex); + SerializedProperty nameProperty = table.FindPropertyRelative(DeviceNamePropertyName); + SerializedProperty sheetProperty = table.FindPropertyRelative(SpriteSheetPropertyName); + SerializedProperty iconProperty = table.FindPropertyRelative(PlatformIconPropertyName); + SerializedProperty entriesProperty = table.FindPropertyRelative(EntriesPropertyName); + + using (new EditorGUILayout.HorizontalScope()) + { + DrawSprite(GUILayoutUtility.GetRect(52f, 52f, GUILayout.Width(52f), GUILayout.Height(52f)), iconProperty.objectReferenceValue as Sprite); + using (new EditorGUILayout.VerticalScope()) + { + nameProperty.stringValue = EditorGUILayout.TextField(nameProperty.stringValue, _headerStyle); + using (new EditorGUILayout.HorizontalScope()) + { + EditorGUILayout.PropertyField(iconProperty, GUIContent.none, GUILayout.MinWidth(160f)); + EditorGUILayout.PropertyField(sheetProperty, GUIContent.none, GUILayout.MinWidth(160f)); + if (GUILayout.Button(_addIcon, EditorStyles.iconButton, GUILayout.Width(28f), GUILayout.Height(22f))) + { + AddEntry(entriesProperty); + } + } + } + } + + EditorGUILayout.Space(12f); + _entryScroll = EditorGUILayout.BeginScrollView(_entryScroll); + for (int i = 0; i < entriesProperty.arraySize; i++) + { + SerializedProperty entry = entriesProperty.GetArrayElementAtIndex(i); + if (!EntryMatches(entry)) + { + continue; + } + + DrawEntry(entriesProperty, i, entry); + } + EditorGUILayout.EndScrollView(); + } + + private void DrawEntry(SerializedProperty entriesProperty, int index, SerializedProperty entry) + { + SerializedProperty spriteProperty = entry.FindPropertyRelative(EntrySpritePropertyName); + SerializedProperty actionProperty = entry.FindPropertyRelative(EntryActionPropertyName); + float spriteHeight = EditorGUI.GetPropertyHeight(spriteProperty, GUIContent.none, true); + float actionHeight = EditorGUI.GetPropertyHeight(actionProperty, GUIContent.none, true); + float contentHeight = Mathf.Max(PreviewSize, spriteHeight + actionHeight + 8f); + Rect rect = GUILayoutUtility.GetRect(1f, Mathf.Max(EntryMinHeight, contentHeight + 16f), GUILayout.ExpandWidth(true)); + GUI.Box(rect, GUIContent.none, _entryCardStyle); + + Rect previewRect = new Rect(rect.x + 8f, rect.y + 8f, PreviewSize, PreviewSize); + DrawSprite(previewRect, spriteProperty.objectReferenceValue as Sprite); + + Rect removeRect = new Rect(rect.xMax - 32f, rect.y + 8f, 24f, 24f); + float fieldX = previewRect.xMax + 12f; + float fieldWidth = Mathf.Max(120f, removeRect.x - fieldX - 10f); + Rect spriteRect = new Rect(fieldX, rect.y + 8f, fieldWidth, spriteHeight); + Rect actionRect = new Rect(fieldX, spriteRect.yMax + 8f, fieldWidth, actionHeight); + EditorGUI.PropertyField(spriteRect, spriteProperty, GUIContent.none, true); + EditorGUI.PropertyField(actionRect, actionProperty, GUIContent.none, true); + + if (GUI.Button(removeRect, _removeIcon, EditorStyles.iconButton)) + { + entriesProperty.DeleteArrayElementAtIndex(index); + MarkDirty(); + GUIUtility.ExitGUI(); + } + } + + private bool EntryMatches(SerializedProperty entry) + { + if (string.IsNullOrWhiteSpace(_search)) + { + return true; + } + + SerializedProperty spriteProperty = entry.FindPropertyRelative(EntrySpritePropertyName); + SerializedProperty actionProperty = entry.FindPropertyRelative(EntryActionPropertyName); + string search = _search.Trim(); + Sprite sprite = spriteProperty.objectReferenceValue as Sprite; + Object actionObject = actionProperty.objectReferenceValue; + return Contains(sprite != null ? sprite.name : string.Empty, search) + || Contains(actionObject != null ? actionObject.name : string.Empty, search); + } + + private static bool Contains(string value, string search) + { + return !string.IsNullOrEmpty(value) && value.IndexOf(search, System.StringComparison.OrdinalIgnoreCase) >= 0; + } + + private void AddEntry(SerializedProperty entriesProperty) + { + int index = entriesProperty.arraySize; + entriesProperty.InsertArrayElementAtIndex(index); + SerializedProperty entry = entriesProperty.GetArrayElementAtIndex(index); + entry.FindPropertyRelative(EntrySpritePropertyName).objectReferenceValue = null; + entry.FindPropertyRelative(EntryActionPropertyName).objectReferenceValue = null; + MarkDirty(); + } + + private void AddTable(string tableName) + { + int index = _tablesProperty.arraySize; + _tablesProperty.InsertArrayElementAtIndex(index); + SerializedProperty table = _tablesProperty.GetArrayElementAtIndex(index); + table.FindPropertyRelative(DeviceNamePropertyName).stringValue = tableName; + table.FindPropertyRelative(SpriteSheetPropertyName).objectReferenceValue = null; + table.FindPropertyRelative(PlatformIconPropertyName).objectReferenceValue = null; + table.FindPropertyRelative(EntriesPropertyName).ClearArray(); + _selectedTable = index; + MarkDirty(); + } + + private void RemoveTable(int index) + { + _tablesProperty.DeleteArrayElementAtIndex(index); + _selectedTable = Mathf.Clamp(_selectedTable, -1, _tablesProperty.arraySize - 1); + MarkDirty(); + } + + private void CreateMissingDefaultTables() + { + for (int i = 0; i < DefaultTableNames.Length; i++) + { + if (!HasTable(DefaultTableNames[i])) + { + AddTable(DefaultTableNames[i]); + } + } + } + + private bool HasTable(string tableName) + { + for (int i = 0; i < _tablesProperty.arraySize; i++) + { + SerializedProperty table = _tablesProperty.GetArrayElementAtIndex(i); + if (string.Equals(table.FindPropertyRelative(DeviceNamePropertyName).stringValue, tableName, System.StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + return false; + } + + private string NextTableName() + { + for (int i = 0; i < DefaultTableNames.Length; i++) + { + if (!HasTable(DefaultTableNames[i])) + { + return DefaultTableNames[i]; + } + } + + return "Table " + (_tablesProperty.arraySize + 1); + } + + private static string EntryCountLabel(SerializedProperty table) + { + SerializedProperty entries = table.FindPropertyRelative(EntriesPropertyName); + return entries.arraySize + " glyphs"; + } + + private void DrawObjectName() + { + using (new EditorGUI.DisabledScope(true)) + { + EditorGUILayout.ObjectField(_database, typeof(InputGlyphDatabase), false, GUILayout.Width(260f)); + } + } + + private static void DrawSprite(Rect rect, Sprite sprite) + { + if (sprite == null) + { + GUI.Box(rect, GUIContent.none, EditorStyles.helpBox); + return; + } + + Texture2D texture = AssetPreview.GetAssetPreview(sprite) ?? AssetPreview.GetMiniThumbnail(sprite); + if (texture != null) + { + GUI.DrawTexture(rect, texture, ScaleMode.ScaleToFit, true); + } + } + + private void Save() + { + _serializedDatabase.ApplyModifiedProperties(); + MarkDirty(); + AssetDatabase.SaveAssetIfDirty(_database); + } + + private void MarkDirty() + { + _database.EditorRefreshCache(); + EditorUtility.SetDirty(_database); + } + + private void BuildStyles() + { + _addIcon ??= EditorGUIUtility.IconContent("Toolbar Plus"); + _removeIcon ??= EditorGUIUtility.IconContent("TreeEditor.Trash"); + _saveIcon ??= EditorGUIUtility.IconContent("SaveActive"); + _refreshIcon ??= EditorGUIUtility.IconContent("Refresh"); + _searchIcon ??= EditorGUIUtility.IconContent("Search Icon"); + _settingsIcon ??= EditorGUIUtility.IconContent("d__Popup"); + + _sidebarStyle ??= new GUIStyle(EditorStyles.helpBox) + { + padding = new RectOffset(8, 8, 8, 8) + }; + + _tableStyle ??= new GUIStyle(EditorStyles.toolbarButton) + { + alignment = TextAnchor.MiddleLeft, + fixedHeight = 44f + }; + + _selectedTableStyle ??= new GUIStyle(_tableStyle) + { + normal = { background = Texture2D.grayTexture }, + fontStyle = FontStyle.Bold + }; + + _entryCardStyle ??= new GUIStyle(EditorStyles.helpBox) + { + padding = new RectOffset(8, 8, 8, 8) + }; + + _headerStyle ??= new GUIStyle(EditorStyles.boldLabel) + { + fontSize = 15 + }; + } +} diff --git a/Runtime/InputGlyph/Editor/InputGlyphDatabaseEditor.cs.meta b/Editor/InputGlyph/InputGlyphDatabaseEditor.cs.meta similarity index 100% rename from Runtime/InputGlyph/Editor/InputGlyphDatabaseEditor.cs.meta rename to Editor/InputGlyph/InputGlyphDatabaseEditor.cs.meta diff --git a/Runtime/InputGlyph/Editor/InputGlyphEditor.cs b/Editor/InputGlyph/InputGlyphEditor.cs similarity index 91% rename from Runtime/InputGlyph/Editor/InputGlyphEditor.cs rename to Editor/InputGlyph/InputGlyphEditor.cs index ed98e75..b54e3ff 100644 --- a/Runtime/InputGlyph/Editor/InputGlyphEditor.cs +++ b/Editor/InputGlyph/InputGlyphEditor.cs @@ -9,6 +9,9 @@ using UnityEngine.InputSystem; [CanEditMultipleObjects] public sealed class InputGlyphEditor : Editor { + private static readonly List CachedActionAssets = new(16); + private static bool _actionAssetCacheDirty = true; + private SerializedProperty _actionSourceMode; private SerializedProperty _actionReference; private SerializedProperty _hotkeyTrigger; @@ -86,6 +89,11 @@ public sealed class InputGlyphEditor : Editor break; case InputGlyph.ActionSourceMode.ActionName: EditorGUILayout.PropertyField(_actionName, new GUIContent("Action Name")); + if (GUILayout.Button("Refresh Action Asset Cache", EditorStyles.miniButton)) + { + _actionAssetCacheDirty = true; + } + EditorGUILayout.LabelField("Supports ActionName or MapName/ActionName.", _hintStyle); break; } @@ -225,14 +233,29 @@ public sealed class InputGlyphEditor : Editor private IEnumerable EnumerateInputActionAssets() { - HashSet visited = new HashSet(); + EnsureActionAssetCache(); + for (int i = 0; i < CachedActionAssets.Count; i++) + { + yield return CachedActionAssets[i]; + } + } + + private static void EnsureActionAssetCache() + { + if (!_actionAssetCacheDirty) + { + return; + } + + _actionAssetCacheDirty = false; + CachedActionAssets.Clear(); InputBindingManager[] managers = Resources.FindObjectsOfTypeAll(); for (int i = 0; i < managers.Length; i++) { InputActionAsset asset = managers[i] != null ? managers[i].actions : null; - if (asset != null && visited.Add(asset)) + if (asset != null && !CachedActionAssets.Contains(asset)) { - yield return asset; + CachedActionAssets.Add(asset); } } @@ -241,9 +264,9 @@ public sealed class InputGlyphEditor : Editor { string path = AssetDatabase.GUIDToAssetPath(guids[i]); InputActionAsset asset = AssetDatabase.LoadAssetAtPath(path); - if (asset != null && visited.Add(asset)) + if (asset != null && !CachedActionAssets.Contains(asset)) { - yield return asset; + CachedActionAssets.Add(asset); } } } diff --git a/Runtime/InputGlyph/Editor/InputGlyphEditor.cs.meta b/Editor/InputGlyph/InputGlyphEditor.cs.meta similarity index 100% rename from Runtime/InputGlyph/Editor/InputGlyphEditor.cs.meta rename to Editor/InputGlyph/InputGlyphEditor.cs.meta diff --git a/Runtime/AlicizaX.UI.Extension.asmdef b/Runtime/AlicizaX.UI.Extension.asmdef index 67a991d..cac3612 100644 --- a/Runtime/AlicizaX.UI.Extension.asmdef +++ b/Runtime/AlicizaX.UI.Extension.asmdef @@ -5,7 +5,8 @@ "GUID:6055be8ebefd69e48b49212b09b47b2f", "GUID:80ecb87cae9c44d19824e70ea7229748", "GUID:75469ad4d38634e559750d17036d5f7c", - "GUID:1619e00706139ce488ff80c0daeea8e7" + "GUID:1619e00706139ce488ff80c0daeea8e7", + "GUID:33661e06c33d31b4c9223810bf503247" ], "includePlatforms": [], "excludePlatforms": [], diff --git a/Runtime/InputGlyph/Core/GlyphService.cs b/Runtime/InputGlyph/Core/GlyphService.cs index 859b656..2fb6946 100644 --- a/Runtime/InputGlyph/Core/GlyphService.cs +++ b/Runtime/InputGlyph/Core/GlyphService.cs @@ -1,8 +1,12 @@ using System; -using System.Collections.Generic; +using Cysharp.Text; using UnityEngine; using UnityEngine.InputSystem; +/// +/// ? Input System ????? UI Sprite?TMP Sprite Tag ???????? +/// ?????????????????????? SetDatabase ??? +/// public static class GlyphService { private static readonly string[] KeyboardGroupHints = { "keyboard", "mouse", "keyboard&mouse", "keyboardmouse", "kbm" }; @@ -10,24 +14,44 @@ public static class GlyphService private static readonly string[] PlayStationGroupHints = { "playstation", "dualshock", "dualsense", "gamepad", "controller" }; private static readonly string[] OtherGamepadGroupHints = { "gamepad", "controller", "joystick" }; private static readonly char[] TrimChars = { '{', '}', '<', '>', '\'', '"' }; - private static readonly Dictionary DisplayNameCache = new(StringComparer.Ordinal); - private static readonly Dictionary SpriteTagCache = new(); + private const int InitialCacheCapacity = 64; + private static string[] DisplayNameKeys = new string[InitialCacheCapacity]; + private static string[] DisplayNameValues = new string[InitialCacheCapacity]; + private static int DisplayNameCount; + private static int[] SpriteTagKeys = new int[InitialCacheCapacity]; + private static string[] SpriteTagValues = new string[InitialCacheCapacity]; + private static int SpriteTagCount; private static InputGlyphDatabase _database; + /// + /// ???? Glyph ???????????????????? + /// ??????????????? Resources.Load? + /// + public static void SetDatabase(InputGlyphDatabase database) + { + _database = database; + } + + /// + /// ???????????? + /// + public static void ClearDatabase() + { + _database = null; + } + static InputGlyphDatabase Database { get { - if (_database == null) - { - _database = Resources.Load("InputGlyphDatabase"); - } - return _database; } } + /// + /// ?????????????????????????? + /// public static string GetBindingControlPath( InputAction action, string compositePartName = null, @@ -46,6 +70,9 @@ public static class GlyphService return GetBindingControlPath(actionReference != null ? actionReference.action : null, compositePartName, deviceOverride); } + /// + /// ? Action ?????????? TMP Sprite Tag????????????? + /// public static bool TryGetTMPTagForActionPath( InputAction action, string compositePartName, @@ -69,6 +96,9 @@ public static class GlyphService return TryGetTMPTagForActionPath(actionReference != null ? actionReference.action : null, compositePartName, device, out tag, out displayFallback, db); } + /// + /// ? Action ?????????????????? UI Sprite? + /// public static bool TryGetUISpriteForActionPath( InputAction action, string compositePartName, @@ -90,6 +120,9 @@ public static class GlyphService return TryGetUISpriteForActionPath(actionReference != null ? actionReference.action : null, compositePartName, device, out sprite, db); } + /// + /// 将原始控制路径解析为 TMP Sprite Tag,失败时回退为可读控制名。 + /// public static bool TryGetTMPTagForActionPath( string controlPath, InputDeviceWatcher.InputDeviceCategory device, @@ -109,6 +142,9 @@ public static class GlyphService return true; } + /// + /// 通过 Glyph 数据库查找表将原始控制路径解析为 UI Sprite。 + /// public static bool TryGetUISpriteForActionPath( string controlPath, InputDeviceWatcher.InputDeviceCategory device, @@ -120,6 +156,9 @@ public static class GlyphService return db != null && db.TryGetSprite(controlPath, device, out sprite); } + /// + /// ?? Action ????????????? + /// public static string GetDisplayNameFromInputAction( InputAction action, string compositePartName = null, @@ -134,6 +173,10 @@ public static class GlyphService return string.IsNullOrEmpty(display) ? GetDisplayNameFromControlPath(GetEffectivePath(binding)) : display; } + /// + /// ????????????????? + /// ?????????????????????????? + /// public static string GetDisplayNameFromControlPath(string controlPath) { if (string.IsNullOrWhiteSpace(controlPath)) @@ -141,24 +184,28 @@ public static class GlyphService return string.Empty; } - if (DisplayNameCache.TryGetValue(controlPath, out string cachedDisplayName)) + int cacheIndex = IndexOf(DisplayNameKeys, DisplayNameCount, controlPath); + if (cacheIndex >= 0) { - return cachedDisplayName; + return DisplayNameValues[cacheIndex]; } string humanReadable = InputControlPath.ToHumanReadableString(controlPath, InputControlPath.HumanReadableStringOptions.OmitDevice); if (!string.IsNullOrWhiteSpace(humanReadable)) { - DisplayNameCache[controlPath] = humanReadable; + AddDisplayNameCache(controlPath, humanReadable); return humanReadable; } int separatorIndex = controlPath.LastIndexOf('/'); string last = (separatorIndex >= 0 ? controlPath.Substring(separatorIndex + 1) : controlPath).Trim(TrimChars); - DisplayNameCache[controlPath] = last; + AddDisplayNameCache(controlPath, last); return last; } + /// + /// ?? binding group ???????????????????? + /// public static bool TryGetBindingControl( InputAction action, string compositePartName, @@ -269,8 +316,7 @@ public static class GlyphService if (tokenLength > 0) { - string token = groups.Substring(tokenStart, tokenLength); - if (ContainsAny(token, hints)) + if (ContainsAny(groups, tokenStart, tokenLength, hints)) { return true; } @@ -290,16 +336,112 @@ public static class GlyphService } int instanceId = sprite.GetInstanceID(); - if (SpriteTagCache.TryGetValue(instanceId, out string cachedTag)) + int cacheIndex = IndexOf(SpriteTagKeys, SpriteTagCount, instanceId); + if (cacheIndex >= 0) { - return cachedTag; + return SpriteTagValues[cacheIndex]; } - cachedTag = $""; - SpriteTagCache[instanceId] = cachedTag; + string cachedTag = ZString.Concat(""); + AddSpriteTagCache(instanceId, cachedTag); return cachedTag; } + private static void AddDisplayNameCache(string key, string value) + { + if (DisplayNameCount == DisplayNameKeys.Length) + { + Array.Resize(ref DisplayNameKeys, DisplayNameKeys.Length << 1); + Array.Resize(ref DisplayNameValues, DisplayNameValues.Length << 1); + } + + DisplayNameKeys[DisplayNameCount] = key; + DisplayNameValues[DisplayNameCount] = value; + DisplayNameCount++; + } + + private static void AddSpriteTagCache(int key, string value) + { + if (SpriteTagCount == SpriteTagKeys.Length) + { + Array.Resize(ref SpriteTagKeys, SpriteTagKeys.Length << 1); + Array.Resize(ref SpriteTagValues, SpriteTagValues.Length << 1); + } + + SpriteTagKeys[SpriteTagCount] = key; + SpriteTagValues[SpriteTagCount] = value; + SpriteTagCount++; + } + + private static int IndexOf(string[] values, int count, string value) + { + for (int i = 0; i < count; i++) + { + if (string.Equals(values[i], value, StringComparison.Ordinal)) + { + return i; + } + } + + return -1; + } + + private static int IndexOf(int[] values, int count, int value) + { + for (int i = 0; i < count; i++) + { + if (values[i] == value) + { + return i; + } + } + + return -1; + } + + private static bool ContainsAny(string source, int startIndex, int length, string[] hints) + { + if (string.IsNullOrEmpty(source) || hints == null || length <= 0) + { + return false; + } + + for (int i = 0; i < hints.Length; i++) + { + if (IndexOfIgnoreCase(source, startIndex, length, hints[i]) >= 0) + { + return true; + } + } + + return false; + } + + private static int IndexOfIgnoreCase(string source, int startIndex, int length, string value) + { + if (string.IsNullOrEmpty(value) || value.Length > length) + { + return -1; + } + + int end = startIndex + length - value.Length; + for (int i = startIndex; i <= end; i++) + { + int valueIndex = 0; + while (valueIndex < value.Length && char.ToUpperInvariant(source[i + valueIndex]) == char.ToUpperInvariant(value[valueIndex])) + { + valueIndex++; + } + + if (valueIndex == value.Length) + { + return i; + } + } + + return -1; + } + private static bool ContainsAny(string source, string[] hints) { if (string.IsNullOrWhiteSpace(source) || hints == null) diff --git a/Runtime/InputGlyph/Core/InputActionReader.cs b/Runtime/InputGlyph/Core/InputActionReader.cs index 3c97638..d6e75f5 100644 --- a/Runtime/InputGlyph/Core/InputActionReader.cs +++ b/Runtime/InputGlyph/Core/InputActionReader.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using UnityEngine; using UnityEngine.InputSystem; @@ -64,25 +63,19 @@ public static class InputActionReader } } - // 记录“本次按下已消费”的键,用于 Once 语义。 - private static readonly HashSet PressedKeys = new(); - // 记录当前处于开启状态的切换键。 - private static readonly HashSet ToggledKeys = new(); + private const int InitialKeyCapacity = 64; + private static InputReadKey[] PressedKeys = new InputReadKey[InitialKeyCapacity]; + private static int PressedKeyCount; + private static InputReadKey[] ToggledKeys = new InputReadKey[InitialKeyCapacity]; + private static int ToggledKeyCount; /// /// 直接读取指定 Action 的值。 /// public static T ReadValue(string actionName) where T : struct { - return ResolveAction(actionName).ReadValue(); - } - - /// - /// 以 object 形式读取指定 Action 的值。 - /// - public static object ReadValue(string actionName) - { - return ResolveAction(actionName).ReadValueAsObject(); + InputAction inputAction = ResolveAction(actionName); + return inputAction != null ? inputAction.ReadValue() : default; } /// @@ -91,7 +84,7 @@ public static class InputActionReader public static bool TryReadValue(string actionName, out T value) where T : struct { InputAction inputAction = ResolveAction(actionName); - if (inputAction.IsPressed()) + if (inputAction != null && inputAction.IsPressed()) { value = inputAction.ReadValue(); return true; @@ -101,22 +94,6 @@ public static class InputActionReader return false; } - /// - /// 仅在 Action 处于按下状态时以 object 形式读取值。 - /// - public static bool TryReadValue(string actionName, out object value) - { - InputAction inputAction = ResolveAction(actionName); - if (inputAction.IsPressed()) - { - value = inputAction.ReadValueAsObject(); - return true; - } - - value = default; - return false; - } - /// /// 只在本次按下的第一帧返回 true,并输出当前值。 /// owner 用来隔离不同对象的读取状态。 @@ -139,9 +116,14 @@ public static class InputActionReader public static bool ReadButton(string actionName) { InputAction inputAction = ResolveAction(actionName); + if (inputAction == null) + { + return false; + } + if (inputAction.type == InputActionType.Button) { - return Convert.ToBoolean(inputAction.ReadValueAsObject()); + return inputAction.IsPressed(); } throw new NotSupportedException("[InputActionReader] The Input Action must be a button type."); @@ -201,7 +183,7 @@ public static class InputActionReader /// public static void ResetToggledButton(string key, string actionName) { - ToggledKeys.Remove(new InputReadKey(actionName, key)); + RemoveKey(ToggledKeys, ref ToggledKeyCount, new InputReadKey(actionName, key)); } /// @@ -209,18 +191,16 @@ public static class InputActionReader /// public static void ResetToggledButton(string actionName) { - if (string.IsNullOrEmpty(actionName) || ToggledKeys.Count == 0) + if (string.IsNullOrEmpty(actionName) || ToggledKeyCount == 0) { return; } - InputReadKey[] snapshot = new InputReadKey[ToggledKeys.Count]; - ToggledKeys.CopyTo(snapshot); - for (int i = 0; i < snapshot.Length; i++) + for (int i = ToggledKeyCount - 1; i >= 0; i--) { - if (string.Equals(snapshot[i].ActionName, actionName, StringComparison.Ordinal)) + if (string.Equals(ToggledKeys[i].ActionName, actionName, StringComparison.Ordinal)) { - ToggledKeys.Remove(snapshot[i]); + RemoveAt(ToggledKeys, ref ToggledKeyCount, i); } } } @@ -230,7 +210,8 @@ public static class InputActionReader /// public static void ResetToggledButtons() { - ToggledKeys.Clear(); + Array.Clear(ToggledKeys, 0, ToggledKeyCount); + ToggledKeyCount = 0; } /// @@ -238,8 +219,7 @@ public static class InputActionReader /// private static InputAction ResolveAction(string actionName) { - return InputBindingManager.Action(actionName) - ?? throw new InvalidOperationException($"[InputActionReader] Action '{actionName}' is not available."); + return InputBindingManager.Action(actionName); } /// @@ -249,9 +229,9 @@ public static class InputActionReader private static bool TryReadValueOnceInternal(InputReadKey readKey, string actionName, out T value) where T : struct { InputAction inputAction = ResolveAction(actionName); - if (inputAction.IsPressed()) + if (inputAction != null && inputAction.IsPressed()) { - if (PressedKeys.Add(readKey)) + if (AddKey(ref PressedKeys, ref PressedKeyCount, readKey)) { value = inputAction.ReadValue(); return true; @@ -259,7 +239,7 @@ public static class InputActionReader } else { - PressedKeys.Remove(readKey); + RemoveKey(PressedKeys, ref PressedKeyCount, readKey); } value = default; @@ -274,10 +254,10 @@ public static class InputActionReader { if (ReadButton(actionName)) { - return PressedKeys.Add(readKey); + return AddKey(ref PressedKeys, ref PressedKeyCount, readKey); } - PressedKeys.Remove(readKey); + RemoveKey(PressedKeys, ref PressedKeyCount, readKey); return false; } @@ -289,12 +269,64 @@ public static class InputActionReader { if (ReadButtonOnceInternal(readKey, actionName)) { - if (!ToggledKeys.Add(readKey)) + if (!AddKey(ref ToggledKeys, ref ToggledKeyCount, readKey)) { - ToggledKeys.Remove(readKey); + RemoveKey(ToggledKeys, ref ToggledKeyCount, readKey); } } - return ToggledKeys.Contains(readKey); + return IndexOf(ToggledKeys, ToggledKeyCount, readKey) >= 0; + } + + private static bool AddKey(ref InputReadKey[] keys, ref int count, InputReadKey key) + { + if (IndexOf(keys, count, key) >= 0) + { + return false; + } + + if (count == keys.Length) + { + Array.Resize(ref keys, keys.Length << 1); + } + + keys[count++] = key; + return true; + } + + private static bool RemoveKey(InputReadKey[] keys, ref int count, InputReadKey key) + { + int index = IndexOf(keys, count, key); + if (index < 0) + { + return false; + } + + RemoveAt(keys, ref count, index); + return true; + } + + private static void RemoveAt(InputReadKey[] keys, ref int count, int index) + { + count--; + if (index < count) + { + keys[index] = keys[count]; + } + + keys[count] = default; + } + + private static int IndexOf(InputReadKey[] keys, int count, InputReadKey key) + { + for (int i = 0; i < count; i++) + { + if (keys[i].Equals(key)) + { + return i; + } + } + + return -1; } } diff --git a/Runtime/InputGlyph/Core/InputBindingManager.cs b/Runtime/InputGlyph/Core/InputBindingManager.cs index a7a4c8f..ef90998 100644 --- a/Runtime/InputGlyph/Core/InputBindingManager.cs +++ b/Runtime/InputGlyph/Core/InputBindingManager.cs @@ -23,6 +23,8 @@ public sealed class InputBindingManager : MonoServiceBehaviour public bool debugMode = false; private InputActionRebindingExtensions.RebindingOperation rebindOperation; + private InputAction rebindAction; + private int rebindBindingIndex = -1; private bool isApplyPending = false; private string defaultBindingsJson = string.Empty; private string cachedSavePath; @@ -32,7 +34,7 @@ public sealed class InputBindingManager : MonoServiceBehaviour private readonly Dictionary actionLookupById = new(); private readonly HashSet ambiguousActionNames = new(StringComparer.Ordinal); - public event Action> OnApply; + public event Action OnApply; public event Action OnRebindPrepare; public event Action OnRebindStart; public event Action OnRebindEnd; @@ -78,36 +80,21 @@ public sealed class InputBindingManager : MonoServiceBehaviour BuildActionMap(); - try - { - defaultBindingsJson = actions.SaveBindingOverridesAsJson(); - } - catch (Exception ex) - { - Log.Warning($"[InputBindingManager] Failed to save default bindings: {ex.Message}"); - defaultBindingsJson = string.Empty; - } + defaultBindingsJson = actions.SaveBindingOverridesAsJson(); if (File.Exists(SavePath)) { - try + var json = File.ReadAllText(SavePath); + if (!string.IsNullOrEmpty(json)) { - var json = File.ReadAllText(SavePath); - if (!string.IsNullOrEmpty(json)) + actions.LoadBindingOverridesFromJson(json); + RefreshBindingPathsFromActions(); + BindingsChanged?.Invoke(); + if (debugMode) { - actions.LoadBindingOverridesFromJson(json); - RefreshBindingPathsFromActions(); - BindingsChanged?.Invoke(); - if (debugMode) - { - Log.Info($"Loaded overrides from {SavePath}"); - } + Log.Info($"Loaded overrides from {SavePath}"); } } - catch (Exception ex) - { - Log.Error("Failed to load overrides: " + ex); - } } actions.Enable(); @@ -350,6 +337,8 @@ public sealed class InputBindingManager : MonoServiceBehaviour private void PerformInteractiveRebinding(InputAction action, int bindingIndex, string deviceMatchPath = null, bool excludeMouseMovementAndScroll = true) { + rebindAction = action; + rebindBindingIndex = bindingIndex; var op = action.PerformInteractiveRebinding(bindingIndex); if (!string.IsNullOrEmpty(deviceMatchPath)) @@ -366,57 +355,75 @@ public sealed class InputBindingManager : MonoServiceBehaviour } rebindOperation = op - .OnApplyBinding((o, path) => - { - RebindContext preparedContext = new RebindContext(action, bindingIndex, path); - if (AnyPreparedRebind(path, action, bindingIndex, out var existing)) - { - PrepareRebind(preparedContext); - PrepareRebind(new RebindContext(existing.action, existing.bindingIndex, NULL_BINDING)); - OnRebindConflict?.Invoke(preparedContext, existing); - } - else if (AnyBindingPath(path, action, bindingIndex, out var dup)) - { - RebindContext conflictingContext = new RebindContext(dup.action, dup.bindingIndex, dup.action.bindings[dup.bindingIndex].path); - PrepareRebind(preparedContext); - PrepareRebind(new RebindContext(dup.action, dup.bindingIndex, NULL_BINDING)); - OnRebindConflict?.Invoke(preparedContext, conflictingContext); - } - else - { - PrepareRebind(preparedContext); - } - }) - .OnComplete(opc => - { - if (debugMode) - { - Log.Info("[InputBindingManager] Rebind completed"); - } - - actions.Enable(); - OnRebindEnd?.Invoke(true, new RebindContext(action, bindingIndex, action.bindings[bindingIndex].effectivePath)); - CleanRebindOperation(); - }) - .OnCancel(opc => - { - if (debugMode) - { - Log.Info("[InputBindingManager] Rebind cancelled"); - } - - actions.Enable(); - OnRebindEnd?.Invoke(false, new RebindContext(action, bindingIndex, action.bindings[bindingIndex].effectivePath)); - CleanRebindOperation(); - }) + .OnApplyBinding(HandleApplyBinding) + .OnComplete(HandleRebindComplete) + .OnCancel(HandleRebindCancel) .WithCancelingThrough(KEYBOARD_ESCAPE) .Start(); } + private void HandleApplyBinding(InputActionRebindingExtensions.RebindingOperation operation, string path) + { + RebindContext preparedContext = new RebindContext(rebindAction, rebindBindingIndex, path); + if (AnyPreparedRebind(path, rebindAction, rebindBindingIndex, out var existing)) + { + PrepareRebind(preparedContext); + PrepareRebind(new RebindContext(existing.action, existing.bindingIndex, NULL_BINDING)); + OnRebindConflict?.Invoke(preparedContext, existing); + } + else if (AnyBindingPath(path, rebindAction, rebindBindingIndex, out var duplicate)) + { + RebindContext conflictingContext = new RebindContext(duplicate.action, duplicate.bindingIndex, duplicate.action.bindings[duplicate.bindingIndex].path); + PrepareRebind(preparedContext); + PrepareRebind(new RebindContext(duplicate.action, duplicate.bindingIndex, NULL_BINDING)); + OnRebindConflict?.Invoke(preparedContext, conflictingContext); + } + else + { + PrepareRebind(preparedContext); + } + } + + private void HandleRebindComplete(InputActionRebindingExtensions.RebindingOperation operation) + { + if (debugMode) + { + Log.Info("[InputBindingManager] Rebind completed"); + } + + actions.Enable(); + OnRebindEnd?.Invoke(true, CreateCurrentRebindContext()); + CleanRebindOperation(); + } + + private void HandleRebindCancel(InputActionRebindingExtensions.RebindingOperation operation) + { + if (debugMode) + { + Log.Info("[InputBindingManager] Rebind cancelled"); + } + + actions.Enable(); + OnRebindEnd?.Invoke(false, CreateCurrentRebindContext()); + CleanRebindOperation(); + } + + private RebindContext CreateCurrentRebindContext() + { + if (rebindAction == null || rebindBindingIndex < 0) + { + return null; + } + + return new RebindContext(rebindAction, rebindBindingIndex, rebindAction.bindings[rebindBindingIndex].effectivePath); + } + private void CleanRebindOperation() { rebindOperation?.Dispose(); rebindOperation = null; + rebindAction = null; + rebindBindingIndex = -1; } private bool AnyPreparedRebind(string bindingPath, InputAction currentAction, int currentIndex, out RebindContext duplicate) @@ -444,7 +451,7 @@ public sealed class InputBindingManager : MonoServiceBehaviour foreach (var bindingPair in actionPair.bindings) { - // Skip if it's the same action and same binding index + // 跳过当前正在重绑定的同一个 action/binding。 if (isSameAction && bindingPair.Key == currentIndex) continue; @@ -463,7 +470,7 @@ public sealed class InputBindingManager : MonoServiceBehaviour private void PrepareRebind(RebindContext context) { - // Remove any existing prepared state for the same action/binding pair. + // 移除同一个 action/binding 已暂存的重绑定状态。 preparedRebinds.Remove(context); BindingPath bindingPath = GetBindingPath(context.action, context.bindingIndex); @@ -488,20 +495,12 @@ public sealed class InputBindingManager : MonoServiceBehaviour private async Task WriteOverridesToDiskAsync() { - try + var json = actions.SaveBindingOverridesAsJson(); + EnsureSaveDirectoryExists(); + using (var sw = new StreamWriter(SavePath, false)) await sw.WriteAsync(json); + if (debugMode) { - var json = actions.SaveBindingOverridesAsJson(); - EnsureSaveDirectoryExists(); - using (var sw = new StreamWriter(SavePath, false)) await sw.WriteAsync(json); - if (debugMode) - { - Log.Info($"Overrides saved to {SavePath}"); - } - } - catch (Exception ex) - { - Log.Error("Failed to save overrides: " + ex); - throw; + Log.Info($"Overrides saved to {SavePath}"); } } @@ -522,14 +521,9 @@ public sealed class InputBindingManager : MonoServiceBehaviour } #region Public API - - // 为键盘选择最佳绑定索引;如果 compositePartName != null 则查找部分 /// - /// 为键盘查找最佳的绑定索引 + /// ?? Action ??????????? /// - /// 输入操作 - /// 复合部分名称(可选) - /// 绑定索引,未找到则返回 -1 public int FindBestBindingIndexForKeyboard(InputAction action, string compositePartName = null) { if (action == null) return -1; @@ -541,15 +535,11 @@ public sealed class InputBindingManager : MonoServiceBehaviour for (int i = 0; i < action.bindings.Count; i++) { var b = action.bindings[i]; - - // 如果搜索特定的复合部分,跳过不匹配的绑定 if (searchingForCompositePart) { if (!b.isPartOfComposite) continue; if (!string.Equals(b.name, compositePartName, StringComparison.OrdinalIgnoreCase)) continue; } - - // 检查此绑定是否用于键盘 bool isKeyboardBinding = (!string.IsNullOrEmpty(b.path) && b.path.StartsWith(KEYBOARD_DEVICE, StringComparison.OrdinalIgnoreCase)) || (!string.IsNullOrEmpty(b.effectivePath) && b.effectivePath.StartsWith(KEYBOARD_DEVICE, StringComparison.OrdinalIgnoreCase)); @@ -567,12 +557,9 @@ public sealed class InputBindingManager : MonoServiceBehaviour return fallbackNonComposite >= 0 ? fallbackNonComposite : fallbackPart; } - /// - /// 根据操作名称获取输入操作 + /// ?? ActionName ? MapName/ActionName ?? Action? /// - /// 操作名称 - /// 输入操作,未找到则返回 null public static InputAction Action(string actionName) { var instance= AppServices.Require(); @@ -608,18 +595,13 @@ public sealed class InputBindingManager : MonoServiceBehaviour action = null; return false; } - /// - /// 开始重新绑定指定的输入操作 + /// ??? Action ??????????? /// - /// 操作名称 - /// 复合部分名称(可选) public void StartRebind(string actionName, string compositePartName = null) { var action = Action(actionName); if (action == null) return; - - // 自动决定 bindingIndex 和 deviceMatch int bindingIndex = FindBestBindingIndexForKeyboard(action, compositePartName); if (bindingIndex < 0) { @@ -635,81 +617,77 @@ public sealed class InputBindingManager : MonoServiceBehaviour Log.Info("[InputBindingManager] Rebind started"); } } - /// - /// 取消当前的重新绑定操作 + /// ??????????????? /// public void CancelRebind() => rebindOperation?.Cancel(); - /// - /// 确认并应用准备好的重新绑定 + /// ???????????????????? /// - /// 是否清除冲突 - /// 是否成功应用 public async Task ConfirmApply(bool clearConflicts = true) { if (!isApplyPending) return false; - try + RebindContext[] appliedContexts = OnApply != null ? BuildPreparedSnapshot() : null; + foreach (var ctx in preparedRebinds) { - // 在清除之前创建准备好的重绑定的副本 - HashSet appliedContexts = OnApply != null - ? new HashSet(preparedRebinds) - : null; - - foreach (var ctx in preparedRebinds) + if (ctx.overridePath == NULL_BINDING && !clearConflicts) { - if (!string.IsNullOrEmpty(ctx.overridePath)) - { - if (ctx.overridePath == NULL_BINDING) - { - ctx.action.RemoveBindingOverride(ctx.bindingIndex); - } - else - { - ctx.action.ApplyBindingOverride(ctx.bindingIndex, ctx.overridePath); - } - } + continue; + } - var bp = GetBindingPath(ctx.action, ctx.bindingIndex); - if (bp != null) + if (!string.IsNullOrEmpty(ctx.overridePath)) + { + if (ctx.overridePath == NULL_BINDING) { - bp.EffectivePath = (ctx.overridePath == NULL_BINDING) ? string.Empty : ctx.overridePath; + ctx.action.RemoveBindingOverride(ctx.bindingIndex); + } + else + { + ctx.action.ApplyBindingOverride(ctx.bindingIndex, ctx.overridePath); } } - preparedRebinds.Clear(); - await WriteOverridesToDiskAsync(); - BindingsChanged?.Invoke(); - OnApply?.Invoke(true, appliedContexts); - isApplyPending = false; - if (debugMode) + var bp = GetBindingPath(ctx.action, ctx.bindingIndex); + if (bp != null) { - Log.Info("[InputBindingManager] Apply confirmed and saved."); + bp.EffectivePath = ctx.action.bindings[ctx.bindingIndex].effectivePath; } + } - return true; - } - catch (Exception ex) + preparedRebinds.Clear(); + await WriteOverridesToDiskAsync(); + BindingsChanged?.Invoke(); + OnApply?.Invoke(true, appliedContexts); + isApplyPending = false; + if (debugMode) { - Log.Error("[InputBindingManager] Failed to apply binds: " + ex); - OnApply?.Invoke(false, null); - return false; + Log.Info("[InputBindingManager] Apply confirmed and saved."); } + + return true; + } + + private RebindContext[] BuildPreparedSnapshot() + { + if (preparedRebinds.Count == 0) + { + return Array.Empty(); + } + + RebindContext[] snapshot = new RebindContext[preparedRebinds.Count]; + preparedRebinds.CopyTo(snapshot); + return snapshot; } /// - /// 丢弃准备好的重新绑定 + /// ??????????????? /// public void DiscardPrepared() { if (!isApplyPending) return; - // 在清除之前创建准备好的重绑定的副本(用于事件通知) - HashSet discardedContexts = OnApply != null - ? new HashSet(preparedRebinds) - : null; - + RebindContext[] discardedContexts = OnApply != null ? BuildPreparedSnapshot() : null; preparedRebinds.Clear(); isApplyPending = false; OnApply?.Invoke(false, discardedContexts); @@ -718,52 +696,40 @@ public sealed class InputBindingManager : MonoServiceBehaviour Log.Info("[InputBindingManager] Prepared rebinds discarded."); } } - /// - /// 重置所有绑定到默认值 + /// ????????????????????? /// public async Task ResetToDefaultAsync() { - try + if (!string.IsNullOrEmpty(defaultBindingsJson)) { - if (!string.IsNullOrEmpty(defaultBindingsJson)) + actions.LoadBindingOverridesFromJson(defaultBindingsJson); + } + else + { + foreach (var map in actionMap.Values) { - actions.LoadBindingOverridesFromJson(defaultBindingsJson); - } - else - { - foreach (var map in actionMap.Values) + foreach (var a in map.actions.Values) { - foreach (var a in map.actions.Values) + for (int b = 0; b < a.action.bindings.Count; b++) { - for (int b = 0; b < a.action.bindings.Count; b++) - { - a.action.RemoveBindingOverride(b); - } + a.action.RemoveBindingOverride(b); } } } - - RefreshBindingPathsFromActions(); - await WriteOverridesToDiskAsync(); - BindingsChanged?.Invoke(); - if (debugMode) - { - Log.Info("Reset to default and saved."); - } } - catch (Exception ex) + + RefreshBindingPathsFromActions(); + await WriteOverridesToDiskAsync(); + BindingsChanged?.Invoke(); + if (debugMode) { - Log.Error("Failed to reset defaults: " + ex); + Log.Info("Reset to default and saved."); } } - /// - /// 获取指定操作的绑定路径 + /// ???? Action ????????????????? /// - /// 操作名称 - /// 绑定索引 - /// 绑定路径,未找到则返回 null public BindingPath GetBindingPath(string actionName, int bindingIndex = 0) { if (TryGetActionRecord(actionName, out var result) diff --git a/Runtime/InputGlyph/Core/InputDeviceWatcher.cs b/Runtime/InputGlyph/Core/InputDeviceWatcher.cs index 8c7487d..d4a5a55 100644 --- a/Runtime/InputGlyph/Core/InputDeviceWatcher.cs +++ b/Runtime/InputGlyph/Core/InputDeviceWatcher.cs @@ -91,7 +91,9 @@ public static class InputDeviceWatcher private static InputAction _anyInputAction; private static float _lastSwitchTime = -Mathf.Infinity; private static DeviceContext _lastEmittedContext = CreateDefaultContext(); - private static readonly Dictionary DeviceContextCache = new(); + private const int InitialDeviceCacheCapacity = 16; + private static DeviceContext[] DeviceContextCache = new DeviceContext[InitialDeviceCacheCapacity]; + private static int DeviceContextCacheCount; private static bool _initialized; public static event Action OnDeviceChanged; @@ -112,13 +114,22 @@ public static class InputDeviceWatcher _anyInputAction = new InputAction("AnyDevice", InputActionType.PassThrough); _anyInputAction.AddBinding("/anyKey"); //为防止误触 暂时屏蔽鼠标检测 - _anyInputAction.AddBinding("/delta"); _anyInputAction.AddBinding("/leftButton"); _anyInputAction.AddBinding("/rightButton"); _anyInputAction.AddBinding("/middleButton"); - // _anyInputAction.AddBinding("/scroll"); - _anyInputAction.AddBinding("/*"); - _anyInputAction.AddBinding("/*"); + _anyInputAction.AddBinding("/buttonSouth"); + _anyInputAction.AddBinding("/buttonNorth"); + _anyInputAction.AddBinding("/buttonEast"); + _anyInputAction.AddBinding("/buttonWest"); + _anyInputAction.AddBinding("/start"); + _anyInputAction.AddBinding("/select"); + _anyInputAction.AddBinding("/leftStick"); + _anyInputAction.AddBinding("/rightStick"); + _anyInputAction.AddBinding("/dpad"); + _anyInputAction.AddBinding("/leftTrigger"); + _anyInputAction.AddBinding("/rightTrigger"); + _anyInputAction.AddBinding("/trigger"); + _anyInputAction.AddBinding("/stick"); _anyInputAction.performed += OnAnyInputPerformed; _anyInputAction.Enable(); @@ -155,7 +166,8 @@ public static class InputDeviceWatcher } InputSystem.onDeviceChange -= OnDeviceChange; - DeviceContextCache.Clear(); + Array.Clear(DeviceContextCache, 0, DeviceContextCacheCount); + DeviceContextCacheCount = 0; ApplyContext(CreateDefaultContext(), false); _lastEmittedContext = CurrentContext; @@ -206,7 +218,7 @@ public static class InputDeviceWatcher { case InputDeviceChange.Removed: case InputDeviceChange.Disconnected: - DeviceContextCache.Remove(device.deviceId); + RemoveCachedContext(device.deviceId); if (device.deviceId == CurrentDeviceId) { PromoteFallbackDevice(device.deviceId); @@ -215,7 +227,7 @@ public static class InputDeviceWatcher break; case InputDeviceChange.Reconnected: case InputDeviceChange.Added: - DeviceContextCache.Remove(device.deviceId); + RemoveCachedContext(device.deviceId); if (CurrentDeviceId < 0 && IsRelevantDevice(device)) { SetCurrentContext(BuildContext(device)); @@ -283,7 +295,7 @@ public static class InputDeviceWatcher return CreateDefaultContext(); } - if (DeviceContextCache.TryGetValue(device.deviceId, out DeviceContext cachedContext)) + if (TryGetCachedContext(device.deviceId, out DeviceContext cachedContext)) { return cachedContext; } @@ -297,10 +309,55 @@ public static class InputDeviceWatcher productId, deviceName, device.layout); - DeviceContextCache[device.deviceId] = context; + AddCachedContext(context); return context; } + private static bool TryGetCachedContext(int deviceId, out DeviceContext context) + { + for (int i = 0; i < DeviceContextCacheCount; i++) + { + if (DeviceContextCache[i].DeviceId == deviceId) + { + context = DeviceContextCache[i]; + return true; + } + } + + context = default; + return false; + } + + private static void AddCachedContext(DeviceContext context) + { + if (DeviceContextCacheCount == DeviceContextCache.Length) + { + Array.Resize(ref DeviceContextCache, DeviceContextCache.Length << 1); + } + + DeviceContextCache[DeviceContextCacheCount++] = context; + } + + private static void RemoveCachedContext(int deviceId) + { + for (int i = 0; i < DeviceContextCacheCount; i++) + { + if (DeviceContextCache[i].DeviceId != deviceId) + { + continue; + } + + DeviceContextCacheCount--; + if (i < DeviceContextCacheCount) + { + DeviceContextCache[i] = DeviceContextCache[DeviceContextCacheCount]; + } + + DeviceContextCache[DeviceContextCacheCount] = default; + return; + } + } + private static DeviceContext CreateDefaultContext() { return new DeviceContext(InputDeviceCategory.Keyboard, -1, 0, 0, DefaultKeyboardDeviceName, Keyboard.current != null ? Keyboard.current.layout : string.Empty); @@ -355,9 +412,9 @@ public static class InputDeviceWatcher case ButtonControl button: return button.IsPressed(); case StickControl stick: - return stick.ReadValue().sqrMagnitude >= StickActivationThreshold; + return stick.ReadValue().sqrMagnitude >= StickActivationThreshold * StickActivationThreshold; case Vector2Control vector2: - return vector2.ReadValue().sqrMagnitude >= StickActivationThreshold; + return vector2.ReadValue().sqrMagnitude >= StickActivationThreshold * StickActivationThreshold; case AxisControl axis: return Mathf.Abs(axis.ReadValue()) >= AxisActivationThreshold; default: diff --git a/Runtime/InputGlyph/Data/InputGlyphDatabase.cs b/Runtime/InputGlyph/Data/InputGlyphDatabase.cs index 59690a2..410216d 100644 --- a/Runtime/InputGlyph/Data/InputGlyphDatabase.cs +++ b/Runtime/InputGlyph/Data/InputGlyphDatabase.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.InputSystem; @@ -26,6 +26,8 @@ public sealed class InputGlyphDatabase : ScriptableObject private const string DeviceXbox = "Xbox"; private const string DevicePlayStation = "PlayStation"; private const string DeviceOther = "Other"; + private const int CategoryCount = 4; + private const int InitialPathCapacity = 128; private static readonly InputDeviceWatcher.InputDeviceCategory[] KeyboardLookupOrder = { InputDeviceWatcher.InputDeviceCategory.Keyboard }; private static readonly InputDeviceWatcher.InputDeviceCategory[] XboxLookupOrder = { @@ -45,13 +47,20 @@ public sealed class InputGlyphDatabase : ScriptableObject InputDeviceWatcher.InputDeviceCategory.Xbox, InputDeviceWatcher.InputDeviceCategory.Keyboard, }; - private static readonly Dictionary NormalizedPathCache = new(StringComparer.Ordinal); public List tables = new List(); public Sprite placeholderSprite; - private Dictionary _tableCache; - private Dictionary> _pathLookup; + private DeviceGlyphTable[] _tableByCategory = new DeviceGlyphTable[CategoryCount]; + private PathLookup[] _pathLookupByCategory = new PathLookup[CategoryCount]; + private bool _cacheBuilt; + + private struct PathLookup + { + public string[] Keys; + public Sprite[] Sprites; + public int Count; + } private void OnEnable() { @@ -73,23 +82,20 @@ public sealed class InputGlyphDatabase : ScriptableObject } EnsureCache(); - _tableCache.TryGetValue(deviceName, out DeviceGlyphTable table); - return table; + return GetTable(ParseCategory(deviceName)); } public DeviceGlyphTable GetTable(InputDeviceWatcher.InputDeviceCategory device) { - switch (device) + EnsureCache(); + int index = CategoryIndex(device); + DeviceGlyphTable table = _tableByCategory[index]; + if (table != null) { - case InputDeviceWatcher.InputDeviceCategory.Keyboard: - return GetTable(DeviceKeyboard); - case InputDeviceWatcher.InputDeviceCategory.Xbox: - return GetTable(DeviceXbox); - case InputDeviceWatcher.InputDeviceCategory.PlayStation: - return GetTable(DevicePlayStation); - default: - return GetTable(DeviceOther) ?? GetTable(DeviceXbox); + return table; } + + return device == InputDeviceWatcher.InputDeviceCategory.Other ? _tableByCategory[CategoryIndex(InputDeviceWatcher.InputDeviceCategory.Xbox)] : null; } public Sprite GetPlatformIcon(InputDeviceWatcher.InputDeviceCategory device) @@ -111,8 +117,7 @@ public sealed class InputGlyphDatabase : ScriptableObject InputDeviceWatcher.InputDeviceCategory[] lookupOrder = GetLookupOrder(device); for (int i = 0; i < lookupOrder.Length; i++) { - InputDeviceWatcher.InputDeviceCategory category = lookupOrder[i]; - if (_pathLookup.TryGetValue(category, out Dictionary map) && map.TryGetValue(key, out sprite) && sprite != null) + if (TryGetSpriteInCategory(lookupOrder[i], key, out sprite)) { return true; } @@ -122,7 +127,7 @@ public sealed class InputGlyphDatabase : ScriptableObject return sprite != null; } - public Sprite FindSprite(string controlPath, InputDeviceWatcher.InputDeviceCategory device) + public Sprite GetSprite(string controlPath, InputDeviceWatcher.InputDeviceCategory device) { return TryGetSprite(controlPath, device, out Sprite sprite) ? sprite : placeholderSprite; } @@ -158,7 +163,7 @@ public sealed class InputGlyphDatabase : ScriptableObject private void EnsureCache() { - if (_tableCache == null || _pathLookup == null) + if (!_cacheBuilt) { BuildCache(); } @@ -166,16 +171,26 @@ public sealed class InputGlyphDatabase : ScriptableObject private void BuildCache() { - _tableCache ??= new Dictionary(StringComparer.OrdinalIgnoreCase); - _tableCache.Clear(); + if (_tableByCategory == null || _tableByCategory.Length != CategoryCount) + { + _tableByCategory = new DeviceGlyphTable[CategoryCount]; + } + else + { + Array.Clear(_tableByCategory, 0, _tableByCategory.Length); + } - _pathLookup ??= new Dictionary>(); - _pathLookup.Clear(); - InitializeLookup(InputDeviceWatcher.InputDeviceCategory.Keyboard); - InitializeLookup(InputDeviceWatcher.InputDeviceCategory.Xbox); - InitializeLookup(InputDeviceWatcher.InputDeviceCategory.PlayStation); - InitializeLookup(InputDeviceWatcher.InputDeviceCategory.Other); + if (_pathLookupByCategory == null || _pathLookupByCategory.Length != CategoryCount) + { + _pathLookupByCategory = new PathLookup[CategoryCount]; + } + for (int i = 0; i < CategoryCount; i++) + { + ResetLookup(ref _pathLookupByCategory[i]); + } + + _cacheBuilt = true; if (tables == null) { return; @@ -189,10 +204,10 @@ public sealed class InputGlyphDatabase : ScriptableObject continue; } - _tableCache[table.deviceName] = table; InputDeviceWatcher.InputDeviceCategory category = ParseCategory(table.deviceName); - Dictionary map = _pathLookup[category]; - RegisterEntries(table, map); + int categoryIndex = CategoryIndex(category); + _tableByCategory[categoryIndex] = table; + RegisterEntries(table, ref _pathLookupByCategory[categoryIndex]); } } @@ -208,12 +223,23 @@ public sealed class InputGlyphDatabase : ScriptableObject } #endif - private void InitializeLookup(InputDeviceWatcher.InputDeviceCategory category) + private static void ResetLookup(ref PathLookup lookup) { - _pathLookup[category] = new Dictionary(StringComparer.OrdinalIgnoreCase); + if (lookup.Keys == null || lookup.Keys.Length == 0) + { + lookup.Keys = new string[InitialPathCapacity]; + lookup.Sprites = new Sprite[InitialPathCapacity]; + } + else + { + Array.Clear(lookup.Keys, 0, lookup.Count); + Array.Clear(lookup.Sprites, 0, lookup.Count); + } + + lookup.Count = 0; } - private void RegisterEntries(DeviceGlyphTable table, Dictionary map) + private void RegisterEntries(DeviceGlyphTable table, ref PathLookup lookup) { if (table.entries == null) { @@ -230,21 +256,56 @@ public sealed class InputGlyphDatabase : ScriptableObject for (int j = 0; j < entry.action.bindings.Count; j++) { - RegisterBinding(map, entry.action.bindings[j].path, entry.Sprite); - RegisterBinding(map, entry.action.bindings[j].effectivePath, entry.Sprite); + RegisterBinding(ref lookup, entry.action.bindings[j].path, entry.Sprite); + RegisterBinding(ref lookup, entry.action.bindings[j].effectivePath, entry.Sprite); } } } - private void RegisterBinding(Dictionary map, string controlPath, Sprite sprite) + private static void RegisterBinding(ref PathLookup lookup, string controlPath, Sprite sprite) { string key = NormalizeControlPath(controlPath); - if (string.IsNullOrEmpty(key) || map.ContainsKey(key)) + if (string.IsNullOrEmpty(key) || IndexOf(lookup.Keys, lookup.Count, key) >= 0) { return; } - map[key] = sprite; + if (lookup.Count == lookup.Keys.Length) + { + Array.Resize(ref lookup.Keys, lookup.Keys.Length << 1); + Array.Resize(ref lookup.Sprites, lookup.Sprites.Length << 1); + } + + lookup.Keys[lookup.Count] = key; + lookup.Sprites[lookup.Count] = sprite; + lookup.Count++; + } + + private bool TryGetSpriteInCategory(InputDeviceWatcher.InputDeviceCategory category, string key, out Sprite sprite) + { + PathLookup lookup = _pathLookupByCategory[CategoryIndex(category)]; + int index = IndexOf(lookup.Keys, lookup.Count, key); + if (index >= 0) + { + sprite = lookup.Sprites[index]; + return sprite != null; + } + + sprite = null; + return false; + } + + private static int IndexOf(string[] keys, int count, string key) + { + for (int i = 0; i < count; i++) + { + if (string.Equals(keys[i], key, StringComparison.OrdinalIgnoreCase)) + { + return i; + } + } + + return -1; } private static string NormalizeControlPath(string controlPath) @@ -254,14 +315,7 @@ public sealed class InputGlyphDatabase : ScriptableObject return string.Empty; } - if (NormalizedPathCache.TryGetValue(controlPath, out string normalizedPath)) - { - return normalizedPath; - } - - normalizedPath = CanonicalizeDeviceLayout(controlPath.Trim().ToLowerInvariant()); - NormalizedPathCache[controlPath] = normalizedPath; - return normalizedPath; + return CanonicalizeDeviceLayout(controlPath.Trim().ToLowerInvariant()); } private static string CanonicalizeDeviceLayout(string controlPath) @@ -342,6 +396,21 @@ public sealed class InputGlyphDatabase : ScriptableObject return InputDeviceWatcher.InputDeviceCategory.Other; } + private static int CategoryIndex(InputDeviceWatcher.InputDeviceCategory category) + { + switch (category) + { + case InputDeviceWatcher.InputDeviceCategory.Keyboard: + return 0; + case InputDeviceWatcher.InputDeviceCategory.Xbox: + return 1; + case InputDeviceWatcher.InputDeviceCategory.PlayStation: + return 2; + default: + return 3; + } + } + private static InputDeviceWatcher.InputDeviceCategory[] GetLookupOrder(InputDeviceWatcher.InputDeviceCategory device) { switch (device) @@ -356,4 +425,4 @@ public sealed class InputGlyphDatabase : ScriptableObject return OtherLookupOrder; } } -} +} \ No newline at end of file diff --git a/Runtime/InputGlyph/Editor/InputGlyphDatabaseEditor.cs b/Runtime/InputGlyph/Editor/InputGlyphDatabaseEditor.cs deleted file mode 100644 index 8d70986..0000000 --- a/Runtime/InputGlyph/Editor/InputGlyphDatabaseEditor.cs +++ /dev/null @@ -1,1189 +0,0 @@ -using System; -using System.Collections.Generic; -using UnityEditor; -using UnityEngine; -using UnityEngine.InputSystem; - -[CustomEditor(typeof(InputGlyphDatabase))] -public sealed class InputGlyphDatabaseEditor : Editor -{ - 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; - private const int DefaultEntriesPerPage = 10; - - private static readonly string[] DefaultTableNames = { "Keyboard", "Xbox", "PlayStation", "Other" }; - private static readonly int[] EntriesPerPageOptions = { 10, 15, 20, 25 }; - private static readonly string[] EntriesPerPageLabels = { "10|页", "15|页", "20|页", "25|页" }; - - private sealed class TableEditorState - { - public Sprite PendingSprite; - public bool ShowValidation = true; - public string EntrySearch = string.Empty; - public int CurrentPage; - public int EntriesPerPage = DefaultEntriesPerPage; - public readonly List FilteredEntryIndices = new(); - public string CachedSearch = string.Empty; - public int CachedEntryCount = -1; - } - - private readonly List _tableStates = new(); - - private InputGlyphDatabase _database; - private SerializedProperty _tablesProperty; - private SerializedProperty _placeholderSpriteProperty; - private int _selectedTab; - private bool _showAddTable; - private bool _showDatabaseValidation = true; - private string _newTableName = string.Empty; - - 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(); - SyncTableStates(); - ClampSelectedTab(); - - DrawToolbar(); - if (_showAddTable) - { - DrawAddTableBar(); - } - - DrawMissingDefaultTablesNotice(); - - EditorGUILayout.Space(6f); - DrawTabs(); - EditorGUILayout.Space(8f); - - using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox)) - { - if (IsSettingsSelected) - { - DrawSettingsPanel(); - } - else - { - DrawTablePanel(_selectedTab); - } - } - - if (serializedObject.ApplyModifiedProperties()) - { - InvalidateAllEntryViews(); - NotifyDatabaseChanged(); - } - } - - private void DrawToolbar() - { - using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar)) - { - if (GUILayout.Button("Save Asset", EditorStyles.toolbarButton, GUILayout.Width(90f))) - { - serializedObject.ApplyModifiedProperties(); - InvalidateAllEntryViews(); - 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 ? "数据库校验" : $"数据库校验 ({issues.Count})"; - _showDatabaseValidation = EditorGUILayout.BeginFoldoutHeaderGroup(_showDatabaseValidation, title); - if (_showDatabaseValidation) - { - if (issues.Count == 0) - { - EditorGUILayout.HelpBox("未发现数据库级别的问题。", 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 ? "校验结果" : $"校验结果 ({issues.Count})"; - state.ShowValidation = EditorGUILayout.BeginFoldoutHeaderGroup(state.ShowValidation, title); - if (state.ShowValidation) - { - if (issues.Count == 0) - { - EditorGUILayout.HelpBox("未发现当前表的问题。", 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} 条问题未展开显示,以保持检视面板可读。", MessageType.None); - } - } - - private void DrawEntriesList(int tableIndex, SerializedProperty entriesProperty) - { - TableEditorState state = _tableStates[tableIndex]; - EditorGUILayout.LabelField("Entries", EditorStyles.boldLabel); - DrawEntriesControls(state, entriesProperty.arraySize); - - if (entriesProperty.arraySize == 0) - { - EditorGUILayout.HelpBox("当前表里还没有任何条目。", MessageType.Info); - return; - } - - List filteredIndices = GetFilteredEntryIndices(tableIndex, entriesProperty, state); - if (filteredIndices.Count == 0) - { - EditorGUILayout.HelpBox("没有匹配搜索条件的条目。", MessageType.Info); - return; - } - - int entriesPerPage = Mathf.Max(1, state.EntriesPerPage); - int totalPages = Mathf.Max(1, Mathf.CeilToInt(filteredIndices.Count / (float)entriesPerPage)); - state.CurrentPage = Mathf.Clamp(state.CurrentPage, 0, totalPages - 1); - - DrawEntriesPagination(state, filteredIndices.Count, entriesProperty.arraySize); - EditorGUILayout.Space(4f); - - int startIndex = state.CurrentPage * entriesPerPage; - int endIndex = Mathf.Min(startIndex + entriesPerPage, filteredIndices.Count); - for (int i = startIndex; i < endIndex; i++) - { - int entryIndex = filteredIndices[i]; - DrawEntryElement(tableIndex, entryIndex, entriesProperty.GetArrayElementAtIndex(entryIndex)); - EditorGUILayout.Space(4f); - } - - if (totalPages > 1) - { - DrawEntriesPagination(state, filteredIndices.Count, entriesProperty.arraySize); - } - } - - private void DrawEntriesControls(TableEditorState state, int totalEntries) - { - string search = EditorGUILayout.TextField("Search", state.EntrySearch); - if (!string.Equals(search, state.EntrySearch, StringComparison.Ordinal)) - { - state.EntrySearch = search; - state.CurrentPage = 0; - InvalidateEntryView(state); - } - - using (new EditorGUILayout.HorizontalScope()) - { - int entriesPerPage = EditorGUILayout.IntPopup("Page Size", state.EntriesPerPage, EntriesPerPageLabels, EntriesPerPageOptions); - if (entriesPerPage != state.EntriesPerPage) - { - state.EntriesPerPage = entriesPerPage; - state.CurrentPage = 0; - } - - GUILayout.FlexibleSpace(); - EditorGUILayout.LabelField($"总数: {totalEntries}", EditorStyles.miniLabel, GUILayout.Width(80f)); - - using (new EditorGUI.DisabledScope(string.IsNullOrEmpty(state.EntrySearch))) - { - if (GUILayout.Button("Clear Search", GUILayout.Width(100f))) - { - state.EntrySearch = string.Empty; - state.CurrentPage = 0; - InvalidateEntryView(state); - GUI.FocusControl(null); - } - } - } - } - - private void DrawEntriesPagination(TableEditorState state, int filteredCount, int totalEntries) - { - int entriesPerPage = Mathf.Max(1, state.EntriesPerPage); - int totalPages = Mathf.Max(1, Mathf.CeilToInt(filteredCount / (float)entriesPerPage)); - int startEntry = filteredCount == 0 ? 0 : state.CurrentPage * entriesPerPage + 1; - int endEntry = Mathf.Min(filteredCount, (state.CurrentPage + 1) * entriesPerPage); - - using (new EditorGUILayout.HorizontalScope(EditorStyles.helpBox)) - { - EditorGUILayout.LabelField( - $"显示 {startEntry}-{endEntry} / {filteredCount} 条", - EditorStyles.miniLabel, - GUILayout.Width(140f)); - - if (filteredCount != totalEntries) - { - EditorGUILayout.LabelField($"(筛选自 {totalEntries} 条)", EditorStyles.miniLabel, GUILayout.Width(100f)); - } - - GUILayout.FlexibleSpace(); - - using (new EditorGUI.DisabledScope(state.CurrentPage <= 0)) - { - if (GUILayout.Button("<<", GUILayout.Width(32f))) - { - state.CurrentPage = 0; - } - - if (GUILayout.Button("<", GUILayout.Width(28f))) - { - state.CurrentPage--; - } - } - - GUILayout.Label($"第 {state.CurrentPage + 1} / {totalPages} 页", EditorStyles.miniLabel, GUILayout.Width(72f)); - - using (new EditorGUI.DisabledScope(state.CurrentPage >= totalPages - 1)) - { - if (GUILayout.Button(">", GUILayout.Width(28f))) - { - state.CurrentPage++; - } - - if (GUILayout.Button(">>", GUILayout.Width(32f))) - { - state.CurrentPage = totalPages - 1; - } - } - } - } - - private void DrawEntryElement(int tableIndex, int entryIndex, SerializedProperty entryProperty) - { - SerializedProperty spriteProperty = entryProperty.FindPropertyRelative(EntrySpritePropertyName); - SerializedProperty actionProperty = entryProperty.FindPropertyRelative(EntryActionPropertyName); - Sprite sprite = spriteProperty.objectReferenceValue as Sprite; - using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox)) - { - using (new EditorGUILayout.HorizontalScope()) - { - GUILayout.Label(GetEntryTitle(tableIndex, entryIndex), EditorStyles.boldLabel); - GUILayout.FlexibleSpace(); - - using (new EditorGUI.DisabledScope(entryIndex <= 0)) - { - if (GUILayout.Button("↑", GUILayout.Width(28f))) - { - ApplyPendingInspectorChanges(); - MoveEntry(tableIndex, entryIndex, entryIndex - 1); - GUIUtility.ExitGUI(); - } - } - - using (new EditorGUI.DisabledScope(entryIndex >= _database.tables[tableIndex].entries.Count - 1)) - { - if (GUILayout.Button("↓", GUILayout.Width(28f))) - { - ApplyPendingInspectorChanges(); - MoveEntry(tableIndex, entryIndex, entryIndex + 1); - GUIUtility.ExitGUI(); - } - } - - using (new EditorGUI.DisabledScope(sprite == null)) - { - if (GUILayout.Button("Ping", GUILayout.Width(48f))) - { - EditorGUIUtility.PingObject(sprite); - } - } - - if (GUILayout.Button("Remove", GUILayout.Width(64f))) - { - if (EditorUtility.DisplayDialog("Remove Entry", "Remove the selected entry from the table?", "Remove", "Cancel")) - { - ApplyPendingInspectorChanges(); - RemoveEntry(tableIndex, entryIndex); - GUIUtility.ExitGUI(); - } - } - } - - using (new EditorGUILayout.HorizontalScope()) - { - Rect previewRect = GUILayoutUtility.GetRect(ListPreviewSize, ListPreviewSize, GUILayout.Width(ListPreviewSize), GUILayout.Height(ListPreviewSize)); - DrawSpritePreview(previewRect, sprite); - - using (new EditorGUILayout.VerticalScope()) - { - EditorGUILayout.PropertyField(spriteProperty, new GUIContent("Sprite"), true); - EditorGUILayout.PropertyField(actionProperty, new GUIContent("Action"), true); - } - } - } - } - - 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 - { - EditorGUI.ObjectField(rect, sprite, typeof(Sprite), false); - } - } - - private List CollectDatabaseValidationIssues() - { - List issues = new(); - if (_database.tables == null || _database.tables.Count == 0) - { - issues.Add("数据库中没有任何表,运行时查询将始终回退到占位图标。"); - return issues; - } - - List missingTables = GetMissingDefaultTables(); - if (missingTables.Count > 0) - { - issues.Add($"缺少推荐表: {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)) - { - issues.Add($"表 {i + 1} 的设备名称为空。"); - continue; - } - - if (!seenNames.Add(tableName)) - { - duplicateNames.Add(tableName); - } - } - - foreach (string duplicateName in duplicateNames) - { - issues.Add($"检测到重复的表名 '{duplicateName}'。"); - } - - return issues; - } - - private List CollectTableValidationIssues(int tableIndex) - { - List issues = new(); - if (!IsValidTableIndex(tableIndex)) - { - issues.Add("当前选中的表无效。"); - return issues; - } - - DeviceGlyphTable table = _database.tables[tableIndex]; - if (table.entries == null || table.entries.Count == 0) - { - issues.Add("当前表没有任何条目。"); - return issues; - } - - 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) - { - issues.Add($"条目 {i + 1} 为空。"); - 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); - } - } - - if (missingSpriteCount > 0) - { - issues.Add($"{missingSpriteCount} 个条目未绑定 Sprite。"); - } - - if (missingActionCount > 0) - { - issues.Add($"{missingActionCount} 个条目未绑定 Action,这些条目不会参与运行时路径查找。"); - } - - foreach (string spriteName in duplicateSprites) - { - issues.Add($"当前表中存在重复的 Sprite 名称 '{spriteName}'。"); - } - - foreach (KeyValuePair> pair in bindingOwners) - { - if (pair.Value.Count <= 1) - { - continue; - } - - issues.Add($"绑定 '{pair.Key}' 被多个条目共用: {string.Join(", ", pair.Value)}。运行时只会保留第一个匹配项。"); - } - - return issues; - } - - private void RegisterBindingOwner(Dictionary> bindingOwners, string controlPath, string ownerLabel) - { - string normalizedPath = InputGlyphDatabase.EditorNormalizeControlPath(controlPath); - if (string.IsNullOrEmpty(normalizedPath)) - { - return; - } - - 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)) - { - return; - } - - 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(); - InvalidateEntryView(tableIndex); - NotifyDatabaseChanged(); - } - - private void ClearEntries(int tableIndex) - { - if (!IsValidTableIndex(tableIndex)) - { - return; - } - - Undo.RecordObject(_database, "Clear glyph entries"); - DeviceGlyphTable table = _database.tables[tableIndex]; - table.entries ??= new List(); - table.entries.Clear(); - serializedObject.Update(); - InvalidateEntryView(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(); - InvalidateAllEntryViews(); - NotifyDatabaseChanged(); - } - - private void RemoveTable(int tableIndex) - { - if (!IsValidTableIndex(tableIndex)) - { - return; - } - - Undo.RecordObject(_database, "Remove glyph table"); - _database.tables.RemoveAt(tableIndex); - SyncTableStates(); - ClampSelectedTab(); - serializedObject.Update(); - InvalidateAllEntryViews(); - 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(); - InvalidateAllEntryViews(); - 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 resolve the sprite sheet asset path."); - return; - } - - UnityEngine.Object[] assets = AssetDatabase.LoadAllAssetsAtPath(path); - if (assets == null || assets.Length == 0) - { - Debug.LogWarning($"[InputGlyphDatabase] No sub-assets found at '{path}'."); - return; - } - - List sprites = new(); - for (int i = 0; i < assets.Length; i++) - { - if (assets[i] is Sprite sprite) - { - sprites.Add(sprite); - } - } - - if (sprites.Count == 0) - { - EditorUtility.DisplayDialog("No Sprites Found", "The selected texture does not contain any sprite sub-assets.", "OK"); - return; - } - - 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++) - { - GlyphEntry entry = table.entries[i]; - if (entry?.Sprite == null) - { - continue; - } - - if (!entriesByName.ContainsKey(entry.Sprite.name)) - { - entriesByName.Add(entry.Sprite.name, entry); - } - } - - int replaced = 0; - int added = 0; - for (int i = 0; i < sprites.Count; i++) - { - Sprite sprite = sprites[i]; - if (entriesByName.TryGetValue(sprite.name, out GlyphEntry entry)) - { - if (entry.Sprite != sprite) - { - entry.Sprite = sprite; - replaced++; - } - } - else - { - GlyphEntry newEntry = new GlyphEntry { Sprite = sprite, action = null }; - table.entries.Add(newEntry); - entriesByName.Add(sprite.name, newEntry); - added++; - } - } - - serializedObject.Update(); - InvalidateEntryView(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}"); - } - - private void ApplyPendingInspectorChanges() - { - if (serializedObject.ApplyModifiedProperties()) - { - InvalidateAllEntryViews(); - 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; - } - - _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 MoveEntry(int tableIndex, int fromIndex, int toIndex) - { - if (!IsValidTableIndex(tableIndex)) - { - return; - } - - List entries = _database.tables[tableIndex].entries; - if (entries == null || fromIndex < 0 || fromIndex >= entries.Count || toIndex < 0 || toIndex >= entries.Count || fromIndex == toIndex) - { - return; - } - - Undo.RecordObject(_database, "Move glyph entry"); - GlyphEntry entry = entries[fromIndex]; - entries.RemoveAt(fromIndex); - entries.Insert(toIndex, entry); - serializedObject.Update(); - InvalidateEntryView(tableIndex); - NotifyDatabaseChanged(); - } - - private void RemoveEntry(int tableIndex, int entryIndex) - { - if (!IsValidTableIndex(tableIndex)) - { - return; - } - - List entries = _database.tables[tableIndex].entries; - if (entries == null || entryIndex < 0 || entryIndex >= entries.Count) - { - return; - } - - Undo.RecordObject(_database, "Remove glyph entry"); - entries.RemoveAt(entryIndex); - serializedObject.Update(); - InvalidateEntryView(tableIndex); - NotifyDatabaseChanged(); - } - - private List GetFilteredEntryIndices(int tableIndex, SerializedProperty entriesProperty, TableEditorState state) - { - string search = state.EntrySearch != null ? state.EntrySearch.Trim() : string.Empty; - if (state.CachedEntryCount == entriesProperty.arraySize && string.Equals(state.CachedSearch, search, StringComparison.Ordinal)) - { - return state.FilteredEntryIndices; - } - - state.FilteredEntryIndices.Clear(); - for (int i = 0; i < entriesProperty.arraySize; i++) - { - if (DoesEntryMatchSearch(tableIndex, i, search)) - { - state.FilteredEntryIndices.Add(i); - } - } - - state.CachedEntryCount = entriesProperty.arraySize; - state.CachedSearch = search; - return state.FilteredEntryIndices; - } - - private bool DoesEntryMatchSearch(int tableIndex, int entryIndex, string search) - { - if (string.IsNullOrWhiteSpace(search)) - { - return true; - } - - if (!IsValidTableIndex(tableIndex)) - { - return false; - } - - List entries = _database.tables[tableIndex].entries; - if (entries == null || entryIndex < 0 || entryIndex >= entries.Count) - { - return false; - } - - GlyphEntry entry = entries[entryIndex]; - if (entry == null) - { - return ContainsIgnoreCase("null", search); - } - - if (ContainsIgnoreCase(entry.Sprite != null ? entry.Sprite.name : string.Empty, search) - || ContainsIgnoreCase(entry.action != null ? entry.action.name : string.Empty, search)) - { - return true; - } - - if (entry.action == null) - { - return false; - } - - for (int i = 0; i < entry.action.bindings.Count; i++) - { - InputBinding binding = entry.action.bindings[i]; - if (ContainsIgnoreCase(binding.path, search) || ContainsIgnoreCase(binding.effectivePath, search)) - { - return true; - } - } - - return false; - } - - private static bool ContainsIgnoreCase(string value, string search) - { - return !string.IsNullOrEmpty(value) && value.IndexOf(search, StringComparison.OrdinalIgnoreCase) >= 0; - } - - private string GetEntryTitle(int tableIndex, int entryIndex) - { - if (!IsValidTableIndex(tableIndex)) - { - return $"Entry #{entryIndex + 1}"; - } - - List entries = _database.tables[tableIndex].entries; - if (entries == null || entryIndex < 0 || entryIndex >= entries.Count) - { - return $"Entry #{entryIndex + 1}"; - } - - GlyphEntry entry = entries[entryIndex]; - string spriteName = entry?.Sprite != null ? entry.Sprite.name : "No Sprite"; - string actionName = entry?.action != null ? entry.action.name : "No Action"; - return $"#{entryIndex + 1} {spriteName} / {actionName}"; - } - - private void InvalidateEntryView(int tableIndex) - { - if (tableIndex < 0 || tableIndex >= _tableStates.Count) - { - return; - } - - InvalidateEntryView(_tableStates[tableIndex]); - } - - private static void InvalidateEntryView(TableEditorState state) - { - state.FilteredEntryIndices.Clear(); - state.CachedSearch = string.Empty; - state.CachedEntryCount = -1; - } - - private void InvalidateAllEntryViews() - { - for (int i = 0; i < _tableStates.Count; i++) - { - InvalidateEntryView(_tableStates[i]); - } - } - - 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/Runtime/InputGlyph/InputGlyph.cs b/Runtime/InputGlyph/InputGlyph.cs index 3d2d64a..c570d2b 100644 --- a/Runtime/InputGlyph/InputGlyph.cs +++ b/Runtime/InputGlyph/InputGlyph.cs @@ -150,14 +150,20 @@ public sealed class InputGlyph : InputGlyphBehaviourBase return; } - string formattedText = Utility.Text.Format(_templateText, replacementToken); if (_cachedReplacementToken == replacementToken - && _cachedFormattedText == formattedText - && targetText.text == formattedText) + && !string.IsNullOrEmpty(_cachedFormattedText) + && targetText.text == _cachedFormattedText) { return; } + string formattedText = Utility.Text.Format(_templateText, replacementToken); + if (_cachedFormattedText == formattedText && targetText.text == formattedText) + { + _cachedReplacementToken = replacementToken; + return; + } + _cachedReplacementToken = replacementToken; if (_cachedFormattedText != formattedText || targetText.text != formattedText) { diff --git a/Runtime/InputGlyph/TestRebindScript.cs b/Samples~/InputGlyph/TestRebindScript.cs similarity index 100% rename from Runtime/InputGlyph/TestRebindScript.cs rename to Samples~/InputGlyph/TestRebindScript.cs diff --git a/Runtime/InputGlyph/TestRebindScript.cs.meta b/Samples~/InputGlyph/TestRebindScript.cs.meta similarity index 100% rename from Runtime/InputGlyph/TestRebindScript.cs.meta rename to Samples~/InputGlyph/TestRebindScript.cs.meta