com.alicizax.unity.ui.exten.../Runtime/InputGlyph/Editor/InputGlyphDatabaseEditor.cs

1190 lines
40 KiB
C#
Raw Normal View History

2026-03-20 16:50:30 +08:00
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;
}
}