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 } ;
2026-03-22 20:21:45 +08:00
private static readonly string [ ] EntriesPerPageLabels = { "10|页" , "15|页" , "20|页" , "25|页" } ;
2026-03-20 16:50:30 +08:00
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 ;
}
}