diff --git a/Editor/DataVisualizer/Data/DataVisualizerSettings.cs b/Editor/DataVisualizer/Data/DataVisualizerSettings.cs index 06e0682..1207062 100644 --- a/Editor/DataVisualizer/Data/DataVisualizerSettings.cs +++ b/Editor/DataVisualizer/Data/DataVisualizerSettings.cs @@ -41,7 +41,8 @@ public sealed class DataVisualizerSettings : ScriptableObject [SerializeField] [ReadOnly] - internal string lastSelectedTypeName; + [FormerlySerializedAs("lastSelectedTypeName")] + internal string lastSelectedTypeFullName; [SerializeField] [ReadOnly] @@ -96,6 +97,18 @@ public void MarkDirty() #endif } + public bool SetSelectActiveObject(bool value) + { + if (selectActiveObject == value) + { + return false; + } + + selectActiveObject = value; + MarkDirty(); + return true; + } + public void HydrateFrom(DataVisualizerUserState userState) { if (userState == null) @@ -104,7 +117,7 @@ public void HydrateFrom(DataVisualizerUserState userState) } lastSelectedNamespaceKey = userState.lastSelectedNamespaceKey; - lastSelectedTypeName = userState.lastSelectedTypeName; + lastSelectedTypeFullName = userState.lastSelectedTypeFullName; namespaceOrder = userState.namespaceOrder?.ToList() ?? new List(); typeOrders = userState.typeOrders?.Select(order => order.Clone()).ToList() @@ -144,35 +157,65 @@ internal List GetOrCreateObjectOrderList(string typeFullName) return entry.ObjectGuids; } - internal void SetLastObjectForType(string typeName, string guid) + internal bool SetLastObjectForType(string typeFullName, string guid) { - if (string.IsNullOrWhiteSpace(typeName)) + if (string.IsNullOrWhiteSpace(typeFullName)) { - return; + return false; } + lastObjectSelections ??= new List(); + int existingIndex = lastObjectSelections.FindIndex(e => + string.Equals(e.typeFullName, typeFullName, StringComparison.Ordinal) + ); if (string.IsNullOrWhiteSpace(guid)) { - return; + if (existingIndex < 0) + { + return false; + } + + lastObjectSelections.RemoveAt(existingIndex); + MarkDirty(); + return true; } - lastObjectSelections.RemoveAll(e => - string.Equals(e.typeFullName, typeName, StringComparison.Ordinal) - ); - lastObjectSelections.Add( - new LastObjectSelectionEntry { typeFullName = typeName, objectGuid = guid } - ); + if ( + existingIndex >= 0 + && string.Equals( + lastObjectSelections[existingIndex].objectGuid, + guid, + StringComparison.Ordinal + ) + ) + { + return false; + } + + if (existingIndex >= 0) + { + lastObjectSelections[existingIndex].objectGuid = guid; + } + else + { + lastObjectSelections.Add( + new LastObjectSelectionEntry { typeFullName = typeFullName, objectGuid = guid } + ); + } + + MarkDirty(); + return true; } - internal string GetLastObjectForType(string typeName) + internal string GetLastObjectForType(string typeFullName) { - if (string.IsNullOrWhiteSpace(typeName)) + if (string.IsNullOrWhiteSpace(typeFullName)) { return null; } return lastObjectSelections - .Find(e => string.Equals(e.typeFullName, typeName, StringComparison.Ordinal)) + ?.Find(e => string.Equals(e.typeFullName, typeFullName, StringComparison.Ordinal)) ?.objectGuid; } @@ -199,6 +242,38 @@ internal bool HasCollapseState(string namespaceKey) return entry != null; } + public bool SetNamespaceCollapsed(string namespaceKey, bool isCollapsed) + { + if (string.IsNullOrWhiteSpace(namespaceKey)) + { + return false; + } + + namespaceCollapseStates ??= new List(); + bool changed = NamespaceCollapseState.SetCollapsed( + namespaceCollapseStates, + namespaceKey, + isCollapsed + ); + if (changed) + { + MarkDirty(); + } + + return changed; + } + + public bool RemoveNamespaceCollapseState(string namespaceKey) + { + bool changed = NamespaceCollapseState.Remove(namespaceCollapseStates, namespaceKey); + if (changed) + { + MarkDirty(); + } + + return changed; + } + internal NamespaceCollapseState GetOrCreateCollapseState(string namespaceKey) { NamespaceCollapseState entry = namespaceCollapseStates.Find(o => diff --git a/Editor/DataVisualizer/Data/DataVisualizerUserState.cs b/Editor/DataVisualizer/Data/DataVisualizerUserState.cs index e3df585..5c31168 100644 --- a/Editor/DataVisualizer/Data/DataVisualizerUserState.cs +++ b/Editor/DataVisualizer/Data/DataVisualizerUserState.cs @@ -3,12 +3,16 @@ namespace WallstopStudios.DataVisualizer.Editor.Data using System; using System.Collections.Generic; using System.Linq; + using UnityEngine; + using UnityEngine.Serialization; [Serializable] public sealed class DataVisualizerUserState { public string lastSelectedNamespaceKey = string.Empty; - public string lastSelectedTypeName = string.Empty; + + [FormerlySerializedAs("lastSelectedTypeName")] + public string lastSelectedTypeFullName = string.Empty; public List namespaceOrder = new(); public List typeOrders = new(); @@ -21,6 +25,31 @@ public sealed class DataVisualizerUserState public List labelFilterConfigs = new(); public List processorStates = new(); + public static DataVisualizerUserState FromJson(string json) + { + if (string.IsNullOrWhiteSpace(json)) + { + return null; + } + + DataVisualizerUserState userState = JsonUtility.FromJson(json); + if (userState == null) + { + return userState; + } + + LegacyUserState legacyUserState = JsonUtility.FromJson(json); + if ( + string.IsNullOrWhiteSpace(userState.lastSelectedTypeFullName) + && !string.IsNullOrWhiteSpace(legacyUserState?.lastSelectedTypeName) + ) + { + userState.lastSelectedTypeFullName = legacyUserState.lastSelectedTypeName; + } + + return userState; + } + public void HydrateFrom(DataVisualizerSettings settings) { if (settings == null) @@ -29,7 +58,7 @@ public void HydrateFrom(DataVisualizerSettings settings) } lastSelectedNamespaceKey = settings.lastSelectedNamespaceKey; - lastSelectedTypeName = settings.lastSelectedTypeName; + lastSelectedTypeFullName = settings.lastSelectedTypeFullName; namespaceOrder = settings.namespaceOrder?.ToList() ?? new List(); typeOrders = settings.typeOrders?.Select(order => order.Clone()).ToList() @@ -67,35 +96,63 @@ public List GetOrCreateObjectOrderList(string typeFullName) return entry.ObjectGuids; } - public void SetLastObjectForType(string typeName, string guid) + public bool SetLastObjectForType(string typeFullName, string guid) { - if (string.IsNullOrWhiteSpace(typeName)) + if (string.IsNullOrWhiteSpace(typeFullName)) { - return; + return false; } + lastObjectSelections ??= new List(); + int existingIndex = lastObjectSelections.FindIndex(e => + string.Equals(e.typeFullName, typeFullName, StringComparison.Ordinal) + ); if (string.IsNullOrWhiteSpace(guid)) { - return; + if (existingIndex < 0) + { + return false; + } + + lastObjectSelections.RemoveAt(existingIndex); + return true; } - lastObjectSelections.RemoveAll(e => - string.Equals(e.typeFullName, typeName, StringComparison.Ordinal) - ); - lastObjectSelections.Add( - new LastObjectSelectionEntry { typeFullName = typeName, objectGuid = guid } - ); + if ( + existingIndex >= 0 + && string.Equals( + lastObjectSelections[existingIndex].objectGuid, + guid, + StringComparison.Ordinal + ) + ) + { + return false; + } + + if (existingIndex >= 0) + { + lastObjectSelections[existingIndex].objectGuid = guid; + } + else + { + lastObjectSelections.Add( + new LastObjectSelectionEntry { typeFullName = typeFullName, objectGuid = guid } + ); + } + + return true; } - public string GetLastObjectForType(string typeName) + public string GetLastObjectForType(string typeFullName) { - if (string.IsNullOrWhiteSpace(typeName)) + if (string.IsNullOrWhiteSpace(typeFullName)) { return null; } return lastObjectSelections - .Find(e => string.Equals(e.typeFullName, typeName, StringComparison.Ordinal)) + ?.Find(e => string.Equals(e.typeFullName, typeFullName, StringComparison.Ordinal)) ?.objectGuid; } @@ -122,6 +179,26 @@ public bool HasCollapseState(string namespaceKey) return entry != null; } + public bool SetNamespaceCollapsed(string namespaceKey, bool isCollapsed) + { + if (string.IsNullOrWhiteSpace(namespaceKey)) + { + return false; + } + + namespaceCollapseStates ??= new List(); + return NamespaceCollapseState.SetCollapsed( + namespaceCollapseStates, + namespaceKey, + isCollapsed + ); + } + + public bool RemoveNamespaceCollapseState(string namespaceKey) + { + return NamespaceCollapseState.Remove(namespaceCollapseStates, namespaceKey); + } + public NamespaceCollapseState GetOrCreateCollapseState(string namespaceKey) { NamespaceCollapseState entry = namespaceCollapseStates.Find(o => @@ -136,5 +213,11 @@ public NamespaceCollapseState GetOrCreateCollapseState(string namespaceKey) namespaceCollapseStates.Add(entry); return entry; } + + [Serializable] + private sealed class LegacyUserState + { + public string lastSelectedTypeName; + } } } diff --git a/Editor/DataVisualizer/Data/NamespaceCollapseState.cs b/Editor/DataVisualizer/Data/NamespaceCollapseState.cs index c433924..b86ffd6 100644 --- a/Editor/DataVisualizer/Data/NamespaceCollapseState.cs +++ b/Editor/DataVisualizer/Data/NamespaceCollapseState.cs @@ -1,6 +1,7 @@ namespace WallstopStudios.DataVisualizer.Editor.Data { using System; + using System.Collections.Generic; [Serializable] public sealed class NamespaceCollapseState @@ -8,6 +9,53 @@ public sealed class NamespaceCollapseState public string namespaceKey = string.Empty; public bool isCollapsed; + public static bool SetCollapsed( + List states, + string namespaceKey, + bool isCollapsed + ) + { + if (states == null || string.IsNullOrWhiteSpace(namespaceKey)) + { + return false; + } + + NamespaceCollapseState entry = states.Find(state => + string.Equals(state?.namespaceKey, namespaceKey, StringComparison.Ordinal) + ); + if (entry == null) + { + states.Add( + new NamespaceCollapseState + { + namespaceKey = namespaceKey, + isCollapsed = isCollapsed, + } + ); + return true; + } + + if (entry.isCollapsed == isCollapsed) + { + return false; + } + + entry.isCollapsed = isCollapsed; + return true; + } + + public static bool Remove(List states, string namespaceKey) + { + if (states == null || string.IsNullOrWhiteSpace(namespaceKey)) + { + return false; + } + + return states.RemoveAll(state => + string.Equals(state?.namespaceKey, namespaceKey, StringComparison.Ordinal) + ) > 0; + } + public NamespaceCollapseState Clone() { return new NamespaceCollapseState diff --git a/Editor/DataVisualizer/Data/NamespaceTypeOrder.cs b/Editor/DataVisualizer/Data/NamespaceTypeOrder.cs index 9267cd9..b1ba373 100644 --- a/Editor/DataVisualizer/Data/NamespaceTypeOrder.cs +++ b/Editor/DataVisualizer/Data/NamespaceTypeOrder.cs @@ -10,6 +10,72 @@ public sealed class NamespaceTypeOrder public string namespaceKey = string.Empty; public List typeNames = new(); + public static int CompareTypesByFullNameOrder( + Type lhs, + Type rhs, + IReadOnlyList typeFullNameOrder + ) + { + return CompareTypeFullNames( + lhs?.FullName ?? string.Empty, + rhs?.FullName ?? string.Empty, + typeFullNameOrder + ); + } + + public static int CompareTypesByFullName(Type lhs, Type rhs) + { + return string.Compare( + lhs?.FullName ?? string.Empty, + rhs?.FullName ?? string.Empty, + StringComparison.Ordinal + ); + } + + public static int CompareTypesByNameThenFullName(Type lhs, Type rhs) + { + int nameComparison = string.Compare( + lhs?.Name ?? string.Empty, + rhs?.Name ?? string.Empty, + StringComparison.Ordinal + ); + return nameComparison != 0 ? nameComparison : CompareTypesByFullName(lhs, rhs); + } + + public static int CompareTypeFullNames( + string lhsFullName, + string rhsFullName, + IReadOnlyList typeFullNameOrder + ) + { + int indexA = IndexOf(typeFullNameOrder, lhsFullName); + int indexB = IndexOf(typeFullNameOrder, rhsFullName); + + switch (indexA) + { + case >= 0 when indexB >= 0: + return indexA.CompareTo(indexB); + case >= 0: + return -1; + } + + return 0 <= indexB + ? 1 + : string.Compare(lhsFullName, rhsFullName, StringComparison.Ordinal); + } + + public static Type FindTypeByFullName(IEnumerable types, string typeFullName) + { + if (types == null || string.IsNullOrWhiteSpace(typeFullName)) + { + return null; + } + + return types.FirstOrDefault(type => + string.Equals(type?.FullName, typeFullName, StringComparison.Ordinal) + ); + } + public NamespaceTypeOrder Clone() { return new NamespaceTypeOrder @@ -18,5 +84,23 @@ public NamespaceTypeOrder Clone() typeNames = typeNames?.ToList() ?? new List(), }; } + + private static int IndexOf(IReadOnlyList values, string value) + { + if (values == null || string.IsNullOrWhiteSpace(value)) + { + return -1; + } + + for (int i = 0; i < values.Count; ++i) + { + if (string.Equals(values[i], value, StringComparison.Ordinal)) + { + return i; + } + } + + return -1; + } } } diff --git a/Editor/DataVisualizer/DataVisualizer.cs b/Editor/DataVisualizer/DataVisualizer.cs index 4710861..8880e44 100644 --- a/Editor/DataVisualizer/DataVisualizer.cs +++ b/Editor/DataVisualizer/DataVisualizer.cs @@ -42,9 +42,7 @@ public sealed class DataVisualizer : EditorWindow private const string SettingsDefaultPath = "Assets/Editor/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"; @@ -767,6 +765,20 @@ Func userStateApplier } } + public static bool ApplySelectActiveObjectPreference( + DataVisualizerSettings settings, + bool selectActiveObject + ) + { + if (settings == null || !settings.SetSelectActiveObject(selectActiveObject)) + { + return false; + } + + AssetDatabase.SaveAssets(); + return true; + } + private void RefreshAllViews() { Type selectedType = _namespaceController.SelectedType; @@ -775,7 +787,7 @@ private void RefreshAllViews() selectedType != null ? NamespaceController.GetNamespaceKey(selectedType) : string.Empty; - string previousTypeName = selectedType?.Name; + string previousTypeFullName = selectedType?.FullName; string previousObjectGuid = null; if (_selectedObject != null) { @@ -788,35 +800,12 @@ private void RefreshAllViews() 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.Find(t => - string.Equals(t.Name, previousTypeName, StringComparison.Ordinal) - ); - } - - selectedType ??= typesInNamespace[0]; - } - } + selectedType = ResolveSelectedTypeByFullName( + _scriptableObjectTypes, + _namespaceOrder, + previousNamespaceKey, + previousTypeFullName + ); // Load once. For an unchanged type the SelectType call near the end no-ops its own load, // so reload here (a refresh must re-scan). For a changed type, let that single SelectType @@ -892,6 +881,14 @@ private void RefreshAllViews() } private VisualElement FindAncestorNamespaceGroup(VisualElement startingElement) + { + return FindAncestorNamespaceGroup(startingElement, _namespaceListContainer); + } + + public static VisualElement FindAncestorNamespaceGroup( + VisualElement startingElement, + VisualElement namespaceListContainer + ) { if (startingElement == null) { @@ -899,9 +896,9 @@ private VisualElement FindAncestorNamespaceGroup(VisualElement startingElement) } VisualElement currentElement = startingElement; - while (currentElement != null && currentElement != _namespaceListContainer) + while (currentElement != null && currentElement != namespaceListContainer) { - if (currentElement.ClassListContains("object-item")) + if (currentElement.ClassListContains(StyleConstants.NamespaceItemClass)) { return currentElement; } @@ -912,6 +909,107 @@ private VisualElement FindAncestorNamespaceGroup(VisualElement startingElement) return null; } + public static string FindFirstNamespaceKeyByOrder( + IReadOnlyDictionary namespaceOrder + ) + { + if (namespaceOrder == null || namespaceOrder.Count == 0) + { + return null; + } + + string firstNamespaceKey = null; + int firstNamespaceIndex = int.MaxValue; + foreach (KeyValuePair entry in namespaceOrder) + { + if ( + entry.Value < firstNamespaceIndex + || ( + entry.Value == firstNamespaceIndex + && ( + firstNamespaceKey == null + || string.CompareOrdinal(entry.Key, firstNamespaceKey) < 0 + ) + ) + ) + { + firstNamespaceKey = entry.Key; + firstNamespaceIndex = entry.Value; + } + } + + return firstNamespaceKey; + } + + public static Type ResolveSelectedTypeByFullName( + IReadOnlyDictionary> typesByNamespace, + IReadOnlyDictionary namespaceOrder, + string savedNamespaceKey, + string savedTypeFullName + ) + { + if (typesByNamespace == null || typesByNamespace.Count == 0) + { + return null; + } + + if (!string.IsNullOrWhiteSpace(savedTypeFullName)) + { + Type savedType = NamespaceTypeOrder.FindTypeByFullName( + typesByNamespace.Values.SelectMany(types => types ?? Enumerable.Empty()), + savedTypeFullName + ); + if (savedType != null) + { + return savedType; + } + } + + if ( + !string.IsNullOrWhiteSpace(savedNamespaceKey) + && typesByNamespace.TryGetValue(savedNamespaceKey, out List savedTypes) + && savedTypes is { Count: > 0 } + ) + { + return savedTypes[0]; + } + + string firstOrderedNamespaceKey = FindFirstNamespaceKeyByOrder(namespaceOrder); + if ( + !string.IsNullOrWhiteSpace(firstOrderedNamespaceKey) + && typesByNamespace.TryGetValue( + firstOrderedNamespaceKey, + out List firstOrderedTypes + ) + && firstOrderedTypes is { Count: > 0 } + ) + { + return firstOrderedTypes[0]; + } + + foreach ( + string namespaceKey in (namespaceOrder ?? new Dictionary()) + .OrderBy(entry => entry.Value) + .ThenBy(entry => entry.Key, StringComparer.Ordinal) + .Select(entry => entry.Key) + ) + { + if ( + typesByNamespace.TryGetValue(namespaceKey, out List orderedTypes) + && orderedTypes is { Count: > 0 } + ) + { + return orderedTypes[0]; + } + } + + return typesByNamespace + .OrderBy(entry => entry.Key, StringComparer.Ordinal) + .Select(entry => entry.Value) + .FirstOrDefault(types => types is { Count: > 0 }) + ?.FirstOrDefault(); + } + private void ExpandNamespaceGroupIfNeeded(VisualElement namespaceGroupItem, bool saveState) { if (namespaceGroupItem == null) @@ -1045,58 +1143,18 @@ private void RestorePreviousSelection() return; } - string savedNamespaceKey = GetLastSelectedNamespaceKey(); - List typesInNamespace; - int namespaceIndex = -1; - - if (!string.IsNullOrWhiteSpace(savedNamespaceKey)) - { - namespaceIndex = _namespaceOrder.GetValueOrDefault(savedNamespaceKey, -1); - } - - if (0 <= namespaceIndex) - { - typesInNamespace = _scriptableObjectTypes.GetValueOrDefault( - savedNamespaceKey, - null - ); - } - else if (0 < _namespaceOrder.Count) - { - int bestIndex = _scriptableObjectTypes.Count; - string bestNamespace = null; - foreach (KeyValuePair entry in _namespaceOrder) - { - if (entry.Value < bestIndex) - { - bestNamespace = entry.Key; - } - } - - typesInNamespace = _scriptableObjectTypes.GetValueOrDefault(bestNamespace, null); - } - else - { - typesInNamespace = null; - } + Type selectedType = ResolveSelectedTypeByFullName( + _scriptableObjectTypes, + _namespaceOrder, + GetLastSelectedNamespaceKey(), + GetLastSelectedTypeFullName() + ); - if (typesInNamespace is not { Count: > 0 }) + if (selectedType == null) { return; } - string savedTypeName = GetLastSelectedTypeName(); - Type selectedType = null; - - if (!string.IsNullOrWhiteSpace(savedTypeName)) - { - selectedType = typesInNamespace.Find(t => - string.Equals(t.FullName, savedTypeName, StringComparison.Ordinal) - ); - } - - selectedType ??= typesInNamespace[0]; - // Build namespace view first so type selection is visible BuildNamespaceView(); @@ -1110,34 +1168,10 @@ private void RestorePreviousSelection() VisualElement typeElementToSelect = FindTypeElement(selectedType); if (typeElementToSelect != null) { - VisualElement ancestorGroup = null; - VisualElement currentElement = typeElementToSelect; - while (currentElement != null && currentElement != _namespaceListContainer) - { - if (currentElement.ClassListContains(NamespaceItemClass)) - { - ancestorGroup = currentElement; - break; - } - - currentElement = currentElement.parent; - } - + VisualElement ancestorGroup = FindAncestorNamespaceGroup(typeElementToSelect); if (ancestorGroup != null) { - Label indicator = ancestorGroup.Q