962 lines
32 KiB
C#
962 lines
32 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using UnityEditor;
|
|
using UnityEditorInternal;
|
|
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 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;
|
|
}
|
|
|
|
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())
|
|
{
|
|
NotifyDatabaseChanged();
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
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 ? "Database Validation" : $"Database Validation ({issues.Count})";
|
|
_showDatabaseValidation = EditorGUILayout.BeginFoldoutHeaderGroup(_showDatabaseValidation, title);
|
|
if (_showDatabaseValidation)
|
|
{
|
|
if (issues.Count == 0)
|
|
{
|
|
EditorGUILayout.HelpBox("No database-level issues found.", 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 ? "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);
|
|
}
|
|
}
|
|
|
|
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} more issues are hidden to keep the inspector readable.", MessageType.None);
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
ReorderableList list = new ReorderableList(serializedObject, entriesProperty, true, true, false, true);
|
|
state.EntriesList = list;
|
|
state.EntriesPropertyPath = entriesProperty.propertyPath;
|
|
|
|
list.drawHeaderCallback = rect =>
|
|
{
|
|
EditorGUI.LabelField(rect, $"Entries ({entriesProperty.arraySize}) - drag to reorder");
|
|
};
|
|
|
|
list.elementHeightCallback = index =>
|
|
{
|
|
if (index < 0 || index >= entriesProperty.arraySize)
|
|
{
|
|
return EditorGUIUtility.singleLineHeight + 8f;
|
|
}
|
|
|
|
return GetEntryElementHeight(entriesProperty.GetArrayElementAtIndex(index));
|
|
};
|
|
|
|
list.drawElementCallback = (rect, index, active, focused) =>
|
|
{
|
|
if (index < 0 || index >= entriesProperty.arraySize)
|
|
{
|
|
return;
|
|
}
|
|
|
|
DrawEntryElement(rect, entriesProperty.GetArrayElementAtIndex(index));
|
|
};
|
|
|
|
list.onReorderCallback = _ =>
|
|
{
|
|
ApplyPendingInspectorChanges();
|
|
};
|
|
|
|
list.onRemoveCallback = currentList =>
|
|
{
|
|
if (currentList.index < 0 || currentList.index >= entriesProperty.arraySize)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!EditorUtility.DisplayDialog("Remove Entry", "Remove the selected entry from the table?", "Remove", "Cancel"))
|
|
{
|
|
return;
|
|
}
|
|
|
|
Undo.RecordObject(_database, "Remove glyph entry");
|
|
entriesProperty.DeleteArrayElementAtIndex(currentList.index);
|
|
serializedObject.ApplyModifiedProperties();
|
|
InvalidateEntriesList(tableIndex);
|
|
NotifyDatabaseChanged();
|
|
};
|
|
|
|
return list;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
private void DrawEntryElement(Rect rect, SerializedProperty entryProperty)
|
|
{
|
|
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))
|
|
{
|
|
if (GUI.Button(pingRect, "Ping"))
|
|
{
|
|
EditorGUIUtility.PingObject(sprite);
|
|
}
|
|
}
|
|
}
|
|
|
|
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("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;
|
|
}
|
|
|
|
private List<string> CollectTableValidationIssues(int tableIndex)
|
|
{
|
|
List<string> issues = new();
|
|
if (!IsValidTableIndex(tableIndex))
|
|
{
|
|
issues.Add("The selected table is invalid.");
|
|
return issues;
|
|
}
|
|
|
|
DeviceGlyphTable table = _database.tables[tableIndex];
|
|
if (table.entries == null || table.entries.Count == 0)
|
|
{
|
|
issues.Add("This table has no entries.");
|
|
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($"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);
|
|
}
|
|
}
|
|
|
|
if (missingSpriteCount > 0)
|
|
{
|
|
issues.Add($"{missingSpriteCount} entr{(missingSpriteCount == 1 ? "y has" : "ies have")} no sprite assigned.");
|
|
}
|
|
|
|
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)
|
|
{
|
|
issues.Add($"Duplicate sprite name '{spriteName}' found in this table.");
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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();
|
|
InvalidateEntriesList(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();
|
|
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();
|
|
}
|
|
|
|
private void RemoveTable(int tableIndex)
|
|
{
|
|
if (!IsValidTableIndex(tableIndex))
|
|
{
|
|
return;
|
|
}
|
|
|
|
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)
|
|
{
|
|
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();
|
|
InvalidateAllEntriesLists();
|
|
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();
|
|
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());
|
|
}
|
|
}
|
|
|
|
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))
|
|
{
|
|
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;
|
|
}
|
|
}
|