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 ) ;
}
2025-12-09 20:31:44 +08:00
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
}