AlicizaX/Client/Assets/Scripts/CustomeModule/InputGlyph/Editor/InputGlyphDatabaseEditor.cs
2026-03-19 20:17:04 +08:00

1190 lines
40 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections.Generic;
using 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<int> FilteredEntryIndices = new();
public string CachedSearch = string.Empty;
public int CachedEntryCount = -1;
}
private readonly List<TableEditorState> _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<string> 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<string> 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<string> 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<string> 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<int> 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<string> CollectDatabaseValidationIssues()
{
List<string> issues = new();
if (_database.tables == null || _database.tables.Count == 0)
{
issues.Add("数据库中没有任何表,运行时查询将始终回退到占位图标。");
return issues;
}
List<string> missingTables = GetMissingDefaultTables();
if (missingTables.Count > 0)
{
issues.Add($"缺少推荐表: {string.Join(", ", missingTables)}。");
}
HashSet<string> seenNames = new(StringComparer.OrdinalIgnoreCase);
HashSet<string> 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<string> CollectTableValidationIssues(int tableIndex)
{
List<string> 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<string> seenSprites = new(StringComparer.OrdinalIgnoreCase);
HashSet<string> duplicateSprites = new(StringComparer.OrdinalIgnoreCase);
Dictionary<string, List<string>> 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<string, List<string>> pair in bindingOwners)
{
if (pair.Value.Count <= 1)
{
continue;
}
issues.Add($"绑定 '{pair.Key}' 被多个条目共用: {string.Join(", ", pair.Value)}。运行时只会保留第一个匹配项。");
}
return issues;
}
private void RegisterBindingOwner(Dictionary<string, List<string>> bindingOwners, string controlPath, string ownerLabel)
{
string normalizedPath = InputGlyphDatabase.EditorNormalizeControlPath(controlPath);
if (string.IsNullOrEmpty(normalizedPath))
{
return;
}
if (!bindingOwners.TryGetValue(normalizedPath, out List<string> owners))
{
owners = new List<string>();
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<GlyphEntry>();
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<GlyphEntry>();
table.entries.Clear();
serializedObject.Update();
InvalidateEntryView(tableIndex);
NotifyDatabaseChanged();
}
private void AddTable(string tableName)
{
Undo.RecordObject(_database, "Add glyph table");
_database.tables ??= new List<DeviceGlyphTable>();
_database.tables.Add(new DeviceGlyphTable
{
deviceName = tableName,
spriteSheetTexture = null,
platformIcons = null,
entries = new List<GlyphEntry>()
});
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<string> missingTables = GetMissingDefaultTables();
if (missingTables.Count == 0)
{
return;
}
Undo.RecordObject(_database, "Create standard glyph tables");
_database.tables ??= new List<DeviceGlyphTable>();
for (int i = 0; i < missingTables.Count; i++)
{
_database.tables.Add(new DeviceGlyphTable
{
deviceName = missingTables[i],
spriteSheetTexture = null,
platformIcons = null,
entries = new List<GlyphEntry>()
});
}
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<Sprite> 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<GlyphEntry>();
Dictionary<string, GlyphEntry> 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<GlyphEntry> 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<GlyphEntry> 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<int> 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<GlyphEntry> 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<GlyphEntry> 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<string> GetMissingDefaultTables()
{
List<string> missingTables = new();
for (int i = 0; i < DefaultTableNames.Length; i++)
{
if (!HasTable(DefaultTableNames[i]))
{
missingTables.Add(DefaultTableNames[i]);
}
}
return missingTables;
}
}