1190 lines
40 KiB
C#
1190 lines
40 KiB
C#
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;
|
||
}
|
||
}
|