AlicizaX/Client/Assets/Scripts/CustomeModule/InputGlyph/Editor/InputGlyphDatabaseEditor.cs

962 lines
32 KiB
C#
Raw Normal View History

2025-12-05 19:04:53 +08:00
using System;
2025-12-05 20:57:29 +08:00
using System.Collections.Generic;
2025-12-05 19:04:53 +08:00
using UnityEditor;
2026-03-19 13:59:42 +08:00
using UnityEditorInternal;
2025-12-05 19:04:53 +08:00
using UnityEngine;
2026-03-19 13:59:42 +08:00
using UnityEngine.InputSystem;
2025-12-05 19:04:53 +08:00
[CustomEditor(typeof(InputGlyphDatabase))]
2026-03-19 13:59:42 +08:00
public sealed class InputGlyphDatabaseEditor : Editor
2025-12-05 19:04:53 +08:00
{
2026-03-19 13:59:42 +08:00
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 static readonly string[] DefaultTableNames = { "Keyboard", "Xbox", "PlayStation", "Other" };
private sealed class TableEditorState
{
public Sprite PendingSprite;
public bool ShowValidation = true;
public ReorderableList EntriesList;
public string EntriesPropertyPath;
}
2025-12-10 17:38:31 +08:00
2026-03-19 13:59:42 +08:00
private readonly List<TableEditorState> _tableStates = new();
2025-12-10 17:38:31 +08:00
2026-03-19 13:59:42 +08:00
private InputGlyphDatabase _database;
private SerializedProperty _tablesProperty;
private SerializedProperty _placeholderSpriteProperty;
private int _selectedTab;
private bool _showAddTable;
private bool _showDatabaseValidation = true;
private string _newTableName = string.Empty;
2026-03-09 20:38:15 +08:00
2026-03-19 13:59:42 +08:00
private bool IsSettingsSelected => _selectedTab >= TableCount;
private int TableCount => _database != null && _database.tables != null
? _database.tables.Count
: (_tablesProperty != null ? _tablesProperty.arraySize : 0);
2025-12-05 20:57:29 +08:00
2026-03-19 13:59:42 +08:00
private void OnEnable()
2025-12-05 19:04:53 +08:00
{
2026-03-19 13:59:42 +08:00
_database = target as InputGlyphDatabase;
_tablesProperty = serializedObject.FindProperty(TablesPropertyName);
_placeholderSpriteProperty = serializedObject.FindProperty(PlaceholderSpritePropertyName);
SyncTableStates();
ClampSelectedTab();
}
2025-12-05 20:57:29 +08:00
2026-03-19 13:59:42 +08:00
public override void OnInspectorGUI()
{
if (_database == null || _tablesProperty == null)
2025-12-05 20:57:29 +08:00
{
2026-03-19 13:59:42 +08:00
DrawDefaultInspector();
2025-12-10 17:38:31 +08:00
return;
2025-12-05 20:57:29 +08:00
}
2025-12-10 17:38:31 +08:00
2025-12-05 19:04:53 +08:00
serializedObject.Update();
2026-03-19 13:59:42 +08:00
SyncTableStates();
ClampSelectedTab();
2025-12-05 19:04:53 +08:00
2026-03-19 13:59:42 +08:00
DrawToolbar();
if (_showAddTable)
2025-12-05 20:57:29 +08:00
{
2026-03-19 13:59:42 +08:00
DrawAddTableBar();
2025-12-05 20:57:29 +08:00
}
2025-12-05 19:04:53 +08:00
2026-03-19 13:59:42 +08:00
DrawMissingDefaultTablesNotice();
EditorGUILayout.Space(6f);
DrawTabs();
EditorGUILayout.Space(8f);
2025-12-05 20:57:29 +08:00
2026-03-19 13:59:42 +08:00
using (new EditorGUILayout.VerticalScope(EditorStyles.helpBox))
2025-12-05 19:04:53 +08:00
{
2026-03-19 13:59:42 +08:00
if (IsSettingsSelected)
{
DrawSettingsPanel();
}
else
{
DrawTablePanel(_selectedTab);
}
2025-12-05 19:04:53 +08:00
}
2026-03-19 13:59:42 +08:00
if (serializedObject.ApplyModifiedProperties())
2025-12-05 19:04:53 +08:00
{
2026-03-19 13:59:42 +08:00
NotifyDatabaseChanged();
2025-12-05 19:04:53 +08:00
}
2026-03-19 13:59:42 +08:00
}
2025-12-05 19:04:53 +08:00
2026-03-19 13:59:42 +08:00
private void DrawToolbar()
{
using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar))
{
if (GUILayout.Button("Save Asset", EditorStyles.toolbarButton, GUILayout.Width(90f)))
{
serializedObject.ApplyModifiedProperties();
NotifyDatabaseChanged(true);
}
if (HasMissingDefaultTables() && GUILayout.Button("Create Standard Tables", EditorStyles.toolbarButton, GUILayout.Width(140f)))
{
ApplyPendingInspectorChanges();
CreateMissingDefaultTables();
}
GUILayout.FlexibleSpace();
GUILayout.Label($"Tables: {TableCount}", EditorStyles.miniLabel);
if (GUILayout.Button(_showAddTable ? "Cancel Add" : "+ Add Table", EditorStyles.toolbarButton, GUILayout.Width(90f)))
{
_showAddTable = !_showAddTable;
_newTableName = string.Empty;
GUI.FocusControl(null);
}
}
}
2025-12-05 19:04:53 +08:00
2026-03-19 13:59:42 +08:00
private void DrawAddTableBar()
{
using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar))
2025-12-10 17:38:31 +08:00
{
2026-03-19 13:59:42 +08:00
GUILayout.Label("Name", GUILayout.Width(40f));
_newTableName = EditorGUILayout.TextField(_newTableName);
using (new EditorGUI.DisabledScope(string.IsNullOrWhiteSpace(_newTableName)))
2025-12-10 17:38:31 +08:00
{
2026-03-19 13:59:42 +08:00
if (GUILayout.Button("Add", EditorStyles.toolbarButton, GUILayout.Width(70f)))
2025-12-10 17:38:31 +08:00
{
2026-03-19 13:59:42 +08:00
string trimmed = _newTableName.Trim();
if (HasTable(trimmed))
2025-12-10 17:38:31 +08:00
{
2026-03-19 13:59:42 +08:00
EditorUtility.DisplayDialog("Duplicate Table", $"A table named '{trimmed}' already exists.", "OK");
2025-12-10 17:38:31 +08:00
}
else
{
2026-03-19 13:59:42 +08:00
ApplyPendingInspectorChanges();
AddTable(trimmed);
_selectedTab = TableCount - 1;
_showAddTable = false;
_newTableName = string.Empty;
GUI.FocusControl(null);
2025-12-10 17:38:31 +08:00
}
}
}
2026-03-19 13:59:42 +08:00
}
}
2026-03-09 20:38:15 +08:00
2026-03-19 13:59:42 +08:00
private void DrawMissingDefaultTablesNotice()
{
List<string> missingTables = GetMissingDefaultTables();
if (missingTables.Count == 0)
{
return;
2025-12-10 17:38:31 +08:00
}
2026-03-19 13:59:42 +08:00
EditorGUILayout.HelpBox(
$"Recommended tables are missing: {string.Join(", ", missingTables)}. Glyph lookup still works, but missing categories may fall back to another table.",
MessageType.Info);
}
2025-12-05 19:04:53 +08:00
2026-03-19 13:59:42 +08:00
private void DrawTabs()
{
using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar))
2025-12-05 19:04:53 +08:00
{
2026-03-19 13:59:42 +08:00
for (int i = 0; i < TableCount; i++)
2025-12-10 17:38:31 +08:00
{
2026-03-19 13:59:42 +08:00
SerializedProperty tableProperty = _tablesProperty.GetArrayElementAtIndex(i);
string tableName = GetTableName(tableProperty, i);
bool selected = !IsSettingsSelected && _selectedTab == i;
2025-12-05 19:04:53 +08:00
2026-03-19 13:59:42 +08:00
if (GUILayout.Toggle(selected, tableName, EditorStyles.toolbarButton, GUILayout.MinWidth(70f)))
2025-12-10 17:38:31 +08:00
{
2026-03-19 13:59:42 +08:00
_selectedTab = i;
}
2025-12-10 17:38:31 +08:00
2026-03-19 13:59:42 +08:00
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();
}
2025-12-10 17:38:31 +08:00
}
}
2026-03-19 13:59:42 +08:00
GUILayout.FlexibleSpace();
bool settingsSelected = IsSettingsSelected;
if (GUILayout.Toggle(settingsSelected, "Settings", EditorStyles.toolbarButton, GUILayout.Width(90f)))
{
_selectedTab = TableCount;
}
2025-12-10 17:38:31 +08:00
}
2026-03-19 13:59:42 +08:00
}
2025-12-05 20:57:29 +08:00
2026-03-19 13:59:42 +08:00
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");
2025-12-05 20:57:29 +08:00
2026-03-19 13:59:42 +08:00
EditorGUILayout.Space(8f);
DrawDatabaseValidationPanel();
}
2025-12-05 19:04:53 +08:00
2026-03-19 13:59:42 +08:00
private void DrawDatabaseValidationPanel()
{
List<string> issues = CollectDatabaseValidationIssues();
string title = issues.Count == 0 ? "Database Validation" : $"Database Validation ({issues.Count})";
_showDatabaseValidation = EditorGUILayout.BeginFoldoutHeaderGroup(_showDatabaseValidation, title);
if (_showDatabaseValidation)
2025-12-10 17:38:31 +08:00
{
2026-03-19 13:59:42 +08:00
if (issues.Count == 0)
2025-12-10 17:38:31 +08:00
{
2026-03-19 13:59:42 +08:00
EditorGUILayout.HelpBox("No database-level issues found.", MessageType.Info);
2025-12-10 17:38:31 +08:00
}
else
{
2026-03-19 13:59:42 +08:00
DrawValidationList(issues, MessageType.Warning);
2025-12-10 17:38:31 +08:00
}
}
2025-12-05 19:04:53 +08:00
2026-03-19 13:59:42 +08:00
EditorGUILayout.EndFoldoutHeaderGroup();
}
2025-12-05 19:04:53 +08:00
2026-03-19 13:59:42 +08:00
private void DrawTablePanel(int tableIndex)
{
if (tableIndex < 0 || tableIndex >= TableCount)
{
EditorGUILayout.HelpBox("Select a valid table.", MessageType.Info);
return;
}
2026-03-09 20:38:15 +08:00
2026-03-19 13:59:42 +08:00
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);
2025-12-10 17:38:31 +08:00
2026-03-19 13:59:42 +08:00
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);
}
2026-03-19 13:59:42 +08:00
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);
2026-03-09 20:38:15 +08:00
2026-03-19 13:59:42 +08:00
if (GUILayout.Button("Merge Sprite Sheet"))
{
ApplyPendingInspectorChanges();
MergeSpriteSheet(tableIndex);
}
2026-03-09 20:38:15 +08:00
2026-03-19 13:59:42 +08:00
using (new EditorGUI.DisabledScope(_database.tables[tableIndex].entries == null || _database.tables[tableIndex].entries.Count == 0))
{
if (GUILayout.Button("Clear Entries"))
2025-12-17 20:03:29 +08:00
{
2026-03-19 13:59:42 +08:00
if (EditorUtility.DisplayDialog("Clear Entries", $"Remove all entries from '{nameProperty.stringValue}'?", "Clear", "Cancel"))
2025-12-17 20:03:29 +08:00
{
2026-03-19 13:59:42 +08:00
ApplyPendingInspectorChanges();
ClearEntries(tableIndex);
2025-12-17 20:03:29 +08:00
}
}
2026-03-19 13:59:42 +08:00
}
}
2026-03-09 20:38:15 +08:00
2026-03-19 13:59:42 +08:00
EditorGUILayout.Space(6f);
DrawQuickAddEntry(tableIndex, state);
2025-12-17 20:03:29 +08:00
2026-03-19 13:59:42 +08:00
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);
2025-12-17 20:03:29 +08:00
2026-03-19 13:59:42 +08:00
using (new EditorGUI.DisabledScope(state.PendingSprite == null))
{
if (GUILayout.Button("Add Entry", GUILayout.Width(90f)))
2025-12-17 20:03:29 +08:00
{
2026-03-19 13:59:42 +08:00
ApplyPendingInspectorChanges();
AddEntry(tableIndex, state.PendingSprite);
state.PendingSprite = null;
2025-12-10 17:38:31 +08:00
}
2026-03-19 13:59:42 +08:00
}
}
}
2026-03-09 20:38:15 +08:00
2026-03-19 13:59:42 +08:00
private void DrawTableValidationPanel(int tableIndex, TableEditorState state)
{
List<string> issues = CollectTableValidationIssues(tableIndex);
string title = issues.Count == 0 ? "Validation" : $"Validation ({issues.Count})";
state.ShowValidation = EditorGUILayout.BeginFoldoutHeaderGroup(state.ShowValidation, title);
if (state.ShowValidation)
{
if (issues.Count == 0)
{
EditorGUILayout.HelpBox("No table-level issues found.", MessageType.Info);
}
else
{
DrawValidationList(issues, MessageType.Warning);
}
}
2025-12-10 17:38:31 +08:00
2026-03-19 13:59:42 +08:00
EditorGUILayout.EndFoldoutHeaderGroup();
}
2025-12-05 20:57:29 +08:00
2026-03-19 13:59:42 +08:00
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);
}
2025-12-10 17:38:31 +08:00
2026-03-19 13:59:42 +08:00
if (issues.Count > visibleCount)
{
EditorGUILayout.HelpBox($"{issues.Count - visibleCount} more issues are hidden to keep the inspector readable.", MessageType.None);
}
}
2025-12-10 17:38:31 +08:00
2026-03-19 13:59:42 +08:00
private void DrawEntriesList(int tableIndex, SerializedProperty entriesProperty)
{
EditorGUILayout.LabelField("Entries", EditorStyles.boldLabel);
ReorderableList list = GetEntriesList(tableIndex, entriesProperty);
list.DoLayoutList();
}
private ReorderableList GetEntriesList(int tableIndex, SerializedProperty entriesProperty)
{
TableEditorState state = _tableStates[tableIndex];
if (state.EntriesList != null && state.EntriesPropertyPath == entriesProperty.propertyPath)
{
return state.EntriesList;
}
2026-03-09 20:38:15 +08:00
2026-03-19 13:59:42 +08:00
ReorderableList list = new ReorderableList(serializedObject, entriesProperty, true, true, false, true);
state.EntriesList = list;
state.EntriesPropertyPath = entriesProperty.propertyPath;
2025-12-10 17:38:31 +08:00
2026-03-19 13:59:42 +08:00
list.drawHeaderCallback = rect =>
{
EditorGUI.LabelField(rect, $"Entries ({entriesProperty.arraySize}) - drag to reorder");
};
2025-12-10 17:38:31 +08:00
2026-03-19 13:59:42 +08:00
list.elementHeightCallback = index =>
{
if (index < 0 || index >= entriesProperty.arraySize)
{
return EditorGUIUtility.singleLineHeight + 8f;
}
2026-03-09 20:38:15 +08:00
2026-03-19 13:59:42 +08:00
return GetEntryElementHeight(entriesProperty.GetArrayElementAtIndex(index));
};
2026-03-09 20:38:15 +08:00
2026-03-19 13:59:42 +08:00
list.drawElementCallback = (rect, index, active, focused) =>
{
if (index < 0 || index >= entriesProperty.arraySize)
{
return;
}
2025-12-10 17:38:31 +08:00
2026-03-19 13:59:42 +08:00
DrawEntryElement(rect, entriesProperty.GetArrayElementAtIndex(index));
};
2025-12-10 17:38:31 +08:00
2026-03-19 13:59:42 +08:00
list.onReorderCallback = _ =>
{
ApplyPendingInspectorChanges();
};
2025-12-10 17:38:31 +08:00
2026-03-19 13:59:42 +08:00
list.onRemoveCallback = currentList =>
{
if (currentList.index < 0 || currentList.index >= entriesProperty.arraySize)
{
return;
}
2025-12-05 19:04:53 +08:00
2026-03-19 13:59:42 +08:00
if (!EditorUtility.DisplayDialog("Remove Entry", "Remove the selected entry from the table?", "Remove", "Cancel"))
{
return;
2025-12-05 20:57:29 +08:00
}
2025-12-05 19:04:53 +08:00
2026-03-19 13:59:42 +08:00
Undo.RecordObject(_database, "Remove glyph entry");
entriesProperty.DeleteArrayElementAtIndex(currentList.index);
serializedObject.ApplyModifiedProperties();
InvalidateEntriesList(tableIndex);
NotifyDatabaseChanged();
};
2025-12-05 19:04:53 +08:00
2026-03-19 13:59:42 +08:00
return list;
}
2025-12-05 19:04:53 +08:00
2026-03-19 13:59:42 +08:00
private float GetEntryElementHeight(SerializedProperty entryProperty)
{
SerializedProperty spriteProperty = entryProperty.FindPropertyRelative(EntrySpritePropertyName);
SerializedProperty actionProperty = entryProperty.FindPropertyRelative(EntryActionPropertyName);
float spriteHeight = EditorGUI.GetPropertyHeight(spriteProperty, true);
float actionHeight = EditorGUI.GetPropertyHeight(actionProperty, true);
float fieldHeight = spriteHeight + actionHeight + 10f;
return Mathf.Max(ListPreviewSize + 10f, fieldHeight + 8f);
2025-12-05 19:04:53 +08:00
}
2026-03-19 13:59:42 +08:00
private void DrawEntryElement(Rect rect, SerializedProperty entryProperty)
2025-12-05 19:04:53 +08:00
{
2026-03-19 13:59:42 +08:00
SerializedProperty spriteProperty = entryProperty.FindPropertyRelative(EntrySpritePropertyName);
SerializedProperty actionProperty = entryProperty.FindPropertyRelative(EntryActionPropertyName);
Sprite sprite = spriteProperty.objectReferenceValue as Sprite;
rect.y += 4f;
rect.height -= 8f;
Rect previewRect = new Rect(rect.x, rect.y, ListPreviewSize, ListPreviewSize);
Rect fieldsRect = new Rect(rect.x + ListPreviewSize + 8f, rect.y, rect.width - ListPreviewSize - 44f, rect.height);
Rect pingRect = new Rect(rect.xMax - 30f, rect.y, 30f, EditorGUIUtility.singleLineHeight);
DrawSpritePreview(previewRect, sprite);
float currentY = fieldsRect.y;
float spriteHeight = EditorGUI.GetPropertyHeight(spriteProperty, true);
Rect spriteRect = new Rect(fieldsRect.x, currentY, fieldsRect.width, spriteHeight);
EditorGUI.PropertyField(spriteRect, spriteProperty, new GUIContent("Sprite"), true);
currentY += spriteHeight + 4f;
float actionHeight = EditorGUI.GetPropertyHeight(actionProperty, true);
Rect actionRect = new Rect(fieldsRect.x, currentY, fieldsRect.width, actionHeight);
EditorGUI.PropertyField(actionRect, actionProperty, new GUIContent("Action"), true);
using (new EditorGUI.DisabledScope(sprite == null))
2025-12-05 19:04:53 +08:00
{
2026-03-19 13:59:42 +08:00
if (GUI.Button(pingRect, "Ping"))
{
EditorGUIUtility.PingObject(sprite);
}
2025-12-05 20:57:29 +08:00
}
2026-03-19 13:59:42 +08:00
}
2025-12-05 20:57:29 +08:00
2026-03-19 13:59:42 +08:00
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);
}
2025-12-05 20:57:29 +08:00
2026-03-19 13:59:42 +08:00
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);
}
2025-12-05 20:57:29 +08:00
}
2026-03-19 13:59:42 +08:00
private void DrawSpritePreview(Rect rect, Sprite sprite)
2025-12-05 20:57:29 +08:00
{
2026-03-19 13:59:42 +08:00
if (sprite == null)
{
EditorGUI.HelpBox(rect, "None", MessageType.None);
return;
}
2025-12-05 20:57:29 +08:00
2026-03-19 13:59:42 +08:00
Texture2D preview = AssetPreview.GetAssetPreview(sprite);
if (preview == null)
{
preview = AssetPreview.GetMiniThumbnail(sprite);
}
2025-12-10 17:38:31 +08:00
2026-03-19 13:59:42 +08:00
if (preview != null)
{
GUI.DrawTexture(rect, preview, ScaleMode.ScaleToFit);
}
else
{
EditorGUI.ObjectField(rect, sprite, typeof(Sprite), false);
}
2025-12-10 17:38:31 +08:00
}
2026-03-19 13:59:42 +08:00
private List<string> CollectDatabaseValidationIssues()
2025-12-10 17:38:31 +08:00
{
2026-03-19 13:59:42 +08:00
List<string> issues = new();
if (_database.tables == null || _database.tables.Count == 0)
{
issues.Add("The database has no tables. Runtime glyph lookup will always fall back to the placeholder sprite.");
return issues;
}
List<string> missingTables = GetMissingDefaultTables();
if (missingTables.Count > 0)
{
issues.Add($"Recommended tables are missing: {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($"Table {i + 1} has an empty device name.");
continue;
}
if (!seenNames.Add(tableName))
{
duplicateNames.Add(tableName);
}
}
foreach (string duplicateName in duplicateNames)
{
issues.Add($"Duplicate table name '{duplicateName}' detected.");
}
return issues;
2025-12-05 19:04:53 +08:00
}
2026-03-19 13:59:42 +08:00
private List<string> CollectTableValidationIssues(int tableIndex)
2025-12-05 19:04:53 +08:00
{
2026-03-19 13:59:42 +08:00
List<string> issues = new();
if (!IsValidTableIndex(tableIndex))
{
issues.Add("The selected table is invalid.");
return issues;
}
2025-12-05 20:57:29 +08:00
2026-03-19 13:59:42 +08:00
DeviceGlyphTable table = _database.tables[tableIndex];
if (table.entries == null || table.entries.Count == 0)
2025-12-17 20:03:29 +08:00
{
2026-03-19 13:59:42 +08:00
issues.Add("This table has no entries.");
return issues;
}
2025-12-17 20:03:29 +08:00
2026-03-19 13:59:42 +08:00
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);
2025-12-05 19:04:53 +08:00
2026-03-19 13:59:42 +08:00
for (int i = 0; i < table.entries.Count; i++)
{
GlyphEntry entry = table.entries[i];
if (entry == null)
2025-12-05 19:04:53 +08:00
{
2026-03-19 13:59:42 +08:00
issues.Add($"Entry {i + 1} is null.");
continue;
}
if (entry.Sprite == null)
{
missingSpriteCount++;
}
else if (!seenSprites.Add(entry.Sprite.name))
{
duplicateSprites.Add(entry.Sprite.name);
}
if (entry.action == null)
{
missingActionCount++;
continue;
}
string entryLabel = entry.Sprite != null ? entry.Sprite.name : $"Entry {i + 1}";
for (int bindingIndex = 0; bindingIndex < entry.action.bindings.Count; bindingIndex++)
{
InputBinding binding = entry.action.bindings[bindingIndex];
RegisterBindingOwner(bindingOwners, binding.path, entryLabel);
RegisterBindingOwner(bindingOwners, binding.effectivePath, entryLabel);
2025-12-05 19:04:53 +08:00
}
2025-12-17 20:03:29 +08:00
}
2026-03-19 13:59:42 +08:00
if (missingSpriteCount > 0)
{
issues.Add($"{missingSpriteCount} entr{(missingSpriteCount == 1 ? "y has" : "ies have")} no sprite assigned.");
}
2025-12-17 20:03:29 +08:00
2026-03-19 13:59:42 +08:00
if (missingActionCount > 0)
{
issues.Add($"{missingActionCount} entr{(missingActionCount == 1 ? "y has" : "ies have")} no action assigned. Those entries will not participate in runtime path lookup.");
}
foreach (string spriteName in duplicateSprites)
2025-12-17 20:03:29 +08:00
{
2026-03-19 13:59:42 +08:00
issues.Add($"Duplicate sprite name '{spriteName}' found in this table.");
2025-12-17 20:03:29 +08:00
}
2026-03-19 13:59:42 +08:00
foreach (KeyValuePair<string, List<string>> pair in bindingOwners)
{
if (pair.Value.Count <= 1)
{
continue;
}
issues.Add($"Binding '{pair.Key}' is mapped by multiple entries: {string.Join(", ", pair.Value)}. Runtime lookup keeps the first match.");
}
return issues;
2025-12-17 20:03:29 +08:00
}
2026-03-19 13:59:42 +08:00
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);
}
2026-03-09 20:38:15 +08:00
2026-03-19 13:59:42 +08:00
if (!owners.Contains(ownerLabel))
{
owners.Add(ownerLabel);
}
}
private void AddEntry(int tableIndex, Sprite sprite)
2025-12-17 20:03:29 +08:00
{
2026-03-19 13:59:42 +08:00
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();
InvalidateEntriesList(tableIndex);
NotifyDatabaseChanged();
}
2025-12-17 20:03:29 +08:00
2026-03-19 13:59:42 +08:00
private void ClearEntries(int tableIndex)
{
if (!IsValidTableIndex(tableIndex))
2025-12-17 20:03:29 +08:00
{
return;
}
2026-03-19 13:59:42 +08:00
Undo.RecordObject(_database, "Clear glyph entries");
DeviceGlyphTable table = _database.tables[tableIndex];
table.entries ??= new List<GlyphEntry>();
table.entries.Clear();
serializedObject.Update();
InvalidateEntriesList(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();
InvalidateAllEntriesLists();
NotifyDatabaseChanged();
}
2025-12-17 20:03:29 +08:00
2026-03-19 13:59:42 +08:00
private void RemoveTable(int tableIndex)
{
if (!IsValidTableIndex(tableIndex))
2025-12-17 20:03:29 +08:00
{
return;
}
2026-03-19 13:59:42 +08:00
Undo.RecordObject(_database, "Remove glyph table");
_database.tables.RemoveAt(tableIndex);
SyncTableStates();
ClampSelectedTab();
serializedObject.Update();
InvalidateAllEntriesLists();
NotifyDatabaseChanged();
}
private void CreateMissingDefaultTables()
{
List<string> missingTables = GetMissingDefaultTables();
if (missingTables.Count == 0)
2026-03-09 20:38:15 +08:00
{
return;
}
2025-12-17 20:03:29 +08:00
2026-03-19 13:59:42 +08:00
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();
InvalidateAllEntriesLists();
NotifyDatabaseChanged();
}
private void MergeSpriteSheet(int tableIndex)
{
if (!IsValidTableIndex(tableIndex))
2025-12-17 20:03:29 +08:00
{
return;
}
2026-03-19 13:59:42 +08:00
DeviceGlyphTable table = _database.tables[tableIndex];
if (table.spriteSheetTexture == null)
2025-12-17 20:03:29 +08:00
{
2026-03-19 13:59:42 +08:00
EditorUtility.DisplayDialog("Missing Sprite Sheet", "Assign a sprite sheet texture first.", "OK");
2025-12-17 20:03:29 +08:00
return;
}
2026-03-19 13:59:42 +08:00
string path = AssetDatabase.GetAssetPath(table.spriteSheetTexture);
if (string.IsNullOrEmpty(path))
2025-12-17 20:03:29 +08:00
{
2026-03-19 13:59:42 +08:00
Debug.LogWarning("[InputGlyphDatabase] Could not resolve the sprite sheet asset path.");
return;
2025-12-17 20:03:29 +08:00
}
2026-03-19 13:59:42 +08:00
UnityEngine.Object[] assets = AssetDatabase.LoadAllAssetsAtPath(path);
if (assets == null || assets.Length == 0)
2025-12-17 20:03:29 +08:00
{
2026-03-19 13:59:42 +08:00
Debug.LogWarning($"[InputGlyphDatabase] No sub-assets found at '{path}'.");
2026-03-09 20:38:15 +08:00
return;
2025-12-17 20:03:29 +08:00
}
2026-03-19 13:59:42 +08:00
List<Sprite> sprites = new();
for (int i = 0; i < assets.Length; i++)
2025-12-17 20:03:29 +08:00
{
2026-03-19 13:59:42 +08:00
if (assets[i] is Sprite sprite)
2026-03-09 20:38:15 +08:00
{
2026-03-19 13:59:42 +08:00
sprites.Add(sprite);
2026-03-09 20:38:15 +08:00
}
2025-12-17 20:03:29 +08:00
}
2025-12-05 19:04:53 +08:00
2026-03-19 13:59:42 +08:00
if (sprites.Count == 0)
2025-12-05 19:04:53 +08:00
{
2026-03-19 13:59:42 +08:00
EditorUtility.DisplayDialog("No Sprites Found", "The selected texture does not contain any sprite sub-assets.", "OK");
return;
2025-12-05 19:04:53 +08:00
}
2026-03-19 13:59:42 +08:00
Undo.RecordObject(_database, "Merge glyph sprite sheet");
table.entries ??= new List<GlyphEntry>();
2026-03-09 20:38:15 +08:00
2026-03-19 13:59:42 +08:00
Dictionary<string, GlyphEntry> entriesByName = new(StringComparer.OrdinalIgnoreCase);
for (int i = 0; i < table.entries.Count; i++)
2025-12-05 19:04:53 +08:00
{
2026-03-19 13:59:42 +08:00
GlyphEntry entry = table.entries[i];
if (entry?.Sprite == null)
{
continue;
}
if (!entriesByName.ContainsKey(entry.Sprite.name))
{
entriesByName.Add(entry.Sprite.name, entry);
}
}
2026-03-09 20:38:15 +08:00
2026-03-19 13:59:42 +08:00
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))
2025-12-05 19:04:53 +08:00
{
2026-03-19 13:59:42 +08:00
if (entry.Sprite != sprite)
2025-12-05 19:04:53 +08:00
{
2026-03-19 13:59:42 +08:00
entry.Sprite = sprite;
replaced++;
2025-12-05 19:04:53 +08:00
}
2026-03-09 20:38:15 +08:00
}
else
{
2026-03-19 13:59:42 +08:00
GlyphEntry newEntry = new GlyphEntry { Sprite = sprite, action = null };
table.entries.Add(newEntry);
entriesByName.Add(sprite.name, newEntry);
2026-03-09 20:38:15 +08:00
added++;
}
2026-03-19 13:59:42 +08:00
}
serializedObject.Update();
InvalidateEntriesList(tableIndex);
NotifyDatabaseChanged();
Debug.Log($"[InputGlyphDatabase] Merged sprite sheet '{table.spriteSheetTexture.name}' into '{table.deviceName}'. sprites={sprites.Count}, replaced={replaced}, added={added}, total={table.entries.Count}");
}
private void ApplyPendingInspectorChanges()
{
if (serializedObject.ApplyModifiedProperties())
{
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());
}
}
2026-03-09 20:38:15 +08:00
2026-03-19 13:59:42 +08:00
private void ClampSelectedTab()
{
int maxIndex = Mathf.Max(0, TableCount);
_selectedTab = Mathf.Clamp(_selectedTab, 0, maxIndex);
}
private void InvalidateEntriesList(int tableIndex)
{
if (tableIndex < 0 || tableIndex >= _tableStates.Count)
{
return;
}
_tableStates[tableIndex].EntriesList = null;
_tableStates[tableIndex].EntriesPropertyPath = null;
}
private void InvalidateAllEntriesLists()
{
for (int i = 0; i < _tableStates.Count; i++)
{
_tableStates[i].EntriesList = null;
_tableStates[i].EntriesPropertyPath = null;
}
}
private bool IsValidTableIndex(int tableIndex)
{
return _database != null
&& _database.tables != null
&& tableIndex >= 0
&& tableIndex < _database.tables.Count;
}
private string GetTableName(SerializedProperty tableProperty, int fallbackIndex)
{
SerializedProperty nameProperty = tableProperty.FindPropertyRelative(DeviceNamePropertyName);
return string.IsNullOrWhiteSpace(nameProperty.stringValue) ? $"Table {fallbackIndex + 1}" : nameProperty.stringValue;
}
private bool HasTable(string tableName)
{
if (string.IsNullOrWhiteSpace(tableName) || _database.tables == null)
{
return false;
}
for (int i = 0; i < _database.tables.Count; i++)
{
if (string.Equals(_database.tables[i].deviceName, tableName, StringComparison.OrdinalIgnoreCase))
2026-03-09 20:38:15 +08:00
{
2026-03-19 13:59:42 +08:00
return true;
2026-03-09 20:38:15 +08:00
}
2026-03-19 13:59:42 +08:00
}
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))
2026-03-09 20:38:15 +08:00
{
2026-03-19 13:59:42 +08:00
return true;
2025-12-05 19:04:53 +08:00
}
}
2026-03-19 13:59:42 +08:00
return false;
}
2026-03-09 20:38:15 +08:00
2026-03-19 13:59:42 +08:00
private bool HasMissingDefaultTables()
{
return GetMissingDefaultTables().Count > 0;
2025-12-05 19:04:53 +08:00
}
2025-12-17 20:03:29 +08:00
2026-03-19 13:59:42 +08:00
private List<string> GetMissingDefaultTables()
2025-12-17 20:03:29 +08:00
{
2026-03-19 13:59:42 +08:00
List<string> missingTables = new();
for (int i = 0; i < DefaultTableNames.Length; i++)
2025-12-17 20:03:29 +08:00
{
2026-03-19 13:59:42 +08:00
if (!HasTable(DefaultTableNames[i]))
{
missingTables.Add(DefaultTableNames[i]);
}
2025-12-17 20:03:29 +08:00
}
2026-03-09 20:38:15 +08:00
2026-03-19 13:59:42 +08:00
return missingTables;
2025-12-17 20:03:29 +08:00
}
2025-12-05 19:04:53 +08:00
}