// ReSharper disable AccessToModifiedClosure // ReSharper disable AccessToDisposedClosure namespace WallstopStudios.DataVisualizer.Editor { #if UNITY_EDITOR using System; using System.Collections.Generic; using System.ComponentModel; using System.IO; using System.Linq; using System.Reflection; using System.Text; using System.Text.RegularExpressions; using Data; using Extensions; using Search; using Sirenix.OdinInspector; using Sirenix.OdinInspector.Editor; using Styles; using UI; using UnityEditor; using UnityEditor.UIElements; using UnityEngine; using UnityEngine.UIElements; using Utilities; using Helper; public sealed class DataVisualizer : EditorWindow { private const string PackageId = "com.wallstop-studios.data-visualizer"; private const string PrefsPrefix = "WallstopStudios.Editor.DataVisualizer."; private const string PrefsSplitterOuterKey = PrefsPrefix + "SplitterOuterFixedPaneWidth"; private const string PrefsSplitterInnerKey = PrefsPrefix + "SplitterInnerFixedPaneWidth"; private const string PrefsInitialSizeAppliedKey = PrefsPrefix + "InitialSizeApplied"; private const string SettingsDefaultPath = "Assets/DataVisualizerSettings.asset"; private const string UserStateFileName = "DataVisualizerUserState.json"; private const string NamespaceItemClass = "namespace-item"; private const string NamespaceGroupHeaderClass = "namespace-group-header"; private const string NamespaceIndicatorClass = "namespace-indicator"; private const string ObjectItemClass = "object-item"; private const string ObjectItemContentClass = "object-item-content"; private const string ObjectItemActionsClass = "object-item-actions"; private const string PopoverListItemClassName = "type-selection-list-item"; private const string PopoverListItemDisabledClassName = "type-selection-list-item--disabled"; private const string PopoverListNamespaceClassName = "type-selection-list-namespace"; private const string PopoverNamespaceHeaderClassName = "popover-namespace-header"; private const string PopoverNamespaceIndicatorClassName = "popover-namespace-indicator"; private const string SearchResultItemClass = "search-result-item"; private const string SearchResultHighlightClass = "search-result-item--highlighted"; private const string PopoverHighlightClass = "popover-item--highlighted"; private const string SearchPlaceholder = "Search..."; private const int MaxSearchResults = 25; private const float DragDistanceThreshold = 5f; private const float DragUpdateThrottleTime = 0.05f; private const float DefaultOuterSplitWidth = 200f; private const float DefaultInnerSplitWidth = 250f; private enum DragType { None = 0, Object = 1, Namespace = 2, Type = 3, } private enum FocusArea { None = 0, TypeList = 1, AddTypePopover = 2, SearchResultsPopover = 3, } private static readonly StringBuilder CachedStringBuilder = new(); internal DataVisualizerUserState UserState { get { #pragma warning disable CS0618 // Type or member is obsolete if (_userState == null) { LoadUserStateFromFile(); } return _userState; #pragma warning restore CS0618 // Type or member is obsolete } } internal DataVisualizerSettings Settings { get { #pragma warning disable CS0618 // Type or member is obsolete if (_settings == null) { _settings = LoadOrCreateSettings(); } return _settings; #pragma warning restore CS0618 // Type or member is obsolete } } private readonly Dictionary> _scriptableObjectTypes = new( StringComparer.Ordinal ); private readonly Dictionary _namespaceOrder = new(StringComparer.Ordinal); private readonly Dictionary _objectVisualElementMap = new(); private readonly List _selectedObjects = new(); private readonly List _allManagedSOsCache = new(); private readonly List _currentSearchResultItems = new(); private readonly List _currentTypePopoverItems = new(); private readonly NamespaceController _namespaceController; private ScriptableObject _selectedObject; private VisualElement _selectedElement; private VisualElement _selectedNamespaceElement; private VisualElement _namespaceListContainer; private VisualElement _objectListContainer; private VisualElement _inspectorContainer; private ScrollView _objectScrollView; private ScrollView _inspectorScrollView; private TwoPaneSplitView _outerSplitView; private TwoPaneSplitView _innerSplitView; private VisualElement _namespaceColumnElement; private VisualElement _objectColumnElement; private VisualElement _settingsPopover; private VisualElement _renamePopover; private VisualElement _confirmDeletePopover; private VisualElement _confirmActionPopover; private VisualElement _typeAddPopover; private VisualElement _activePopover; private VisualElement _confirmNamespaceAddPopover; private VisualElement _activeNestedPopover; private object _popoverContext; private TextField _searchField; private VisualElement _searchPopover; private bool _isSearchCachePopulated; private string _lastSearchString; private Button _addTypeButton; private Button _settingsButton; private TextField _typeSearchField; private VisualElement _typePopoverListContainer; private float _lastSavedOuterWidth = -1f; private float _lastSavedInnerWidth = -1f; private IVisualElementScheduledItem _saveWidthsTask; private int _searchHighlightIndex = -1; private int _typePopoverHighlightIndex = -1; private string _lastTypeAddSearchTerm; private FocusArea _lastActiveFocusArea = FocusArea.None; private DragType _activeDragType = DragType.None; private object _draggedData; private VisualElement _inPlaceGhost; private int _lastGhostInsertIndex = -1; private VisualElement _lastGhostParent; private VisualElement _draggedElement; private VisualElement _dragGhost; private Vector2 _dragStartPosition; internal bool _isDragging; private float _lastDragUpdateTime; private SerializedObject _currentInspectorScriptableObject; private string _userStateFilePath; [Obsolete("Use UserState instead.")] private DataVisualizerUserState _userState; private bool _userStateDirty; [Obsolete("User Settings instead.")] private DataVisualizerSettings _settings; private List _relevantScriptableObjectTypes; private float? _lastAddTypeClicked; private float? _lastSettingsClicked; private float? _lastEnterPressed; private Label _dataFolderPathDisplay; #if ODIN_INSPECTOR private PropertyTree _odinPropertyTree; private IMGUIContainer _odinInspectorContainer; private IVisualElementScheduledItem _odinRepaintSchedule; #endif public DataVisualizer() { _namespaceController = new NamespaceController(_scriptableObjectTypes, _namespaceOrder); } [MenuItem("Tools/Data Visualizer")] public static void ShowWindow() { DataVisualizer window = GetWindow("Data Visualizer"); window.titleContent = new GUIContent("Data Visualizer"); bool initialSizeApplied = EditorPrefs.GetBool(PrefsInitialSizeAppliedKey, false); if (initialSizeApplied) { return; } float width = Mathf.Max(800, window.position.width); float height = Mathf.Max(400, window.position.height); Rect monitorArea = MonitorUtility.GetPrimaryMonitorRect(); float centerX = (monitorArea.width - width) / 2f; float centerY = (monitorArea.height - height) / 2f; float x = Mathf.Max(0, centerX); float y = Mathf.Max(0, centerY); window.position = new Rect(x, y, width, height); EditorPrefs.SetBool(PrefsInitialSizeAppliedKey, true); } private void OnEnable() { _isSearchCachePopulated = false; _objectVisualElementMap.Clear(); _selectedObject = null; _selectedElement = null; _selectedObjects.Clear(); #if ODIN_INSPECTOR _odinPropertyTree = null; #endif _userStateFilePath = Path.Combine(Application.persistentDataPath, UserStateFileName); LoadScriptableObjectTypes(); rootVisualElement.RegisterCallback( HandleGlobalKeyDown, TrickleDown.TrickleDown ); rootVisualElement .schedule.Execute(() => { PopulateSearchCache(); RestorePreviousSelection(); StartPeriodicWidthSave(); }) .ExecuteLater(10); } private void OnDisable() { rootVisualElement.UnregisterCallback( HandleGlobalKeyDown, TrickleDown.TrickleDown ); Cleanup(); } private void OnDestroy() { Cleanup(); } private void Cleanup() { _selectedElement = null; _selectedObject = null; _namespaceController.SelectType(this, null); _scriptableObjectTypes.Clear(); _namespaceOrder.Clear(); _namespaceController.Clear(); _allManagedSOsCache.Clear(); _currentSearchResultItems.Clear(); _currentTypePopoverItems.Clear(); _isSearchCachePopulated = false; CloseActivePopover(); CancelDrag(); _saveWidthsTask?.Pause(); if (!Settings.persistStateInSettingsAsset && _userStateDirty) { SaveUserStateToFile(); } _saveWidthsTask = null; _currentInspectorScriptableObject?.Dispose(); _currentInspectorScriptableObject = null; _dragGhost?.RemoveFromHierarchy(); _dragGhost = null; _draggedElement = null; #if ODIN_INSPECTOR _odinRepaintSchedule?.Pause(); _odinRepaintSchedule = null; if (_odinPropertyTree != null) { _odinPropertyTree.OnPropertyValueChanged -= HandleOdinPropertyValueChanged; _odinPropertyTree.Dispose(); _odinPropertyTree = null; } _odinInspectorContainer?.RemoveFromHierarchy(); _odinInspectorContainer?.Dispose(); _odinInspectorContainer = null; #endif } private void PopulateSearchCache() { _allManagedSOsCache.Clear(); HashSet managedTypes = _scriptableObjectTypes .SelectMany(tuple => tuple.Value) .ToHashSet(); HashSet uniqueGuids = new(StringComparer.OrdinalIgnoreCase); foreach ( Type type in AppDomain .CurrentDomain.GetAssemblies() .SelectMany(assembly => assembly.GetTypes()) .Where(managedTypes.Contains) ) { string[] guids = AssetDatabase.FindAssets($"t:{type.Name}"); foreach (string guid in guids) { if (!uniqueGuids.Add(guid)) { continue; } string path = AssetDatabase.GUIDToAssetPath(guid); if (!string.IsNullOrWhiteSpace(path)) { ScriptableObject obj = AssetDatabase.LoadMainAssetAtPath(path) as ScriptableObject; if (obj != null && obj.GetType() == type) { _allManagedSOsCache.Add(obj); } } } } _allManagedSOsCache.Sort( (a, b) => { int comparison = string.Compare(a.name, b.name, StringComparison.Ordinal); if (comparison != 0) { return comparison; } return string.Compare( a.GetType().FullName, b.GetType().FullName, StringComparison.Ordinal ); } ); _isSearchCachePopulated = true; } public static void SignalRefresh() { if (!HasOpenInstances()) { return; } DataVisualizer window = GetWindow(false, null, false); if (window != null) { window.ScheduleRefresh(); } } private void ScheduleRefresh() { rootVisualElement.schedule.Execute(RefreshAllViews).ExecuteLater(50); } private void SyncNamespaceAndTypeOrders() { List namespaceOrder = _namespaceOrder .OrderBy(kvp => kvp.Value) .Select(kvp => kvp.Key) .ToList(); List typeOrder = _namespaceOrder .OrderBy(kvp => kvp.Value) .Select(kvp => new NamespaceTypeOrder() { namespaceKey = kvp.Key, typeNames = _scriptableObjectTypes[kvp.Key] .Select(type => type.FullName) .ToList(), }) .ToList(); PersistSettings( settings => { settings.namespaceOrder = namespaceOrder; settings.typeOrders = typeOrder; return true; }, userState => { userState.namespaceOrder = namespaceOrder; userState.typeOrders = typeOrder; return true; } ); } internal void PersistSettings( Func settingsApplier, Func userStateApplier ) { DataVisualizerSettings settings = Settings; if (settings.persistStateInSettingsAsset) { if (settingsApplier(settings)) { settings.MarkDirty(); AssetDatabase.SaveAssets(); } } else if (userStateApplier(UserState)) { MarkUserStateDirty(); } } private void RefreshAllViews() { Type selectedType = _namespaceController.SelectedType; string previousNamespaceKey = selectedType != null ? NamespaceController.GetNamespaceKey(selectedType) : string.Empty; string previousTypeName = selectedType?.Name; string previousObjectGuid = null; if (_selectedObject != null) { string path = AssetDatabase.GetAssetPath(_selectedObject); if (!string.IsNullOrWhiteSpace(path)) { previousObjectGuid = AssetDatabase.AssetPathToGUID(path); } } LoadScriptableObjectTypes(); int namespaceIndex = -1; if (!string.IsNullOrWhiteSpace(previousNamespaceKey)) { namespaceIndex = _namespaceOrder.GetValueOrDefault(previousNamespaceKey, -1); } if (namespaceIndex < 0 && 0 < _scriptableObjectTypes.Count) { namespaceIndex = 0; } if (0 <= namespaceIndex) { List typesInNamespace = _scriptableObjectTypes.GetValueOrDefault( previousNamespaceKey, null ); if (0 < typesInNamespace?.Count) { if (!string.IsNullOrWhiteSpace(previousTypeName)) { selectedType = typesInNamespace.FirstOrDefault(t => string.Equals(t.Name, previousTypeName, StringComparison.Ordinal) ); } selectedType ??= typesInNamespace[0]; } else { Debug.LogWarning( $"Failed to find any types for namespace {previousNamespaceKey}." ); } } if (selectedType != null) { LoadObjectTypes(selectedType); } else { _selectedObjects.Clear(); } ScriptableObject selectedObject = _selectedObject; if ( selectedType != null && !string.IsNullOrWhiteSpace(previousObjectGuid) && 0 < _selectedObjects.Count ) { selectedObject = _selectedObjects.Find(obj => { if (obj == null) { return false; } string path = AssetDatabase.GetAssetPath(obj); return !string.IsNullOrWhiteSpace(path) && string.Equals( AssetDatabase.AssetPathToGUID(path), previousObjectGuid, StringComparison.OrdinalIgnoreCase ); }); } BuildNamespaceView(); BuildObjectsView(); VisualElement typeElementToSelect = FindTypeElement(selectedType); if (typeElementToSelect != null) { VisualElement ancestorGroup = FindAncestorNamespaceGroup(typeElementToSelect); if (ancestorGroup != null) { ExpandNamespaceGroupIfNeeded(ancestorGroup, false); } } SelectObject(selectedObject); PopulateSearchCache(); _namespaceController.SelectType(this, selectedType); } private VisualElement FindAncestorNamespaceGroup(VisualElement startingElement) { if (startingElement == null) { return null; } VisualElement currentElement = startingElement; while (currentElement != null && currentElement != _namespaceListContainer) { if (currentElement.ClassListContains("object-item")) { return currentElement; } currentElement = currentElement.parent; } return null; } private void ExpandNamespaceGroupIfNeeded(VisualElement namespaceGroupItem, bool saveState) { if (namespaceGroupItem == null) { return; } Label indicator = namespaceGroupItem.Q