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 _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 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 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 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 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 CollectDatabaseValidationIssues() { List 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 missingTables = GetMissingDefaultTables(); if (missingTables.Count > 0) { issues.Add($"Recommended tables are missing: {string.Join(", ", missingTables)}."); } HashSet seenNames = new(StringComparer.OrdinalIgnoreCase); HashSet 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 CollectTableValidationIssues(int tableIndex) { List 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 seenSprites = new(StringComparer.OrdinalIgnoreCase); HashSet duplicateSprites = new(StringComparer.OrdinalIgnoreCase); Dictionary> 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> 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> bindingOwners, string controlPath, string ownerLabel) { string normalizedPath = InputGlyphDatabase.EditorNormalizeControlPath(controlPath); if (string.IsNullOrEmpty(normalizedPath)) { return; } if (!bindingOwners.TryGetValue(normalizedPath, out List owners)) { owners = new List(); 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(); 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(); table.entries.Clear(); serializedObject.Update(); InvalidateEntriesList(tableIndex); NotifyDatabaseChanged(); } private void AddTable(string tableName) { Undo.RecordObject(_database, "Add glyph table"); _database.tables ??= new List(); _database.tables.Add(new DeviceGlyphTable { deviceName = tableName, spriteSheetTexture = null, platformIcons = null, entries = new List() }); 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 missingTables = GetMissingDefaultTables(); if (missingTables.Count == 0) { return; } Undo.RecordObject(_database, "Create standard glyph tables"); _database.tables ??= new List(); for (int i = 0; i < missingTables.Count; i++) { _database.tables.Add(new DeviceGlyphTable { deviceName = missingTables[i], spriteSheetTexture = null, platformIcons = null, entries = new List() }); } 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 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(); Dictionary 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 GetMissingDefaultTables() { List missingTables = new(); for (int i = 0; i < DefaultTableNames.Length; i++) { if (!HasTable(DefaultTableNames[i])) { missingTables.Add(DefaultTableNames[i]); } } return missingTables; } }