From 536c4b1d1335f4fcdad3de95a408c12550b2a2f1 Mon Sep 17 00:00:00 2001 From: Matt Schoen Date: Thu, 26 May 2022 23:01:33 -0700 Subject: [PATCH 01/14] Initial commit for layer users feature --- Editor/PrefabLayerUsers.cs | 484 ++++++++++++++++++++++ Editor/PrefabLayerUsers.cs.meta | 11 + Editor/Unity.Labs.SuperScience.Editor.api | 5 + 3 files changed, 500 insertions(+) create mode 100644 Editor/PrefabLayerUsers.cs create mode 100644 Editor/PrefabLayerUsers.cs.meta diff --git a/Editor/PrefabLayerUsers.cs b/Editor/PrefabLayerUsers.cs new file mode 100644 index 0000000..e92e135 --- /dev/null +++ b/Editor/PrefabLayerUsers.cs @@ -0,0 +1,484 @@ +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using UnityEditor; +using UnityEngine; +using Debug = UnityEngine.Debug; + +namespace Unity.Labs.SuperScience +{ + /// + /// Scans the project for textures comprised of a single solid color. + /// Use this utility to identify redundant textures, and textures which are larger than they need to be. + /// + public class PrefabLayerUsers : EditorWindow + { + /// + /// Container for unique color rows. + /// + class LayerRow + { + public readonly List prefabs = new List(); + } + + /// + /// Tree structure for folder scan results. + /// This is the root object for the project scan, and represents the results in a hierarchy that matches the + /// project's folder structure for an easy to read presentation of solid color textures. + /// When the Scan method encounters a texture, we initialize one of these using the asset path to determine where it belongs. + /// + class Folder + { + // TODO: Share code between this window and MissingProjectReferences + static class Styles + { + internal static readonly GUIStyle ProSkinLineStyle = new GUIStyle + { + normal = new GUIStyleState + { + background = Texture2D.grayTexture + } + }; + } + + const string k_LabelFormat = "{0}: {1}"; + const int k_IndentAmount = 15; + const int k_SeparatorLineHeight = 1; + + readonly SortedDictionary m_Subfolders = new SortedDictionary(); + readonly List<(string, GameObject, SortedSet)> m_Prefabs = new List<(string, GameObject, SortedSet)>(); + readonly SortedDictionary m_CountPerLayer = new SortedDictionary(); + int m_TotalCount; + bool m_Visible; + + /// + /// Clear the contents of this container. + /// + public void Clear() + { + m_Subfolders.Clear(); + m_Prefabs.Clear(); + m_CountPerLayer.Clear(); + } + + /// + /// Add a texture to this folder at a given path. + /// + /// The path of the texture. + /// The prefab to add. + /// List of layers used by this prefab + public void AddPrefabAtPath(string path, GameObject prefabAsset, SortedSet layers) + { + var folder = GetOrCreateFolderForAssetPath(path, layers); + folder.m_Prefabs.Add((path, prefabAsset, layers)); + } + + /// + /// Get the Folder object which corresponds to the given path. + /// If this is the first asset encountered for a given folder, create a chain of folder objects + /// rooted with this one and return the folder at the end of that chain. + /// Every time a folder is accessed, its Count property is incremented to indicate that it contains one + /// more solid color texture. + /// + /// Path to a solid color texture relative to this folder. + /// The layers for the object which will be added to this folder when it is created (used to aggregate layer counts) + /// The folder object corresponding to the folder containing the texture at the given path. + Folder GetOrCreateFolderForAssetPath(string path, SortedSet layers) + { + var directories = path.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + var folder = this; + folder.AggregateCount(layers); + var length = directories.Length - 1; + for (var i = 0; i < length; i++) + { + var directory = directories[i]; + Folder subfolder; + var subfolders = folder.m_Subfolders; + if (!subfolders.TryGetValue(directory, out subfolder)) + { + subfolder = new Folder(); + subfolders[directory] = subfolder; + } + + folder = subfolder; + folder.AggregateCount(layers); + } + + return folder; + } + + /// + /// Draw GUI for this Folder. + /// + /// The name of the folder. + /// (Optional) Layer used to filter results + public void Draw(string name, int layerFilter = -1) + { + var wasVisible = m_Visible; + m_Visible = EditorGUILayout.Foldout(m_Visible, string.Format(k_LabelFormat, name, GetCount(layerFilter)), true); + + DrawLineSeparator(); + + // Hold alt to apply visibility state to all children (recursively) + if (m_Visible != wasVisible && Event.current.alt) + SetVisibleRecursively(m_Visible); + + if (!m_Visible) + return; + + using (new EditorGUI.IndentLevelScope()) + { + foreach (var kvp in m_Subfolders) + { + var folder = kvp.Value; + if (folder.GetCount(layerFilter) == 0) + continue; + + folder.Draw(kvp.Key, layerFilter); + } + + foreach (var (_, prefab, layers) in m_Prefabs) + { + if (layerFilter == -1 || layers.Contains(layerFilter)) + EditorGUILayout.ObjectField($"{prefab.name} ({string.Join(", ", layers)})", prefab, typeof(GameObject), false); + } + + if (m_Prefabs.Count > 0) + DrawLineSeparator(); + } + } + + /// + /// Draw a separator line. + /// + static void DrawLineSeparator() + { + EditorGUILayout.Separator(); + using (new GUILayout.HorizontalScope()) + { + GUILayout.Space(EditorGUI.indentLevel * k_IndentAmount); + GUILayout.Box(GUIContent.none, Styles.ProSkinLineStyle, GUILayout.Height(k_SeparatorLineHeight), GUILayout.ExpandWidth(true)); + } + + EditorGUILayout.Separator(); + } + + /// + /// Set the visibility state of this folder, its contents and their children and all of its subfolders and their contents and children. + /// + /// Whether this object and its children should be visible in the GUI. + void SetVisibleRecursively(bool visible) + { + m_Visible = visible; + foreach (var kvp in m_Subfolders) + { + kvp.Value.SetVisibleRecursively(visible); + } + } + + /// + /// Sort the contents of this folder and all subfolders by name. + /// + public void SortContentsRecursively() + { + m_Prefabs.Sort((a, b) => a.Item2.name.CompareTo(b.Item2.name)); + foreach (var kvp in m_Subfolders) + { + kvp.Value.SortContentsRecursively(); + } + } + + public int GetCount(int layerFilter = -1) + { + if (layerFilter == -1) + return m_TotalCount; + + m_CountPerLayer.TryGetValue(layerFilter, out var count); + return count; + } + + void AggregateCount(SortedSet layers) + { + m_TotalCount++; + foreach (var layer in layers) + { + m_CountPerLayer.TryGetValue(layer, out var count); + count++; + m_CountPerLayer[layer] = count; + } + } + } + + const string k_NoMissingReferences = "No prefabs using a non-default layer"; + const string k_ProjectFolderName = "Project"; + const int k_TextureColumnWidth = 150; + const int k_ColorPanelWidth = 150; + const string k_WindowTitle = "Layer Users"; + const string k_Instructions = "Click the Scan button to scan your project for users of non-default layers. WARNING: " + + "This will load every prefab in your project. For large projects, this may take a long time and/or crash the Editor."; + const string k_ScanFilter = "t:Prefab"; + const int k_ProgressBarHeight = 15; + const int k_MaxScanUpdateTimeMilliseconds = 50; + + static readonly GUIContent k_ScanGUIContent = new GUIContent("Scan", "Scan the project for users of non-default layers"); + static readonly GUILayoutOption k_LayerPanelWidthOption = GUILayout.Width(k_ColorPanelWidth); + static readonly Vector2 k_MinSize = new Vector2(400, 200); + + static readonly Stopwatch k_StopWatch = new Stopwatch(); + + Vector2 m_ColorListScrollPosition; + Vector2 m_FolderTreeScrollPosition; + readonly Folder m_ParentFolder = new Folder(); + readonly SortedDictionary m_PrefabsByLayer = new SortedDictionary(); + static readonly string[] k_ScanFolders = new[] { "Assets", "Packages" }; + int m_ScanCount; + int m_ScanProgress; + IEnumerator m_ScanEnumerator; + readonly List m_LayersWithNames = new List(); + int m_LayerFilter = -1; + + // Local method use only -- created here to reduce garbage collection. Collections must be cleared before use + static readonly List k_Components = new List(); + + /// + /// Initialize the window + /// + [MenuItem("Window/SuperScience/Layer Users")] + static void Init() + { + GetWindow(k_WindowTitle).Show(); + } + + void OnEnable() + { + minSize = k_MinSize; + m_ScanCount = 0; + m_ScanProgress = 0; + } + + void OnGUI() + { + EditorGUIUtility.labelWidth = position.width - k_TextureColumnWidth - k_ColorPanelWidth; + + var rect = GUILayoutUtility.GetRect(0, float.PositiveInfinity, k_ProgressBarHeight, k_ProgressBarHeight); + EditorGUI.ProgressBar(rect, (float)m_ScanProgress / m_ScanCount, $"{m_ScanProgress} / {m_ScanCount}"); + if (GUILayout.Button(k_ScanGUIContent)) + Scan(); + + if (m_ParentFolder.GetCount(m_LayerFilter) == 0) + { + EditorGUILayout.HelpBox(k_Instructions, MessageType.Info); + GUIUtility.ExitGUI(); + return; + } + + if (m_ParentFolder.GetCount(m_LayerFilter) == 0) + { + GUILayout.Label(k_NoMissingReferences); + } + else + { + GUILayout.Label($"Layer Filter: {(m_LayerFilter == -1 ? "All" : m_LayerFilter.ToString())}"); + using (new GUILayout.HorizontalScope()) + { + using (new GUILayout.VerticalScope(k_LayerPanelWidthOption)) + { + DrawColors(); + } + + using (var scrollView = new GUILayout.ScrollViewScope(m_FolderTreeScrollPosition)) + { + m_FolderTreeScrollPosition = scrollView.scrollPosition; + m_ParentFolder.Draw(k_ProjectFolderName, m_LayerFilter); + } + } + } + } + + /// + /// Draw a list of unique layers. + /// + void DrawColors() + { + GUILayout.Label($"{m_PrefabsByLayer.Count} Used Layers"); + if (GUILayout.Button("All")) + m_LayerFilter = -1; + + using (var scrollView = new GUILayout.ScrollViewScope(m_ColorListScrollPosition)) + { + m_ColorListScrollPosition = scrollView.scrollPosition; + foreach (var kvp in m_PrefabsByLayer) + { + var layer = kvp.Key; + if (GUILayout.Button($"Layer {layer} ({kvp.Value.prefabs.Count})")) + m_LayerFilter = layer; + } + } + } + + /// + /// Update the current scan coroutine. + /// + void UpdateScan() + { + if (m_ScanEnumerator == null) + return; + + k_StopWatch.Reset(); + k_StopWatch.Start(); + + // Process as many steps as possible within a given time frame + while (m_ScanEnumerator.MoveNext()) + { + // Process for a maximum amount of time and early-out to keep the UI responsive + if (k_StopWatch.ElapsedMilliseconds > k_MaxScanUpdateTimeMilliseconds) + break; + } + + m_ParentFolder.SortContentsRecursively(); + } + + /// + /// Coroutine for processing scan results. + /// + /// Prefab assets to scan. + /// IEnumerator used to run the coroutine. + IEnumerator ProcessScan(Dictionary prefabAssets) + { + m_ScanCount = prefabAssets.Count; + foreach (var kvp in prefabAssets) + { + FindLayerUsersInPrefab(kvp.Key, kvp.Value); + m_ScanProgress++; + yield return null; + } + + m_ScanEnumerator = null; + EditorApplication.update -= UpdateScan; + } + + void FindLayerUsersInPrefab(string path, GameObject prefabAsset) + { + var layers = new SortedSet(); + FindLayerUsersRecursively(prefabAsset, layers); + if (layers.Count > 0) + { + m_ParentFolder.AddPrefabAtPath(path, prefabAsset, layers); + foreach (var layer in layers) + { + var layerRow = GetOrCreateRowForColor(layer); + layerRow.prefabs.Add(prefabAsset); + } + } + } + + void FindLayerUsersRecursively(GameObject gameObject, SortedSet layers) + { + var layer = gameObject.layer; + if (layer != 0) + layers.Add(layer); + + // GetComponents will clear the list, so we don't have to + gameObject.GetComponents(k_Components); + foreach (var component in k_Components) + { + var serializedObject = new SerializedObject(component); + var iterator = serializedObject.GetIterator(); + while (iterator.Next(true)) + { + if (iterator.propertyType != SerializedPropertyType.LayerMask) + continue; + + GetLayersFromLayerMask(layers, iterator.intValue); + } + } + + // Clear the list after we're done to avoid lingering references + k_Components.Clear(); + + foreach (Transform child in gameObject.transform) + { + FindLayerUsersRecursively(child.gameObject, layers); + } + } + + void GetLayersFromLayerMask(SortedSet layers, int layerMask) + { + // If layer 0 is in the mask, assume that "on" is the default, meaning that a layer that is not the mask + // counts as "used". Otherwise, if layer 0 is not in the mask, layers that are in the mask count as "used" + var defaultValue = (layerMask & 1) != 0; + + // Exclude the special cases where every layer except default is included or excluded + if (layerMask == -2 || layerMask == 1) + return; + + // Skip layer 0 since we only want non-default layers + var count = m_LayersWithNames.Count; + for (var i = 1; i < count; i++) + { + var layer = m_LayersWithNames[i]; + if ((layerMask & (1 << layer)) != 0 ^ defaultValue) + layers.Add(layer); + } + } + + /// + /// Scan the project for solid color textures and populate the data structures for UI. + /// + void Scan() + { + var guids = AssetDatabase.FindAssets(k_ScanFilter, k_ScanFolders); + if (guids == null || guids.Length == 0) + return; + + m_PrefabsByLayer.Clear(); + m_ParentFolder.Clear(); + + m_LayersWithNames.Clear(); + for (var i = 0; i < 32; i++) + { + if (!string.IsNullOrEmpty(LayerMask.LayerToName(i))) + m_LayersWithNames.Add(i); + } + + var prefabAssets = new Dictionary(); + foreach (var guid in guids) + { + var path = AssetDatabase.GUIDToAssetPath(guid); + if (string.IsNullOrEmpty(path)) + { + Debug.Log($"Could not convert {guid} to path"); + continue; + } + + var prefab = AssetDatabase.LoadAssetAtPath(path); + if (prefab == null) + { + Debug.LogWarning($"Could not load prefab at {path}"); + continue; + } + + prefabAssets.Add(path, prefab); + } + + m_ScanEnumerator = ProcessScan(prefabAssets); + EditorApplication.update += UpdateScan; + } + + /// + /// Get or create a for a given layer value. + /// + /// The layer value to use for this row. + /// The layer row for the layer value. + LayerRow GetOrCreateRowForColor(int layer) + { + if (m_PrefabsByLayer.TryGetValue(layer, out var row)) + return row; + + row = new LayerRow(); + m_PrefabsByLayer[layer] = row; + return row; + } + } +} diff --git a/Editor/PrefabLayerUsers.cs.meta b/Editor/PrefabLayerUsers.cs.meta new file mode 100644 index 0000000..e4eb1bf --- /dev/null +++ b/Editor/PrefabLayerUsers.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6c6cd2b1d787a3145beb17890a5a9672 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Unity.Labs.SuperScience.Editor.api b/Editor/Unity.Labs.SuperScience.Editor.api index 46a5dcf..fcf6565 100644 --- a/Editor/Unity.Labs.SuperScience.Editor.api +++ b/Editor/Unity.Labs.SuperScience.Editor.api @@ -29,6 +29,11 @@ namespace Unity.Labs.SuperScience [System.Runtime.CompilerServices.Extension] public static void StopRunInEditMode(UnityEngine.MonoBehaviour behaviour); } + public class PrefabLayerUsers : UnityEditor.EditorWindow + { + public PrefabLayerUsers() {} + } + public class RunInEditHelper : UnityEditor.EditorWindow { public RunInEditHelper() {} From a005dbd9e79cc91ed91531c39d84efe2ee71469d Mon Sep 17 00:00:00 2001 From: Matt Schoen Date: Sat, 28 May 2022 01:38:00 -0700 Subject: [PATCH 02/14] Fix inconsistencies scanning and counting layer mask fields; Display layer names instead of numbers; UI polish; Code cleanup --- Editor/PrefabLayerUsers.cs | 384 ++++++++++++++++++++++++++----------- 1 file changed, 273 insertions(+), 111 deletions(-) diff --git a/Editor/PrefabLayerUsers.cs b/Editor/PrefabLayerUsers.cs index e92e135..1cb8ca9 100644 --- a/Editor/PrefabLayerUsers.cs +++ b/Editor/PrefabLayerUsers.cs @@ -1,7 +1,9 @@ -using System.Collections; +using System; +using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.Text; using UnityEditor; using UnityEngine; using Debug = UnityEngine.Debug; @@ -14,14 +16,6 @@ namespace Unity.Labs.SuperScience /// public class PrefabLayerUsers : EditorWindow { - /// - /// Container for unique color rows. - /// - class LayerRow - { - public readonly List prefabs = new List(); - } - /// /// Tree structure for folder scan results. /// This is the root object for the project scan, and represents the results in a hierarchy that matches the @@ -31,27 +25,20 @@ class LayerRow class Folder { // TODO: Share code between this window and MissingProjectReferences - static class Styles - { - internal static readonly GUIStyle ProSkinLineStyle = new GUIStyle - { - normal = new GUIStyleState - { - background = Texture2D.grayTexture - } - }; - } - - const string k_LabelFormat = "{0}: {1}"; const int k_IndentAmount = 15; const int k_SeparatorLineHeight = 1; readonly SortedDictionary m_Subfolders = new SortedDictionary(); - readonly List<(string, GameObject, SortedSet)> m_Prefabs = new List<(string, GameObject, SortedSet)>(); - readonly SortedDictionary m_CountPerLayer = new SortedDictionary(); + readonly List<(string, GameObject, SortedSet, SortedSet)> m_Prefabs = new List<(string, GameObject, SortedSet, SortedSet)>(); + readonly SortedDictionary m_TotalCountPerLayer = new SortedDictionary(); + readonly SortedDictionary m_TotalWithoutLayerMasksPerLayer = new SortedDictionary(); int m_TotalCount; + int m_TotalWithoutLayerMasks; bool m_Visible; + // Local method use only -- created here to reduce garbage collection. Collections must be cleared before use + static readonly StringBuilder k_StringBuilder = new StringBuilder(); + /// /// Clear the contents of this container. /// @@ -59,7 +46,10 @@ public void Clear() { m_Subfolders.Clear(); m_Prefabs.Clear(); - m_CountPerLayer.Clear(); + m_TotalCountPerLayer.Clear(); + m_TotalWithoutLayerMasksPerLayer.Clear(); + m_TotalCount = 0; + m_TotalWithoutLayerMasks = 0; } /// @@ -67,11 +57,12 @@ public void Clear() /// /// The path of the texture. /// The prefab to add. - /// List of layers used by this prefab - public void AddPrefabAtPath(string path, GameObject prefabAsset, SortedSet layers) + /// List of layers used by GameObjects in this prefab + /// List of layers used by LayerMask fields in this prefab + public void AddPrefabAtPath(string path, GameObject prefabAsset, SortedSet gameObjectLayers, SortedSet layerMaskLayers) { - var folder = GetOrCreateFolderForAssetPath(path, layers); - folder.m_Prefabs.Add((path, prefabAsset, layers)); + var folder = GetOrCreateFolderForAssetPath(path, gameObjectLayers, layerMaskLayers); + folder.m_Prefabs.Add((path, prefabAsset, gameObjectLayers, layerMaskLayers)); } /// @@ -82,27 +73,27 @@ public void AddPrefabAtPath(string path, GameObject prefabAsset, SortedSet /// more solid color texture. /// /// Path to a solid color texture relative to this folder. - /// The layers for the object which will be added to this folder when it is created (used to aggregate layer counts) + /// The GameObject layers for the object which will be added to this folder when it is created (used to aggregate layer counts) + /// The LayerMask layers for the object which will be added to this folder when it is created (used to aggregate layer counts) /// The folder object corresponding to the folder containing the texture at the given path. - Folder GetOrCreateFolderForAssetPath(string path, SortedSet layers) + Folder GetOrCreateFolderForAssetPath(string path, SortedSet gameObjectLayers, SortedSet layerMaskLayers) { var directories = path.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); var folder = this; - folder.AggregateCount(layers); + folder.AggregateCount(gameObjectLayers, layerMaskLayers); var length = directories.Length - 1; for (var i = 0; i < length; i++) { var directory = directories[i]; - Folder subfolder; var subfolders = folder.m_Subfolders; - if (!subfolders.TryGetValue(directory, out subfolder)) + if (!subfolders.TryGetValue(directory, out var subfolder)) { subfolder = new Folder(); subfolders[directory] = subfolder; } folder = subfolder; - folder.AggregateCount(layers); + folder.AggregateCount(gameObjectLayers, layerMaskLayers); } return folder; @@ -112,11 +103,15 @@ Folder GetOrCreateFolderForAssetPath(string path, SortedSet layers) /// Draw GUI for this Folder. /// /// The name of the folder. - /// (Optional) Layer used to filter results - public void Draw(string name, int layerFilter = -1) + /// Layer to Name dictionary for fast lookup of layer names. + /// (Optional) Layer used to filter results. + /// (Optional) Whether to include layers from LayerMask fields in the results. + public void Draw(string name, Dictionary layerToName, int layerFilter = k_InvalidLayer, bool includeLayerMaskFields = true) { var wasVisible = m_Visible; - m_Visible = EditorGUILayout.Foldout(m_Visible, string.Format(k_LabelFormat, name, GetCount(layerFilter)), true); + var layerNameList = GetLayerNameList(m_TotalCountPerLayer.Keys, m_TotalWithoutLayerMasksPerLayer.Keys, layerToName, layerFilter, includeLayerMaskFields); + var label = $"{name}: {GetCount(layerFilter, includeLayerMaskFields)} {{{layerNameList}}}"; + m_Visible = EditorGUILayout.Foldout(m_Visible, label, true); DrawLineSeparator(); @@ -132,23 +127,62 @@ public void Draw(string name, int layerFilter = -1) foreach (var kvp in m_Subfolders) { var folder = kvp.Value; - if (folder.GetCount(layerFilter) == 0) + if (folder.GetCount(layerFilter, includeLayerMaskFields) == 0) continue; - folder.Draw(kvp.Key, layerFilter); + folder.Draw(kvp.Key, layerToName, layerFilter, includeLayerMaskFields); } - foreach (var (_, prefab, layers) in m_Prefabs) + var showedPrefab = false; + foreach (var (_, prefab, gameObjectLayers, layerMaskLayers) in m_Prefabs) { - if (layerFilter == -1 || layers.Contains(layerFilter)) - EditorGUILayout.ObjectField($"{prefab.name} ({string.Join(", ", layers)})", prefab, typeof(GameObject), false); + if (layerFilter == k_InvalidLayer || gameObjectLayers.Contains(layerFilter) || layerMaskLayers.Contains(layerFilter)) + { + layerNameList = GetLayerNameList(gameObjectLayers, layerMaskLayers, layerToName, layerFilter, includeLayerMaskFields); + EditorGUILayout.ObjectField($"{prefab.name} ({layerNameList})", prefab, typeof(GameObject), false); + showedPrefab = true; + } } - if (m_Prefabs.Count > 0) + if (showedPrefab) DrawLineSeparator(); } } + static string GetLayerNameList(IEnumerable gameObjectLayers, IEnumerable layerMaskLayers, + Dictionary layerToName, int layerFilter = k_InvalidLayer, bool includeLayerMaskFields = true) + { + if (layerFilter >= 0) + { + layerToName.TryGetValue(layerFilter, out var layerName); + if (string.IsNullOrEmpty(layerName)) + layerName = layerFilter.ToString(); + + return layerName; + } + + k_StringBuilder.Length = 0; + k_LayerUnionHashSet.Clear(); + k_LayerUnionHashSet.UnionWith(gameObjectLayers); + if (includeLayerMaskFields) + k_LayerUnionHashSet.UnionWith(layerMaskLayers); + + foreach (var layer in k_LayerUnionHashSet) + { + layerToName.TryGetValue(layer, out var layerName); + if (string.IsNullOrEmpty(layerName)) + layerName = layer.ToString(); + + k_StringBuilder.Append($"{layerName}, "); + } + + // Remove the last ", ". If we didn't add any layers, the StringBuilder will be empty so skip this step + if (k_StringBuilder.Length >= 2) + k_StringBuilder.Length -= 2; + + return k_StringBuilder.ToString(); + } + /// /// Draw a separator line. /// @@ -158,7 +192,7 @@ static void DrawLineSeparator() using (new GUILayout.HorizontalScope()) { GUILayout.Space(EditorGUI.indentLevel * k_IndentAmount); - GUILayout.Box(GUIContent.none, Styles.ProSkinLineStyle, GUILayout.Height(k_SeparatorLineHeight), GUILayout.ExpandWidth(true)); + GUILayout.Box(GUIContent.none, Styles.LineStyle, GUILayout.Height(k_SeparatorLineHeight), GUILayout.ExpandWidth(true)); } EditorGUILayout.Separator(); @@ -189,40 +223,101 @@ public void SortContentsRecursively() } } - public int GetCount(int layerFilter = -1) + public int GetCount(int layerFilter = k_InvalidLayer, bool includeLayerMaskFields = true) { - if (layerFilter == -1) + var unfiltered = layerFilter == k_InvalidLayer; + if (unfiltered && includeLayerMaskFields) return m_TotalCount; - m_CountPerLayer.TryGetValue(layerFilter, out var count); - return count; + if (unfiltered) + return m_TotalWithoutLayerMasks; + + if (includeLayerMaskFields) + { + m_TotalCountPerLayer.TryGetValue(layerFilter, out var totalCount); + return totalCount; + } + + m_TotalWithoutLayerMasksPerLayer.TryGetValue(layerFilter, out var totalWithoutLayerMasks); + return totalWithoutLayerMasks; } - void AggregateCount(SortedSet layers) + void AggregateCount(SortedSet gameObjectLayers, SortedSet layerMaskLayers) { - m_TotalCount++; - foreach (var layer in layers) + var hasGameObjectLayers = gameObjectLayers.Count > 0; + if (hasGameObjectLayers || layerMaskLayers.Count > 0) + m_TotalCount++; + + if (hasGameObjectLayers) + m_TotalWithoutLayerMasks++; + + k_LayerUnionHashSet.Clear(); + k_LayerUnionHashSet.UnionWith(gameObjectLayers); + k_LayerUnionHashSet.UnionWith(layerMaskLayers); + foreach (var layer in k_LayerUnionHashSet) { - m_CountPerLayer.TryGetValue(layer, out var count); + m_TotalCountPerLayer.TryGetValue(layer, out var count); count++; - m_CountPerLayer[layer] = count; + m_TotalCountPerLayer[layer] = count; + } + + foreach (var layer in gameObjectLayers) + { + m_TotalWithoutLayerMasksPerLayer.TryGetValue(layer, out var count); + count++; + m_TotalWithoutLayerMasksPerLayer[layer] = count; } } } + static class Styles + { + internal static readonly GUIStyle ActiveFilterButton = new GUIStyle(GUI.skin.button) + { + alignment = TextAnchor.MiddleLeft, + fontStyle = FontStyle.Bold + }; + + internal static readonly GUIStyle InactiveFilterButton = new GUIStyle(GUI.skin.button) + { + alignment = TextAnchor.MiddleLeft + }; + + internal static readonly GUIStyle LineStyle = new GUIStyle + { + normal = new GUIStyleState + { +#if UNITY_2019_4_OR_NEWER + background = Texture2D.grayTexture +#else + background = Texture2D.whiteTexture +#endif + } + }; + } + + struct FilterRow + { + public HashSet AllUsers; + public HashSet UsersWithoutLayerMasks; + } + const string k_NoMissingReferences = "No prefabs using a non-default layer"; const string k_ProjectFolderName = "Project"; - const int k_TextureColumnWidth = 150; - const int k_ColorPanelWidth = 150; - const string k_WindowTitle = "Layer Users"; + const int k_FilterPanelWidth = 180; + const string k_WindowTitle = "Prefab Layer Users"; const string k_Instructions = "Click the Scan button to scan your project for users of non-default layers. WARNING: " + "This will load every prefab in your project. For large projects, this may take a long time and/or crash the Editor."; const string k_ScanFilter = "t:Prefab"; const int k_ProgressBarHeight = 15; const int k_MaxScanUpdateTimeMilliseconds = 50; + const int k_InvalidLayer = -1; + + static readonly GUIContent k_IncludeLayerMaskFieldsGUIContent = new GUIContent("Include LayerMask Fields", + "Include layers from layer mask fields in the results. This is only possible if there is at least one layer without a name."); static readonly GUIContent k_ScanGUIContent = new GUIContent("Scan", "Scan the project for users of non-default layers"); - static readonly GUILayoutOption k_LayerPanelWidthOption = GUILayout.Width(k_ColorPanelWidth); + static readonly GUILayoutOption k_LayerPanelWidthOption = GUILayout.Width(k_FilterPanelWidth); static readonly Vector2 k_MinSize = new Vector2(400, 200); static readonly Stopwatch k_StopWatch = new Stopwatch(); @@ -230,16 +325,23 @@ void AggregateCount(SortedSet layers) Vector2 m_ColorListScrollPosition; Vector2 m_FolderTreeScrollPosition; readonly Folder m_ParentFolder = new Folder(); - readonly SortedDictionary m_PrefabsByLayer = new SortedDictionary(); - static readonly string[] k_ScanFolders = new[] { "Assets", "Packages" }; + readonly SortedDictionary m_FilterRows = new SortedDictionary(); + static readonly string[] k_ScanFolders = { "Assets", "Packages" }; int m_ScanCount; int m_ScanProgress; IEnumerator m_ScanEnumerator; - readonly List m_LayersWithNames = new List(); - int m_LayerFilter = -1; + readonly Dictionary m_LayerToName = new Dictionary(); + int m_LayerWithNoName = k_InvalidLayer; + + [SerializeField] + bool m_IncludeLayerMaskFields = true; + + [SerializeField] + int m_LayerFilter = k_InvalidLayer; // Local method use only -- created here to reduce garbage collection. Collections must be cleared before use static readonly List k_Components = new List(); + static readonly SortedSet k_LayerUnionHashSet = new SortedSet(); /// /// Initialize the window @@ -259,59 +361,85 @@ void OnEnable() void OnGUI() { - EditorGUIUtility.labelWidth = position.width - k_TextureColumnWidth - k_ColorPanelWidth; - - var rect = GUILayoutUtility.GetRect(0, float.PositiveInfinity, k_ProgressBarHeight, k_ProgressBarHeight); - EditorGUI.ProgressBar(rect, (float)m_ScanProgress / m_ScanCount, $"{m_ScanProgress} / {m_ScanCount}"); if (GUILayout.Button(k_ScanGUIContent)) Scan(); - if (m_ParentFolder.GetCount(m_LayerFilter) == 0) + // If m_LayerToName hasn't been set up, we haven't scanned yet + // This dictionary will always at least include the built-in layer names + if (m_LayerToName.Count == 0) { EditorGUILayout.HelpBox(k_Instructions, MessageType.Info); GUIUtility.ExitGUI(); return; } - if (m_ParentFolder.GetCount(m_LayerFilter) == 0) + if (m_ParentFolder.GetCount(m_LayerFilter, m_IncludeLayerMaskFields) == 0) { GUILayout.Label(k_NoMissingReferences); } else { - GUILayout.Label($"Layer Filter: {(m_LayerFilter == -1 ? "All" : m_LayerFilter.ToString())}"); using (new GUILayout.HorizontalScope()) { using (new GUILayout.VerticalScope(k_LayerPanelWidthOption)) { - DrawColors(); + DrawFilters(); } - using (var scrollView = new GUILayout.ScrollViewScope(m_FolderTreeScrollPosition)) + using (new GUILayout.VerticalScope()) { - m_FolderTreeScrollPosition = scrollView.scrollPosition; - m_ParentFolder.Draw(k_ProjectFolderName, m_LayerFilter); + using (new EditorGUI.DisabledScope(m_LayerWithNoName == k_InvalidLayer)) + { + m_IncludeLayerMaskFields = EditorGUILayout.Toggle(k_IncludeLayerMaskFieldsGUIContent, m_IncludeLayerMaskFields); + } + + using (var scrollView = new GUILayout.ScrollViewScope(m_FolderTreeScrollPosition)) + { + m_FolderTreeScrollPosition = scrollView.scrollPosition; + m_ParentFolder.Draw(k_ProjectFolderName, m_LayerToName, m_LayerFilter, m_IncludeLayerMaskFields); + } } } } + + if (m_ScanCount > 0 && m_ScanCount - m_ScanProgress > 0) + { + var rect = GUILayoutUtility.GetRect(0, float.PositiveInfinity, k_ProgressBarHeight, k_ProgressBarHeight); + EditorGUI.ProgressBar(rect, (float)m_ScanProgress / m_ScanCount, $"{m_ScanProgress} / {m_ScanCount}"); + } } /// /// Draw a list of unique layers. /// - void DrawColors() + void DrawFilters() { - GUILayout.Label($"{m_PrefabsByLayer.Count} Used Layers"); - if (GUILayout.Button("All")) - m_LayerFilter = -1; + var count = m_ParentFolder.GetCount(k_InvalidLayer, m_IncludeLayerMaskFields); + var style = m_LayerFilter == k_InvalidLayer ? Styles.ActiveFilterButton : Styles.InactiveFilterButton; + if (GUILayout.Button($"All ({count})", style)) + m_LayerFilter = k_InvalidLayer; using (var scrollView = new GUILayout.ScrollViewScope(m_ColorListScrollPosition)) { m_ColorListScrollPosition = scrollView.scrollPosition; - foreach (var kvp in m_PrefabsByLayer) + foreach (var kvp in m_LayerToName) { - var layer = kvp.Key; - if (GUILayout.Button($"Layer {layer} ({kvp.Value.prefabs.Count})")) + var layer = kvp.Key; + + // Skip the default layer + if (layer == 0) + continue; + + m_LayerToName.TryGetValue(layer, out var layerName); + if (string.IsNullOrEmpty(layerName)) + layerName = layer.ToString(); + + count = 0; + if (m_FilterRows.TryGetValue(layer, out var filterRow)) + count = m_IncludeLayerMaskFields ? filterRow.AllUsers.Count : filterRow.UsersWithoutLayerMasks.Count; + + style = m_LayerFilter == layer ? Styles.ActiveFilterButton : Styles.InactiveFilterButton; + if (GUILayout.Button($"{layer}: {layerName} ({count})", style)) m_LayerFilter = layer; } } @@ -337,6 +465,7 @@ void UpdateScan() } m_ParentFolder.SortContentsRecursively(); + Repaint(); } /// @@ -347,6 +476,7 @@ void UpdateScan() IEnumerator ProcessScan(Dictionary prefabAssets) { m_ScanCount = prefabAssets.Count; + m_ScanProgress = 0; foreach (var kvp in prefabAssets) { FindLayerUsersInPrefab(kvp.Key, kvp.Value); @@ -360,24 +490,34 @@ IEnumerator ProcessScan(Dictionary prefabAssets) void FindLayerUsersInPrefab(string path, GameObject prefabAsset) { - var layers = new SortedSet(); - FindLayerUsersRecursively(prefabAsset, layers); - if (layers.Count > 0) + var gameObjectLayers = new SortedSet(); + var layerMaskLayers = new SortedSet(); + FindLayerUsersRecursively(prefabAsset, gameObjectLayers, layerMaskLayers); + if (gameObjectLayers.Count > 0 || layerMaskLayers.Count > 0) { - m_ParentFolder.AddPrefabAtPath(path, prefabAsset, layers); - foreach (var layer in layers) + m_ParentFolder.AddPrefabAtPath(path, prefabAsset, gameObjectLayers, layerMaskLayers); + k_LayerUnionHashSet.Clear(); + k_LayerUnionHashSet.UnionWith(gameObjectLayers); + k_LayerUnionHashSet.UnionWith(layerMaskLayers); + foreach (var layer in k_LayerUnionHashSet) + { + var filterRow = GetOrCreatePrefabHashSetForLayer(layer); + filterRow.AllUsers.Add(prefabAsset); + } + + foreach (var layer in gameObjectLayers) { - var layerRow = GetOrCreateRowForColor(layer); - layerRow.prefabs.Add(prefabAsset); + var filterRow = GetOrCreatePrefabHashSetForLayer(layer); + filterRow.UsersWithoutLayerMasks.Add(prefabAsset); } } } - void FindLayerUsersRecursively(GameObject gameObject, SortedSet layers) + void FindLayerUsersRecursively(GameObject gameObject, SortedSet gameObjectLayers, SortedSet layerMaskLayers) { var layer = gameObject.layer; if (layer != 0) - layers.Add(layer); + gameObjectLayers.Add(layer); // GetComponents will clear the list, so we don't have to gameObject.GetComponents(k_Components); @@ -390,7 +530,7 @@ void FindLayerUsersRecursively(GameObject gameObject, SortedSet layers) if (iterator.propertyType != SerializedPropertyType.LayerMask) continue; - GetLayersFromLayerMask(layers, iterator.intValue); + GetLayersFromLayerMask(layerMaskLayers, iterator.intValue); } } @@ -399,25 +539,34 @@ void FindLayerUsersRecursively(GameObject gameObject, SortedSet layers) foreach (Transform child in gameObject.transform) { - FindLayerUsersRecursively(child.gameObject, layers); + FindLayerUsersRecursively(child.gameObject, gameObjectLayers, layerMaskLayers); } } void GetLayersFromLayerMask(SortedSet layers, int layerMask) { - // If layer 0 is in the mask, assume that "on" is the default, meaning that a layer that is not the mask - // counts as "used". Otherwise, if layer 0 is not in the mask, layers that are in the mask count as "used" - var defaultValue = (layerMask & 1) != 0; + // If all layers are named, it is not possible to infer whether layer mask fields "use" a layer + if (m_LayerWithNoName == k_InvalidLayer) + return; - // Exclude the special cases where every layer except default is included or excluded - if (layerMask == -2 || layerMask == 1) + // Exclude the special cases where every layer is included or excluded + if (layerMask == k_InvalidLayer || layerMask == 0) return; - // Skip layer 0 since we only want non-default layers - var count = m_LayersWithNames.Count; - for (var i = 1; i < count; i++) + // Depending on whether or not the mask started out as "Everything" or "Nothing", a layer will count as "used" when the user toggles its state. + // We use the layer without a name to check whether or not the starting point is "Everything" or "Nothing." If this layer's bit is 0, we assume + // the mask started with "Nothing." Otherwise, if its bit is 1, we assume the mask started with "Everything." + var defaultValue = (layerMask & 1 << m_LayerWithNoName) != 0; + + foreach (var kvp in m_LayerToName) { - var layer = m_LayersWithNames[i]; + var layer = kvp.Key; + + // Skip layer 0 since we only want non-default layers + if (layer == 0) + continue; + + // We compare (using xor) this layer's bit value with the default value. If they are different, the layer counts as "used." if ((layerMask & (1 << layer)) != 0 ^ defaultValue) layers.Add(layer); } @@ -432,16 +581,24 @@ void Scan() if (guids == null || guids.Length == 0) return; - m_PrefabsByLayer.Clear(); + m_FilterRows.Clear(); m_ParentFolder.Clear(); - m_LayersWithNames.Clear(); + m_LayerWithNoName = k_InvalidLayer; + m_LayerToName.Clear(); for (var i = 0; i < 32; i++) { - if (!string.IsNullOrEmpty(LayerMask.LayerToName(i))) - m_LayersWithNames.Add(i); + var layerName = LayerMask.LayerToName(i); + if (!string.IsNullOrEmpty(layerName)) + m_LayerToName.Add(i, layerName); + else + m_LayerWithNoName = i; } + // LayerMask field scanning requires at least one layer without a name + if (m_LayerWithNoName == k_InvalidLayer) + m_IncludeLayerMaskFields = false; + var prefabAssets = new Dictionary(); foreach (var guid in guids) { @@ -467,18 +624,23 @@ void Scan() } /// - /// Get or create a for a given layer value. + /// Get or create a for a given layer value. /// /// The layer value to use for this row. - /// The layer row for the layer value. - LayerRow GetOrCreateRowForColor(int layer) + /// The row for the layer value. + FilterRow GetOrCreatePrefabHashSetForLayer(int layer) { - if (m_PrefabsByLayer.TryGetValue(layer, out var row)) - return row; + if (m_FilterRows.TryGetValue(layer, out var filterRow)) + return filterRow; + + filterRow = new FilterRow + { + AllUsers = new HashSet(), + UsersWithoutLayerMasks = new HashSet() + }; - row = new LayerRow(); - m_PrefabsByLayer[layer] = row; - return row; + m_FilterRows[layer] = filterRow; + return filterRow; } } } From 6ef0094c62a124d6e6aec5f32210102af51e9d79 Mon Sep 17 00:00:00 2001 From: Matt Schoen Date: Sun, 29 May 2022 15:48:37 -0700 Subject: [PATCH 03/14] Display child GameObjects and components which use layers within each prefab; Cleanup and optimization --- Editor/PrefabLayerUsers.cs | 395 +++++++++++++++++++++++++++++-------- 1 file changed, 308 insertions(+), 87 deletions(-) diff --git a/Editor/PrefabLayerUsers.cs b/Editor/PrefabLayerUsers.cs index 1cb8ca9..6266598 100644 --- a/Editor/PrefabLayerUsers.cs +++ b/Editor/PrefabLayerUsers.cs @@ -1,10 +1,10 @@ -using System; -using System.Collections; +using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Text; using UnityEditor; +using UnityEditor.Experimental.SceneManagement; using UnityEngine; using Debug = UnityEngine.Debug; @@ -29,16 +29,13 @@ class Folder const int k_SeparatorLineHeight = 1; readonly SortedDictionary m_Subfolders = new SortedDictionary(); - readonly List<(string, GameObject, SortedSet, SortedSet)> m_Prefabs = new List<(string, GameObject, SortedSet, SortedSet)>(); + readonly List m_Prefabs = new List(); readonly SortedDictionary m_TotalCountPerLayer = new SortedDictionary(); readonly SortedDictionary m_TotalWithoutLayerMasksPerLayer = new SortedDictionary(); int m_TotalCount; int m_TotalWithoutLayerMasks; bool m_Visible; - // Local method use only -- created here to reduce garbage collection. Collections must be cleared before use - static readonly StringBuilder k_StringBuilder = new StringBuilder(); - /// /// Clear the contents of this container. /// @@ -55,14 +52,10 @@ public void Clear() /// /// Add a texture to this folder at a given path. /// - /// The path of the texture. - /// The prefab to add. - /// List of layers used by GameObjects in this prefab - /// List of layers used by LayerMask fields in this prefab - public void AddPrefabAtPath(string path, GameObject prefabAsset, SortedSet gameObjectLayers, SortedSet layerMaskLayers) + public void AddPrefab(PrefabRow prefabRow) { - var folder = GetOrCreateFolderForAssetPath(path, gameObjectLayers, layerMaskLayers); - folder.m_Prefabs.Add((path, prefabAsset, gameObjectLayers, layerMaskLayers)); + var folder = GetOrCreateFolderForAssetPath(prefabRow); + folder.m_Prefabs.Add(prefabRow); } /// @@ -72,14 +65,14 @@ public void AddPrefabAtPath(string path, GameObject prefabAsset, SortedSet /// Every time a folder is accessed, its Count property is incremented to indicate that it contains one /// more solid color texture. /// - /// Path to a solid color texture relative to this folder. - /// The GameObject layers for the object which will be added to this folder when it is created (used to aggregate layer counts) - /// The LayerMask layers for the object which will be added to this folder when it is created (used to aggregate layer counts) + /// A struct containing the prefab asset reference and its metadata /// The folder object corresponding to the folder containing the texture at the given path. - Folder GetOrCreateFolderForAssetPath(string path, SortedSet gameObjectLayers, SortedSet layerMaskLayers) + Folder GetOrCreateFolderForAssetPath(PrefabRow prefabRow) { - var directories = path.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + var directories = prefabRow.Path.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); var folder = this; + var gameObjectLayers = prefabRow.GameObjectLayers; + var layerMaskLayers = prefabRow.LayerMaskLayers; folder.AggregateCount(gameObjectLayers, layerMaskLayers); var length = directories.Length - 1; for (var i = 0; i < length; i++) @@ -134,12 +127,13 @@ public void Draw(string name, Dictionary layerToName, int layerFilt } var showedPrefab = false; - foreach (var (_, prefab, gameObjectLayers, layerMaskLayers) in m_Prefabs) + foreach (var prefabRow in m_Prefabs) { + var gameObjectLayers = prefabRow.GameObjectLayers; + var layerMaskLayers = prefabRow.LayerMaskLayers; if (layerFilter == k_InvalidLayer || gameObjectLayers.Contains(layerFilter) || layerMaskLayers.Contains(layerFilter)) { - layerNameList = GetLayerNameList(gameObjectLayers, layerMaskLayers, layerToName, layerFilter, includeLayerMaskFields); - EditorGUILayout.ObjectField($"{prefab.name} ({layerNameList})", prefab, typeof(GameObject), false); + prefabRow.Draw(layerToName, layerFilter, includeLayerMaskFields); showedPrefab = true; } } @@ -149,40 +143,6 @@ public void Draw(string name, Dictionary layerToName, int layerFilt } } - static string GetLayerNameList(IEnumerable gameObjectLayers, IEnumerable layerMaskLayers, - Dictionary layerToName, int layerFilter = k_InvalidLayer, bool includeLayerMaskFields = true) - { - if (layerFilter >= 0) - { - layerToName.TryGetValue(layerFilter, out var layerName); - if (string.IsNullOrEmpty(layerName)) - layerName = layerFilter.ToString(); - - return layerName; - } - - k_StringBuilder.Length = 0; - k_LayerUnionHashSet.Clear(); - k_LayerUnionHashSet.UnionWith(gameObjectLayers); - if (includeLayerMaskFields) - k_LayerUnionHashSet.UnionWith(layerMaskLayers); - - foreach (var layer in k_LayerUnionHashSet) - { - layerToName.TryGetValue(layer, out var layerName); - if (string.IsNullOrEmpty(layerName)) - layerName = layer.ToString(); - - k_StringBuilder.Append($"{layerName}, "); - } - - // Remove the last ", ". If we didn't add any layers, the StringBuilder will be empty so skip this step - if (k_StringBuilder.Length >= 2) - k_StringBuilder.Length -= 2; - - return k_StringBuilder.ToString(); - } - /// /// Draw a separator line. /// @@ -216,7 +176,7 @@ void SetVisibleRecursively(bool visible) /// public void SortContentsRecursively() { - m_Prefabs.Sort((a, b) => a.Item2.name.CompareTo(b.Item2.name)); + m_Prefabs.Sort((a, b) => a.PrefabAsset.name.CompareTo(b.PrefabAsset.name)); foreach (var kvp in m_Subfolders) { kvp.Value.SortContentsRecursively(); @@ -270,6 +230,132 @@ void AggregateCount(SortedSet gameObjectLayers, SortedSet layerMaskLay } } + class PrefabRow + { + public string Path; + public GameObject PrefabAsset; + public SortedSet GameObjectLayers; + public SortedSet LayerMaskLayers; + public List LayerUsers; + public int LayerUsersWithoutLayerMasks; + + bool m_Expanded; + + public void Draw(Dictionary layerToName, int layerFilter = k_InvalidLayer, bool includeLayerMaskFields = true) + { + var layerNameList = GetLayerNameList(GameObjectLayers, LayerMaskLayers, layerToName, layerFilter, includeLayerMaskFields); + var label = $"{PrefabAsset.name} {{{layerNameList}}}"; + if (includeLayerMaskFields && LayerUsers.Count > 0 || LayerUsersWithoutLayerMasks > 0) + { + m_Expanded = EditorGUILayout.Foldout(m_Expanded, label, true); + EditorGUILayout.ObjectField(PrefabAsset, typeof(GameObject), false); + } + else + { + EditorGUILayout.ObjectField(label, PrefabAsset, typeof(GameObject), false); + } + + if (!m_Expanded) + return; + + using (new EditorGUI.IndentLevelScope()) + { + foreach (var layerUser in LayerUsers) + { + if (layerFilter != k_InvalidLayer) + { + var gameObjectLayerMatchesFilter = layerUser.PrefabGameObject.layer == layerFilter; + var layerMasksMatchFilter = includeLayerMaskFields && layerUser.LayerMaskLayers.Contains(layerFilter); + if (!(gameObjectLayerMatchesFilter || layerMasksMatchFilter)) + continue; + } + + layerUser.Draw(layerToName, layerFilter, includeLayerMaskFields); + } + } + } + } + + class GameObjectRow + { + public string TransformPath; + public GameObject PrefabGameObject; + public List LayerMaskComponents; + public SortedSet LayerMaskLayers; + + bool m_Expanded; + + public void Draw(Dictionary layerToName, int layerFilter = k_InvalidLayer, bool includeLayerMaskFields = true) + { + var layer = PrefabGameObject.layer; + var layerName = GetLayerNameString(layerToName, layer); + + k_StringBuilder.Length = 0; + k_StringBuilder.Append($"{TransformPath} - Layer: {layerName}"); + if (includeLayerMaskFields && LayerMaskLayers.Count > 0) + { + var layerNameList = GetLayerNameList(LayerMaskLayers, layerToName, layerFilter); + k_StringBuilder.Append($" LayerMasks:{{{layerNameList}}}"); + } + + var label = k_StringBuilder.ToString(); + + if (includeLayerMaskFields && GetComponentCount(layerFilter) > 0) + { + m_Expanded = EditorGUILayout.Foldout(m_Expanded, label, true); + EditorGUILayout.ObjectField(PrefabGameObject, typeof(GameObject), true); + } + else + { + EditorGUILayout.ObjectField(label, PrefabGameObject, typeof(GameObject), true); + } + + if (!m_Expanded || !includeLayerMaskFields) + return; + + using (new EditorGUI.IndentLevelScope()) + { + foreach (var component in LayerMaskComponents) + { + if (layerFilter != k_InvalidLayer && !component.UsedLayers.Contains(layerFilter)) + continue; + + component.Draw(layerToName, layerFilter); + } + } + } + + int GetComponentCount(int layerFilter = k_InvalidLayer) + { + if (layerFilter == k_InvalidLayer) + return LayerMaskComponents.Count; + + var count = 0; + foreach (var component in LayerMaskComponents) + { + if (component.UsedLayers.Contains(layerFilter)) + count++; + } + + return count; + } + } + + struct ComponentRow + { + public Component PrefabComponent; + public SortedSet UsedLayers; + + public void Draw(Dictionary layerToName, int layerFilter = k_InvalidLayer) + { + using (new GUILayout.HorizontalScope()) + { + var layerNameList = GetLayerNameList(UsedLayers, layerToName, layerFilter); + EditorGUILayout.ObjectField($"{PrefabComponent.name} ({PrefabComponent.GetType().Name}) {{{layerNameList}}}", PrefabComponent, typeof(Component), true); + } + } + } + static class Styles { internal static readonly GUIStyle ActiveFilterButton = new GUIStyle(GUI.skin.button) @@ -296,15 +382,16 @@ static class Styles }; } - struct FilterRow + class FilterRow { - public HashSet AllUsers; - public HashSet UsersWithoutLayerMasks; + public readonly HashSet AllUsers = new HashSet(); + public readonly HashSet UsersWithoutLayerMasks = new HashSet(); } const string k_NoMissingReferences = "No prefabs using a non-default layer"; const string k_ProjectFolderName = "Project"; const int k_FilterPanelWidth = 180; + const int k_ObjectFieldWidth = 150; const string k_WindowTitle = "Prefab Layer Users"; const string k_Instructions = "Click the Scan button to scan your project for users of non-default layers. WARNING: " + "This will load every prefab in your project. For large projects, this may take a long time and/or crash the Editor."; @@ -317,6 +404,7 @@ struct FilterRow "Include layers from layer mask fields in the results. This is only possible if there is at least one layer without a name."); static readonly GUIContent k_ScanGUIContent = new GUIContent("Scan", "Scan the project for users of non-default layers"); + static readonly GUIContent k_CancelGUIContent = new GUIContent("Cancel", "Cancel the current scan"); static readonly GUILayoutOption k_LayerPanelWidthOption = GUILayout.Width(k_FilterPanelWidth); static readonly Vector2 k_MinSize = new Vector2(400, 200); @@ -340,13 +428,15 @@ struct FilterRow int m_LayerFilter = k_InvalidLayer; // Local method use only -- created here to reduce garbage collection. Collections must be cleared before use + static readonly StringBuilder k_StringBuilder = new StringBuilder(4096); static readonly List k_Components = new List(); static readonly SortedSet k_LayerUnionHashSet = new SortedSet(); + static readonly Stack k_TransformPathStack = new Stack(); /// /// Initialize the window /// - [MenuItem("Window/SuperScience/Layer Users")] + [MenuItem("Window/SuperScience/Prefab Layer Users")] static void Init() { GetWindow(k_WindowTitle).Show(); @@ -359,10 +449,25 @@ void OnEnable() m_ScanProgress = 0; } + void OnDisable() + { + m_ScanEnumerator = null; + } + void OnGUI() { - if (GUILayout.Button(k_ScanGUIContent)) - Scan(); + EditorGUIUtility.labelWidth = position.width - k_FilterPanelWidth - k_ObjectFieldWidth; + + if (m_ScanEnumerator == null) + { + if (GUILayout.Button(k_ScanGUIContent)) + Scan(); + } + else + { + if (GUILayout.Button(k_CancelGUIContent)) + m_ScanEnumerator = null; + } // If m_LayerToName hasn't been set up, we haven't scanned yet // This dictionary will always at least include the built-in layer names @@ -422,7 +527,7 @@ void DrawFilters() using (var scrollView = new GUILayout.ScrollViewScope(m_ColorListScrollPosition)) { m_ColorListScrollPosition = scrollView.scrollPosition; - foreach (var kvp in m_LayerToName) + foreach (var kvp in m_FilterRows) { var layer = kvp.Key; @@ -430,14 +535,11 @@ void DrawFilters() if (layer == 0) continue; - m_LayerToName.TryGetValue(layer, out var layerName); - if (string.IsNullOrEmpty(layerName)) - layerName = layer.ToString(); - count = 0; if (m_FilterRows.TryGetValue(layer, out var filterRow)) count = m_IncludeLayerMaskFields ? filterRow.AllUsers.Count : filterRow.UsersWithoutLayerMasks.Count; + var layerName = GetLayerNameString(m_LayerToName, layer); style = m_LayerFilter == layer ? Styles.ActiveFilterButton : Styles.InactiveFilterButton; if (GUILayout.Button($"{layer}: {layerName} ({count})", style)) m_LayerFilter = layer; @@ -473,13 +575,13 @@ void UpdateScan() /// /// Prefab assets to scan. /// IEnumerator used to run the coroutine. - IEnumerator ProcessScan(Dictionary prefabAssets) + IEnumerator ProcessScan(List<(string, GameObject)> prefabAssets) { m_ScanCount = prefabAssets.Count; m_ScanProgress = 0; - foreach (var kvp in prefabAssets) + foreach (var (path, prefabAsset) in prefabAssets) { - FindLayerUsersInPrefab(kvp.Key, kvp.Value); + FindLayerUsersInPrefab(path, prefabAsset); m_ScanProgress++; yield return null; } @@ -492,10 +594,28 @@ void FindLayerUsersInPrefab(string path, GameObject prefabAsset) { var gameObjectLayers = new SortedSet(); var layerMaskLayers = new SortedSet(); - FindLayerUsersRecursively(prefabAsset, gameObjectLayers, layerMaskLayers); + var layerUsers = new List(); + var layerUsersWithoutLayerMasks = 0; + + var prefabRoot = prefabAsset; + var prefabStage = PrefabStageUtility.GetCurrentPrefabStage(); + if (prefabStage != null && prefabStage.assetPath == path) + prefabRoot = prefabStage.prefabContentsRoot; + + FindLayerUsersRecursively(prefabRoot, gameObjectLayers, layerMaskLayers, layerUsers, ref layerUsersWithoutLayerMasks); if (gameObjectLayers.Count > 0 || layerMaskLayers.Count > 0) { - m_ParentFolder.AddPrefabAtPath(path, prefabAsset, gameObjectLayers, layerMaskLayers); + var prefabRow = new PrefabRow + { + Path = path, + PrefabAsset = prefabAsset, + GameObjectLayers = gameObjectLayers, + LayerMaskLayers = layerMaskLayers, + LayerUsers = layerUsers, + LayerUsersWithoutLayerMasks = layerUsersWithoutLayerMasks + }; + + m_ParentFolder.AddPrefab(prefabRow); k_LayerUnionHashSet.Clear(); k_LayerUnionHashSet.UnionWith(gameObjectLayers); k_LayerUnionHashSet.UnionWith(layerMaskLayers); @@ -513,11 +633,19 @@ void FindLayerUsersInPrefab(string path, GameObject prefabAsset) } } - void FindLayerUsersRecursively(GameObject gameObject, SortedSet gameObjectLayers, SortedSet layerMaskLayers) + void FindLayerUsersRecursively(GameObject gameObject, SortedSet prefabGameObjectLayers, SortedSet prefabLayerMaskLayers, List layerUsers, ref int layerUsersWithoutLayerMasks) { + var isLayerUser = false; var layer = gameObject.layer; if (layer != 0) - gameObjectLayers.Add(layer); + { + isLayerUser = true; + prefabGameObjectLayers.Add(layer); + layerUsersWithoutLayerMasks++; + } + + var layerMaskLayers = new SortedSet(); + var componentRows = new List(); // GetComponents will clear the list, so we don't have to gameObject.GetComponents(k_Components); @@ -525,39 +653,66 @@ void FindLayerUsersRecursively(GameObject gameObject, SortedSet gameObjectL { var serializedObject = new SerializedObject(component); var iterator = serializedObject.GetIterator(); + var componentUsesLayers = false; + var usedLayers = new SortedSet(); while (iterator.Next(true)) { if (iterator.propertyType != SerializedPropertyType.LayerMask) continue; - GetLayersFromLayerMask(layerMaskLayers, iterator.intValue); + componentUsesLayers |= GetLayersFromLayerMask(usedLayers, iterator.intValue); + } + + isLayerUser |= componentUsesLayers; + + if (componentUsesLayers) + { + prefabLayerMaskLayers.UnionWith(usedLayers); + layerMaskLayers.UnionWith(usedLayers); + componentRows.Add(new ComponentRow + { + PrefabComponent = component, + UsedLayers = usedLayers + }); } } // Clear the list after we're done to avoid lingering references k_Components.Clear(); + if (isLayerUser) + { + layerUsers.Add(new GameObjectRow + { + TransformPath = GetTransformPath(gameObject.transform), + PrefabGameObject = gameObject, + LayerMaskComponents = componentRows, + LayerMaskLayers = layerMaskLayers + }); + } + foreach (Transform child in gameObject.transform) { - FindLayerUsersRecursively(child.gameObject, gameObjectLayers, layerMaskLayers); + FindLayerUsersRecursively(child.gameObject, prefabGameObjectLayers, prefabLayerMaskLayers, layerUsers, ref layerUsersWithoutLayerMasks); } } - void GetLayersFromLayerMask(SortedSet layers, int layerMask) + bool GetLayersFromLayerMask(SortedSet layers, int layerMask) { // If all layers are named, it is not possible to infer whether layer mask fields "use" a layer if (m_LayerWithNoName == k_InvalidLayer) - return; + return false; // Exclude the special cases where every layer is included or excluded if (layerMask == k_InvalidLayer || layerMask == 0) - return; + return false; // Depending on whether or not the mask started out as "Everything" or "Nothing", a layer will count as "used" when the user toggles its state. // We use the layer without a name to check whether or not the starting point is "Everything" or "Nothing." If this layer's bit is 0, we assume // the mask started with "Nothing." Otherwise, if its bit is 1, we assume the mask started with "Everything." var defaultValue = (layerMask & 1 << m_LayerWithNoName) != 0; + var isLayerMaskUser = false; foreach (var kvp in m_LayerToName) { var layer = kvp.Key; @@ -568,8 +723,13 @@ void GetLayersFromLayerMask(SortedSet layers, int layerMask) // We compare (using xor) this layer's bit value with the default value. If they are different, the layer counts as "used." if ((layerMask & (1 << layer)) != 0 ^ defaultValue) + { + isLayerMaskUser = true; layers.Add(layer); + } } + + return isLayerMaskUser; } /// @@ -590,16 +750,21 @@ void Scan() { var layerName = LayerMask.LayerToName(i); if (!string.IsNullOrEmpty(layerName)) + { m_LayerToName.Add(i, layerName); + m_FilterRows.Add(i, new FilterRow()); + } else + { m_LayerWithNoName = i; + } } // LayerMask field scanning requires at least one layer without a name if (m_LayerWithNoName == k_InvalidLayer) m_IncludeLayerMaskFields = false; - var prefabAssets = new Dictionary(); + var prefabAssets = new List<(string, GameObject)>(); foreach (var guid in guids) { var path = AssetDatabase.GUIDToAssetPath(guid); @@ -616,7 +781,7 @@ void Scan() continue; } - prefabAssets.Add(path, prefab); + prefabAssets.Add((path, prefab)); } m_ScanEnumerator = ProcessScan(prefabAssets); @@ -633,14 +798,70 @@ FilterRow GetOrCreatePrefabHashSetForLayer(int layer) if (m_FilterRows.TryGetValue(layer, out var filterRow)) return filterRow; - filterRow = new FilterRow - { - AllUsers = new HashSet(), - UsersWithoutLayerMasks = new HashSet() - }; - + filterRow = new FilterRow(); m_FilterRows[layer] = filterRow; return filterRow; } + + static string GetLayerNameList(IEnumerable gameObjectLayers, IEnumerable layerMaskLayers, + Dictionary layerToName, int layerFilter = k_InvalidLayer, bool includeLayerMaskFields = true) + { + k_StringBuilder.Length = 0; + k_LayerUnionHashSet.Clear(); + k_LayerUnionHashSet.UnionWith(gameObjectLayers); + if (includeLayerMaskFields) + k_LayerUnionHashSet.UnionWith(layerMaskLayers); + + return GetLayerNameList(k_LayerUnionHashSet, layerToName, layerFilter); + } + + static string GetLayerNameList(IEnumerable layers, Dictionary layerToName, int layerFilter = k_InvalidLayer) + { + if (layerFilter >= 0) + return GetLayerNameString(layerToName, layerFilter); + + k_StringBuilder.Length = 0; + foreach (var layer in layers) + { + k_StringBuilder.Append($"{GetLayerNameString(layerToName, layer)}, "); + } + + // Remove the last ", ". If we didn't add any layers, the StringBuilder will be empty so skip this step + if (k_StringBuilder.Length >= 2) + k_StringBuilder.Length -= 2; + + return k_StringBuilder.ToString(); + } + + static string GetLayerNameString(Dictionary layerToName, int layer) + { + layerToName.TryGetValue(layer, out var layerName); + if (string.IsNullOrEmpty(layerName)) + layerName = layer.ToString(); + return layerName; + } + + static string GetTransformPath(Transform transform) + { + if (transform.parent == null) + return transform.name; + + k_TransformPathStack.Clear(); + while (transform != null) + { + k_TransformPathStack.Push(transform.name); + transform = transform.parent; + } + + k_StringBuilder.Length = 0; + while (k_TransformPathStack.Count > 0) + { + k_StringBuilder.Append(k_TransformPathStack.Pop()); + k_StringBuilder.Append("/"); + } + + k_StringBuilder.Length -= 1; + return k_StringBuilder.ToString(); + } } } From cc852c28205d883a6e997297804a2974aafdf587 Mon Sep 17 00:00:00 2001 From: Matt Schoen Date: Sun, 29 May 2022 17:38:57 -0700 Subject: [PATCH 04/14] Add PrefabTagUsers window --- Editor/PrefabLayerUsers.cs | 52 +- Editor/PrefabTagUsers.cs | 594 ++++++++++++++++++++++ Editor/PrefabTagUsers.cs.meta | 11 + Editor/Unity.Labs.SuperScience.Editor.api | 5 + 4 files changed, 636 insertions(+), 26 deletions(-) create mode 100644 Editor/PrefabTagUsers.cs create mode 100644 Editor/PrefabTagUsers.cs.meta diff --git a/Editor/PrefabLayerUsers.cs b/Editor/PrefabLayerUsers.cs index 6266598..d2927ed 100644 --- a/Editor/PrefabLayerUsers.cs +++ b/Editor/PrefabLayerUsers.cs @@ -11,20 +11,20 @@ namespace Unity.Labs.SuperScience { /// - /// Scans the project for textures comprised of a single solid color. - /// Use this utility to identify redundant textures, and textures which are larger than they need to be. + /// Scans the project for prefabs which use non-default layers. + /// Use this utility to track down where a certain layer is used. /// public class PrefabLayerUsers : EditorWindow { /// /// Tree structure for folder scan results. /// This is the root object for the project scan, and represents the results in a hierarchy that matches the - /// project's folder structure for an easy to read presentation of solid color textures. - /// When the Scan method encounters a texture, we initialize one of these using the asset path to determine where it belongs. + /// project's folder structure for an easy to read presentation of layer users. + /// When the Scan method encounters a layer user, we search the parent folder for one of these using the asset path to file it where it belongs. /// class Folder { - // TODO: Share code between this window and MissingProjectReferences + // TODO: Share code between this window and others that display a folder structure const int k_IndentAmount = 15; const int k_SeparatorLineHeight = 1; @@ -50,7 +50,7 @@ public void Clear() } /// - /// Add a texture to this folder at a given path. + /// Add a prefab to this folder at a given path. /// public void AddPrefab(PrefabRow prefabRow) { @@ -59,14 +59,13 @@ public void AddPrefab(PrefabRow prefabRow) } /// - /// Get the Folder object which corresponds to the given path. + /// Get the Folder object which corresponds to the path of a given . /// If this is the first asset encountered for a given folder, create a chain of folder objects /// rooted with this one and return the folder at the end of that chain. - /// Every time a folder is accessed, its Count property is incremented to indicate that it contains one - /// more solid color texture. + /// Every time a folder is accessed, its Count property is incremented to indicate that it contains one more layer user. /// /// A struct containing the prefab asset reference and its metadata - /// The folder object corresponding to the folder containing the texture at the given path. + /// The folder object corresponding to the folder containing the layer users at the given path. Folder GetOrCreateFolderForAssetPath(PrefabRow prefabRow) { var directories = prefabRow.Path.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); @@ -356,6 +355,12 @@ public void Draw(Dictionary layerToName, int layerFilter = k_Invali } } + class FilterRow + { + public readonly HashSet AllUsers = new HashSet(); + public readonly HashSet UsersWithoutLayerMasks = new HashSet(); + } + static class Styles { internal static readonly GUIStyle ActiveFilterButton = new GUIStyle(GUI.skin.button) @@ -382,17 +387,12 @@ static class Styles }; } - class FilterRow - { - public readonly HashSet AllUsers = new HashSet(); - public readonly HashSet UsersWithoutLayerMasks = new HashSet(); - } - + const string k_MenuItemName = "Window/SuperScience/Prefab Layer Users"; + const string k_WindowTitle = "Prefab Layer Users"; const string k_NoMissingReferences = "No prefabs using a non-default layer"; const string k_ProjectFolderName = "Project"; const int k_FilterPanelWidth = 180; const int k_ObjectFieldWidth = 150; - const string k_WindowTitle = "Prefab Layer Users"; const string k_Instructions = "Click the Scan button to scan your project for users of non-default layers. WARNING: " + "This will load every prefab in your project. For large projects, this may take a long time and/or crash the Editor."; const string k_ScanFilter = "t:Prefab"; @@ -405,7 +405,7 @@ class FilterRow static readonly GUIContent k_ScanGUIContent = new GUIContent("Scan", "Scan the project for users of non-default layers"); static readonly GUIContent k_CancelGUIContent = new GUIContent("Cancel", "Cancel the current scan"); - static readonly GUILayoutOption k_LayerPanelWidthOption = GUILayout.Width(k_FilterPanelWidth); + static readonly GUILayoutOption k_FilterPanelWidthOption = GUILayout.Width(k_FilterPanelWidth); static readonly Vector2 k_MinSize = new Vector2(400, 200); static readonly Stopwatch k_StopWatch = new Stopwatch(); @@ -436,7 +436,7 @@ class FilterRow /// /// Initialize the window /// - [MenuItem("Window/SuperScience/Prefab Layer Users")] + [MenuItem(k_MenuItemName)] static void Init() { GetWindow(k_WindowTitle).Show(); @@ -486,7 +486,7 @@ void OnGUI() { using (new GUILayout.HorizontalScope()) { - using (new GUILayout.VerticalScope(k_LayerPanelWidthOption)) + using (new GUILayout.VerticalScope(k_FilterPanelWidthOption)) { DrawFilters(); } @@ -515,7 +515,7 @@ void OnGUI() } /// - /// Draw a list of unique layers. + /// Draw a list buttons for filtering based on layer. /// void DrawFilters() { @@ -621,13 +621,13 @@ void FindLayerUsersInPrefab(string path, GameObject prefabAsset) k_LayerUnionHashSet.UnionWith(layerMaskLayers); foreach (var layer in k_LayerUnionHashSet) { - var filterRow = GetOrCreatePrefabHashSetForLayer(layer); + var filterRow = GetOrCreateFilterRowSetForLayer(layer); filterRow.AllUsers.Add(prefabAsset); } foreach (var layer in gameObjectLayers) { - var filterRow = GetOrCreatePrefabHashSetForLayer(layer); + var filterRow = GetOrCreateFilterRowSetForLayer(layer); filterRow.UsersWithoutLayerMasks.Add(prefabAsset); } } @@ -733,7 +733,7 @@ bool GetLayersFromLayerMask(SortedSet layers, int layerMask) } /// - /// Scan the project for solid color textures and populate the data structures for UI. + /// Scan the project for layer users and populate the data structures for UI. /// void Scan() { @@ -793,7 +793,7 @@ void Scan() /// /// The layer value to use for this row. /// The row for the layer value. - FilterRow GetOrCreatePrefabHashSetForLayer(int layer) + FilterRow GetOrCreateFilterRowSetForLayer(int layer) { if (m_FilterRows.TryGetValue(layer, out var filterRow)) return filterRow; @@ -806,7 +806,6 @@ FilterRow GetOrCreatePrefabHashSetForLayer(int layer) static string GetLayerNameList(IEnumerable gameObjectLayers, IEnumerable layerMaskLayers, Dictionary layerToName, int layerFilter = k_InvalidLayer, bool includeLayerMaskFields = true) { - k_StringBuilder.Length = 0; k_LayerUnionHashSet.Clear(); k_LayerUnionHashSet.UnionWith(gameObjectLayers); if (includeLayerMaskFields) @@ -838,6 +837,7 @@ static string GetLayerNameString(Dictionary layerToName, int layer) layerToName.TryGetValue(layer, out var layerName); if (string.IsNullOrEmpty(layerName)) layerName = layer.ToString(); + return layerName; } diff --git a/Editor/PrefabTagUsers.cs b/Editor/PrefabTagUsers.cs new file mode 100644 index 0000000..f762b56 --- /dev/null +++ b/Editor/PrefabTagUsers.cs @@ -0,0 +1,594 @@ +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Text; +using UnityEditor; +using UnityEditor.Experimental.SceneManagement; +using UnityEngine; +using Debug = UnityEngine.Debug; + +namespace Unity.Labs.SuperScience +{ + /// + /// Scans the project for prefabs which use tags. + /// Use this utility to track down where a certain tag is used. + /// + public class PrefabTagUsers : EditorWindow + { + /// + /// Tree structure for folder scan results. + /// This is the root object for the project scan, and represents the results in a hierarchy that matches the + /// project's folder structure for an easy to read presentation of tag users. + /// When the Scan method encounters a tag user, we search the parent folder for one of these using the asset path to file it where it belongs. + /// + class Folder + { + // TODO: Share code between this window and others that display a folder structure + const int k_IndentAmount = 15; + const int k_SeparatorLineHeight = 1; + + readonly SortedDictionary m_Subfolders = new SortedDictionary(); + readonly List m_Prefabs = new List(); + readonly SortedDictionary m_CountPerTag = new SortedDictionary(); + int m_TotalCount; + bool m_Visible; + + /// + /// Clear the contents of this container. + /// + public void Clear() + { + m_Subfolders.Clear(); + m_Prefabs.Clear(); + m_CountPerTag.Clear(); + m_TotalCount = 0; + } + + /// + /// Add a prefab to this folder at a given path. + /// + public void AddPrefab(PrefabRow prefabRow) + { + var folder = GetOrCreateFolderForAssetPath(prefabRow); + folder.m_Prefabs.Add(prefabRow); + } + + /// + /// Get the Folder object which corresponds to the path of a given . + /// If this is the first asset encountered for a given folder, create a chain of folder objects + /// rooted with this one and return the folder at the end of that chain. + /// Every time a folder is accessed, its Count property is incremented to indicate that it contains one more tag user. + /// + /// A object containing the prefab asset reference and its metadata + /// The folder object corresponding to the folder containing the tag users at the given path. + Folder GetOrCreateFolderForAssetPath(PrefabRow prefabRow) + { + var directories = prefabRow.Path.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + var folder = this; + folder.AggregateCount(prefabRow.Tags); + var length = directories.Length - 1; + for (var i = 0; i < length; i++) + { + var directory = directories[i]; + var subfolders = folder.m_Subfolders; + if (!subfolders.TryGetValue(directory, out var subfolder)) + { + subfolder = new Folder(); + subfolders[directory] = subfolder; + } + + folder = subfolder; + folder.AggregateCount(prefabRow.Tags); + } + + return folder; + } + + /// + /// Draw GUI for this Folder. + /// + /// The name of the folder. + /// (Optional) Tag used to filter results. + public void Draw(string name, string tagFilter = null) + { + var wasVisible = m_Visible; + var tagList = GetTagList(m_CountPerTag.Keys, tagFilter); + var label = $"{name}: {m_TotalCount} {{{tagList}}}"; + m_Visible = EditorGUILayout.Foldout(m_Visible, label, true); + + DrawLineSeparator(); + + // Hold alt to apply visibility state to all children (recursively) + if (m_Visible != wasVisible && Event.current.alt) + SetVisibleRecursively(m_Visible); + + if (!m_Visible) + return; + + using (new EditorGUI.IndentLevelScope()) + { + foreach (var kvp in m_Subfolders) + { + var folder = kvp.Value; + if (folder.GetCount(tagFilter) == 0) + continue; + + folder.Draw(kvp.Key, tagFilter); + } + + var showedPrefab = false; + foreach (var prefabRow in m_Prefabs) + { + var tags = prefabRow.Tags; + if (string.IsNullOrEmpty(tagFilter) || tags.Contains(tagFilter)) + { + prefabRow.Draw(tagFilter); + showedPrefab = true; + } + } + + if (showedPrefab) + DrawLineSeparator(); + } + } + + /// + /// Draw a separator line. + /// + static void DrawLineSeparator() + { + EditorGUILayout.Separator(); + using (new GUILayout.HorizontalScope()) + { + GUILayout.Space(EditorGUI.indentLevel * k_IndentAmount); + GUILayout.Box(GUIContent.none, Styles.LineStyle, GUILayout.Height(k_SeparatorLineHeight), GUILayout.ExpandWidth(true)); + } + + EditorGUILayout.Separator(); + } + + /// + /// Set the visibility state of this folder, its contents and their children and all of its subfolders and their contents and children. + /// + /// Whether this object and its children should be visible in the GUI. + void SetVisibleRecursively(bool visible) + { + m_Visible = visible; + foreach (var kvp in m_Subfolders) + { + kvp.Value.SetVisibleRecursively(visible); + } + } + + /// + /// Sort the contents of this folder and all subfolders by name. + /// + public void SortContentsRecursively() + { + m_Prefabs.Sort((a, b) => a.PrefabAsset.name.CompareTo(b.PrefabAsset.name)); + foreach (var kvp in m_Subfolders) + { + kvp.Value.SortContentsRecursively(); + } + } + + public int GetCount(string tagFilter = null) + { + if (string.IsNullOrEmpty(tagFilter)) + return m_TotalCount; + + m_CountPerTag.TryGetValue(tagFilter, out var count); + return count; + } + + void AggregateCount(SortedSet tags) + { + m_TotalCount++; + foreach (var tag in tags) + { + m_CountPerTag.TryGetValue(tag, out var count); + count++; + m_CountPerTag[tag] = count; + } + } + } + + class PrefabRow + { + public string Path; + public GameObject PrefabAsset; + public SortedSet Tags; + public List TagUsers; + + bool m_Expanded; + + public void Draw(string tagFilter = null) + { + var tagList = GetTagList(Tags, tagFilter); + var label = $"{PrefabAsset.name} {{{tagList}}}"; + if (TagUsers.Count > 0) + { + m_Expanded = EditorGUILayout.Foldout(m_Expanded, label, true); + EditorGUILayout.ObjectField(PrefabAsset, typeof(GameObject), false); + } + else + { + EditorGUILayout.ObjectField(label, PrefabAsset, typeof(GameObject), false); + } + + if (!m_Expanded) + return; + + using (new EditorGUI.IndentLevelScope()) + { + foreach (var user in TagUsers) + { + if (!string.IsNullOrEmpty(tagFilter) && !user.PrefabGameObject.CompareTag(tagFilter)) + continue; + + user.Draw(); + } + } + } + } + + struct GameObjectRow + { + public string TransformPath; + public GameObject PrefabGameObject; + + public void Draw() + { + EditorGUILayout.ObjectField($"{TransformPath} - Tag: {PrefabGameObject.tag}", PrefabGameObject, typeof(GameObject), true); + } + } + + static class Styles + { + internal static readonly GUIStyle ActiveFilterButton = new GUIStyle(GUI.skin.button) + { + alignment = TextAnchor.MiddleLeft, + fontStyle = FontStyle.Bold + }; + + internal static readonly GUIStyle InactiveFilterButton = new GUIStyle(GUI.skin.button) + { + alignment = TextAnchor.MiddleLeft + }; + + internal static readonly GUIStyle LineStyle = new GUIStyle + { + normal = new GUIStyleState + { +#if UNITY_2019_4_OR_NEWER + background = Texture2D.grayTexture +#else + background = Texture2D.whiteTexture +#endif + } + }; + } + + const string k_MenuItemName = "Window/SuperScience/Prefab Tag Users"; + const string k_WindowTitle = "Prefab Tag Users"; + const string k_NoMissingReferences = "No prefabs using any tags"; + const string k_ProjectFolderName = "Project"; + const int k_FilterPanelWidth = 180; + const int k_ObjectFieldWidth = 150; + const string k_Instructions = "Click the Scan button to scan your project for tag users. WARNING: " + + "This will load every prefab in your project. For large projects, this may take a long time and/or crash the Editor."; + const string k_ScanFilter = "t:Prefab"; + const int k_ProgressBarHeight = 15; + const int k_MaxScanUpdateTimeMilliseconds = 50; + const string k_UntaggedString = "Untagged"; + + static readonly GUIContent k_ScanGUIContent = new GUIContent("Scan", "Scan the project for tag users"); + static readonly GUIContent k_CancelGUIContent = new GUIContent("Cancel", "Cancel the current scan"); + static readonly GUILayoutOption k_FilterPanelWidthOption = GUILayout.Width(k_FilterPanelWidth); + static readonly Vector2 k_MinSize = new Vector2(400, 200); + + static readonly Stopwatch k_StopWatch = new Stopwatch(); + + Vector2 m_ColorListScrollPosition; + Vector2 m_FolderTreeScrollPosition; + readonly Folder m_ParentFolder = new Folder(); + readonly SortedDictionary> m_FilterRows = new SortedDictionary>(); + static readonly string[] k_ScanFolders = { "Assets", "Packages" }; + int m_ScanCount; + int m_ScanProgress; + IEnumerator m_ScanEnumerator; + bool m_Scanned; + + [SerializeField] + string m_TagFilter; + + // Local method use only -- created here to reduce garbage collection. Collections must be cleared before use + static readonly StringBuilder k_StringBuilder = new StringBuilder(4096); + static readonly Stack k_TransformPathStack = new Stack(); + + /// + /// Initialize the window + /// + [MenuItem(k_MenuItemName)] + static void Init() + { + GetWindow(k_WindowTitle).Show(); + } + + void OnEnable() + { + minSize = k_MinSize; + m_ScanCount = 0; + m_ScanProgress = 0; + m_Scanned = false; + } + + void OnDisable() + { + m_ScanEnumerator = null; + } + + void OnGUI() + { + EditorGUIUtility.labelWidth = position.width - k_FilterPanelWidth - k_ObjectFieldWidth; + + if (m_ScanEnumerator == null) + { + if (GUILayout.Button(k_ScanGUIContent)) + Scan(); + } + else + { + if (GUILayout.Button(k_CancelGUIContent)) + m_ScanEnumerator = null; + } + + if (!m_Scanned) + { + EditorGUILayout.HelpBox(k_Instructions, MessageType.Info); + GUIUtility.ExitGUI(); + return; + } + + if (m_ParentFolder.GetCount(m_TagFilter) == 0) + { + GUILayout.Label(k_NoMissingReferences); + } + else + { + using (new GUILayout.HorizontalScope()) + { + using (new GUILayout.VerticalScope(k_FilterPanelWidthOption)) + { + DrawFilters(); + } + + using (var scrollView = new GUILayout.ScrollViewScope(m_FolderTreeScrollPosition)) + { + m_FolderTreeScrollPosition = scrollView.scrollPosition; + m_ParentFolder.Draw(k_ProjectFolderName, m_TagFilter); + } + } + } + + if (m_ScanCount > 0 && m_ScanCount - m_ScanProgress > 0) + { + var rect = GUILayoutUtility.GetRect(0, float.PositiveInfinity, k_ProgressBarHeight, k_ProgressBarHeight); + EditorGUI.ProgressBar(rect, (float)m_ScanProgress / m_ScanCount, $"{m_ScanProgress} / {m_ScanCount}"); + } + } + + /// + /// Draw a list buttons for filtering based on tag. + /// + void DrawFilters() + { + var count = m_ParentFolder.GetCount(); + var style = string.IsNullOrEmpty(m_TagFilter) ? Styles.ActiveFilterButton : Styles.InactiveFilterButton; + if (GUILayout.Button($"All ({count})", style)) + m_TagFilter = null; + + using (var scrollView = new GUILayout.ScrollViewScope(m_ColorListScrollPosition)) + { + m_ColorListScrollPosition = scrollView.scrollPosition; + foreach (var kvp in m_FilterRows) + { + var tag = kvp.Key; + + count = 0; + if (m_FilterRows.TryGetValue(tag, out var filterRow)) + count = filterRow.Count; + + style = m_TagFilter == tag ? Styles.ActiveFilterButton : Styles.InactiveFilterButton; + if (GUILayout.Button($"{tag} ({count})", style)) + m_TagFilter = tag; + } + } + } + + /// + /// Update the current scan coroutine. + /// + void UpdateScan() + { + if (m_ScanEnumerator == null) + return; + + k_StopWatch.Reset(); + k_StopWatch.Start(); + + // Process as many steps as possible within a given time frame + while (m_ScanEnumerator.MoveNext()) + { + // Process for a maximum amount of time and early-out to keep the UI responsive + if (k_StopWatch.ElapsedMilliseconds > k_MaxScanUpdateTimeMilliseconds) + break; + } + + m_ParentFolder.SortContentsRecursively(); + Repaint(); + } + + /// + /// Coroutine for processing scan results. + /// + /// Prefab assets to scan. + /// IEnumerator used to run the coroutine. + IEnumerator ProcessScan(List<(string, GameObject)> prefabAssets) + { + m_Scanned = true; + m_ScanCount = prefabAssets.Count; + m_ScanProgress = 0; + foreach (var (path, prefabAsset) in prefabAssets) + { + FindTagUsersInPrefab(path, prefabAsset); + m_ScanProgress++; + yield return null; + } + + m_ScanEnumerator = null; + EditorApplication.update -= UpdateScan; + } + + void FindTagUsersInPrefab(string path, GameObject prefabAsset) + { + var tags = new SortedSet(); + var tagUsers = new List(); + + var prefabRoot = prefabAsset; + var prefabStage = PrefabStageUtility.GetCurrentPrefabStage(); + if (prefabStage != null && prefabStage.assetPath == path) + prefabRoot = prefabStage.prefabContentsRoot; + + FindTagUsersRecursively(prefabRoot, tags, tagUsers); + if (tags.Count > 0) + { + var prefabRow = new PrefabRow + { + Path = path, + PrefabAsset = prefabAsset, + Tags = tags, + TagUsers = tagUsers + }; + + m_ParentFolder.AddPrefab(prefabRow); + foreach (var tag in tags) + { + var prefabs = GetOrCreatePrefabHashSetForTag(tag); + prefabs.Add(prefabAsset); + } + } + } + + static void FindTagUsersRecursively(GameObject gameObject, SortedSet tags, List tagUsers) + { + var tag = gameObject.tag; + if (!gameObject.CompareTag(k_UntaggedString)) + { + tags.Add(tag); + tagUsers.Add(new GameObjectRow + { + TransformPath = GetTransformPath(gameObject.transform), + PrefabGameObject = gameObject + }); + } + + foreach (Transform child in gameObject.transform) + { + FindTagUsersRecursively(child.gameObject, tags, tagUsers); + } + } + + /// + /// Scan the project for tag users and populate the data structures for UI. + /// + void Scan() + { + var guids = AssetDatabase.FindAssets(k_ScanFilter, k_ScanFolders); + if (guids == null || guids.Length == 0) + return; + + m_FilterRows.Clear(); + m_ParentFolder.Clear(); + var prefabAssets = new List<(string, GameObject)>(); + foreach (var guid in guids) + { + var path = AssetDatabase.GUIDToAssetPath(guid); + if (string.IsNullOrEmpty(path)) + { + Debug.Log($"Could not convert {guid} to path"); + continue; + } + + var prefab = AssetDatabase.LoadAssetAtPath(path); + if (prefab == null) + { + Debug.LogWarning($"Could not load prefab at {path}"); + continue; + } + + prefabAssets.Add((path, prefab)); + } + + m_ScanEnumerator = ProcessScan(prefabAssets); + EditorApplication.update += UpdateScan; + } + + /// + /// Get or create a HashSet<GameObject> for a given tag. + /// + /// The tag to use for this row. + /// The row for the tag. + HashSet GetOrCreatePrefabHashSetForTag(string tag) + { + if (m_FilterRows.TryGetValue(tag, out var filterRow)) + return filterRow; + + filterRow = new HashSet(); + m_FilterRows[tag] = filterRow; + return filterRow; + } + + static string GetTagList(IEnumerable tags, string tagFilter = null) + { + if (!string.IsNullOrEmpty(tagFilter)) + return tagFilter; + + k_StringBuilder.Length = 0; + foreach (var tag in tags) + { + k_StringBuilder.Append($"{tag}, "); + } + + // Remove the last ", ". If we didn't add any tags, the StringBuilder will be empty so skip this step + if (k_StringBuilder.Length >= 2) + k_StringBuilder.Length -= 2; + + return k_StringBuilder.ToString(); + } + + static string GetTransformPath(Transform transform) + { + if (transform.parent == null) + return transform.name; + + k_TransformPathStack.Clear(); + while (transform != null) + { + k_TransformPathStack.Push(transform.name); + transform = transform.parent; + } + + k_StringBuilder.Length = 0; + while (k_TransformPathStack.Count > 0) + { + k_StringBuilder.Append(k_TransformPathStack.Pop()); + k_StringBuilder.Append("/"); + } + + k_StringBuilder.Length -= 1; + return k_StringBuilder.ToString(); + } + } +} diff --git a/Editor/PrefabTagUsers.cs.meta b/Editor/PrefabTagUsers.cs.meta new file mode 100644 index 0000000..5821642 --- /dev/null +++ b/Editor/PrefabTagUsers.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 22c63d32dfd469744a9c8864c8260297 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Unity.Labs.SuperScience.Editor.api b/Editor/Unity.Labs.SuperScience.Editor.api index fcf6565..5a90bf1 100644 --- a/Editor/Unity.Labs.SuperScience.Editor.api +++ b/Editor/Unity.Labs.SuperScience.Editor.api @@ -34,6 +34,11 @@ namespace Unity.Labs.SuperScience public PrefabLayerUsers() {} } + public class PrefabTagUsers : UnityEditor.EditorWindow + { + public PrefabTagUsers() {} + } + public class RunInEditHelper : UnityEditor.EditorWindow { public RunInEditHelper() {} From 5f6aad54bfba2603363c78018738336ff4b8bcf8 Mon Sep 17 00:00:00 2001 From: Matt Schoen Date: Mon, 30 May 2022 01:42:12 -0700 Subject: [PATCH 05/14] Refactor MisingReferenceWindow to avoid allocating and discarding lists or containers; Clean up Prefab Tag/Layer Users windows --- Editor/.plan | 2 + .../MissingProjectReferences.cs | 64 +++-- .../MissingReferencesWindow.cs | 237 +++++++++++++----- .../MissingSceneReferences.cs | 43 ++-- Editor/PrefabLayerUsers.cs | 28 +-- Editor/PrefabTagUsers.cs | 18 +- Runtime/Unity.Labs.SuperScience.api | 1 + 7 files changed, 266 insertions(+), 127 deletions(-) create mode 100644 Editor/.plan diff --git a/Editor/.plan b/Editor/.plan new file mode 100644 index 0000000..6de4c1f --- /dev/null +++ b/Editor/.plan @@ -0,0 +1,2 @@ +Update scene scanning to be async +Rename to ProjectAnalysisWindow to accomodate Prefab/Tag users and Solid Color Textures \ No newline at end of file diff --git a/Editor/MissingReferences/MissingProjectReferences.cs b/Editor/MissingReferences/MissingProjectReferences.cs index cf4f928..42d5393 100644 --- a/Editor/MissingReferences/MissingProjectReferences.cs +++ b/Editor/MissingReferences/MissingProjectReferences.cs @@ -33,12 +33,20 @@ class AssetContainer : MissingReferencesContainer const string k_SubAssetsLabelFormat = "Sub-assets: {0}"; readonly UnityObject m_Object; + readonly List m_SubAssets; + readonly List m_PropertiesWithMissingReferences; + bool m_SubAssetsVisible; - public readonly List SubAssets = new List(); - public readonly List PropertiesWithMissingReferences = new List(); public override UnityObject Object => m_Object; + AssetContainer(UnityObject unityObject, List subAssets, List propertiesWithMissingReferences) + { + m_Object = unityObject; + m_SubAssets = subAssets; + m_PropertiesWithMissingReferences = propertiesWithMissingReferences; + } + /// /// Initialize an AssetContainer to represent the given UnityObject /// This will scan the object for missing references and retain the information for display in @@ -47,33 +55,47 @@ class AssetContainer : MissingReferencesContainer /// The main UnityObject for this asset /// The path to this asset, for gathering sub-assets /// User-configurable options for this view - public AssetContainer(UnityObject unityObject, string path, Options options) + public static AssetContainer CreateIfNecessary(UnityObject unityObject, string path, Options options) { - m_Object = unityObject; - CheckForMissingReferences(unityObject, PropertiesWithMissingReferences, options); + var missingReferences = CheckForMissingReferences(unityObject, options); // Collect any sub-asset references + List subAssets = null; foreach (var asset in AssetDatabase.LoadAllAssetRepresentationsAtPath(path)) { if (asset is GameObject prefab) { - var gameObjectContainer = new GameObjectContainer(prefab, options); - if (gameObjectContainer.Count > 0) - SubAssets.Add(gameObjectContainer); + var gameObjectContainer = GameObjectContainer.CreateIfNecessary(prefab, options); + if (gameObjectContainer != null) + { + subAssets ??= new List(); + subAssets.Add(gameObjectContainer); + } } else { - var assetContainer = new AssetContainer(asset, options); - if (assetContainer.PropertiesWithMissingReferences.Count > 0) - SubAssets.Add(assetContainer); + var assetContainer = CreateIfNecessary(asset, options); + if (assetContainer != null) + { + subAssets ??= new List(); + subAssets.Add(assetContainer); + } } } + + if (missingReferences != null || subAssets != null) + return new AssetContainer(unityObject, subAssets, missingReferences); + + return null; } - AssetContainer(UnityObject unityObject, Options options) + static AssetContainer CreateIfNecessary(UnityObject unityObject, Options options) { - m_Object = unityObject; - CheckForMissingReferences(unityObject, PropertiesWithMissingReferences, options); + var missingReferences = CheckForMissingReferences(unityObject, options); + if (missingReferences != null) + return new AssetContainer(unityObject, null, missingReferences); + + return null; } /// @@ -83,9 +105,9 @@ public override void Draw() { using (new EditorGUI.IndentLevelScope()) { - DrawPropertiesWithMissingReferences(PropertiesWithMissingReferences); + DrawPropertiesWithMissingReferences(m_PropertiesWithMissingReferences); - var count = SubAssets.Count; + var count = m_SubAssets.Count; if (count == 0) return; @@ -93,7 +115,7 @@ public override void Draw() if (!m_SubAssetsVisible) return; - foreach (var asset in SubAssets) + foreach (var asset in m_SubAssets) { asset.Draw(); } @@ -142,14 +164,14 @@ public void AddAssetAtPath(string path, Options options) // Model prefabs may contain materials which we want to scan. The "real prefab" as a sub-asset if (asset is GameObject prefab && PrefabUtility.GetPrefabAssetType(asset) != PrefabAssetType.Model) { - var gameObjectContainer = new GameObjectContainer(prefab, options); - if (gameObjectContainer.Count > 0) + var gameObjectContainer = GameObjectContainer.CreateIfNecessary(prefab, options); + if (gameObjectContainer != null) GetOrCreateFolderForAssetPath(path).m_Assets.Add(gameObjectContainer); } else { - var assetContainer = new AssetContainer(asset, path, options); - if (assetContainer.PropertiesWithMissingReferences.Count > 0 || assetContainer.SubAssets.Count > 0) + var assetContainer = AssetContainer.CreateIfNecessary(asset, path, options); + if (assetContainer != null) GetOrCreateFolderForAssetPath(path).m_Assets.Add(assetContainer); } } diff --git a/Editor/MissingReferences/MissingReferencesWindow.cs b/Editor/MissingReferences/MissingReferencesWindow.cs index 018337e..3f6aa21 100644 --- a/Editor/MissingReferences/MissingReferencesWindow.cs +++ b/Editor/MissingReferences/MissingReferencesWindow.cs @@ -4,6 +4,7 @@ using System.Reflection; using UnityEditor; using UnityEngine; +using UnityEngine.SceneManagement; using UnityObject = UnityEngine.Object; namespace Unity.Labs.SuperScience @@ -23,6 +24,74 @@ protected abstract class MissingReferencesContainer public abstract void SetVisibleRecursively(bool visible); } + protected class SceneContainer + { + const string k_UntitledSceneName = "Untitled"; + + readonly string m_SceneName; + readonly List m_Roots; + readonly int m_Count; + + bool m_Visible; + + public List Roots => m_Roots; + + public SceneContainer(string sceneName, List roots, int count) + { + m_SceneName = sceneName; + m_Roots = roots; + m_Count = count; + } + + public void Draw() + { + var wasVisible = m_Visible; + m_Visible = EditorGUILayout.Foldout(m_Visible, $"{m_SceneName} ({m_Count})", true, Styles.RichTextFoldout); + + // Hold alt to apply visibility state to all children (recursively) + if (m_Visible != wasVisible && Event.current.alt) + { + foreach (var gameObjectContainer in m_Roots) + { + gameObjectContainer.SetVisibleRecursively(m_Visible); + } + } + + if (!m_Visible) + return; + + using (new EditorGUI.IndentLevelScope()) + { + foreach (var gameObjectContainer in Roots) + { + gameObjectContainer.Draw(); + } + } + } + + public static SceneContainer CreateIfNecessary(Scene scene, Options options) + { + var sceneName = scene.name; + if (string.IsNullOrEmpty(sceneName)) + sceneName = k_UntitledSceneName; + + var count = 0; + List roots = null; + foreach (var gameObject in scene.GetRootGameObjects()) + { + var rootContainer = GameObjectContainer.CreateIfNecessary(gameObject, options); + if (rootContainer != null) + { + roots ??= new List(); + roots.Add(rootContainer); + count += rootContainer.Count; + } + } + + return roots != null ? new SceneContainer(sceneName, roots, count) : null; + } + } + /// /// Tree structure for GameObject scan results /// When the Scan method encounters a GameObject in a scene or a prefab in the project, we initialize one of @@ -40,19 +109,14 @@ internal class ComponentContainer const string k_MissingScriptLabel = "Missing Script!"; readonly Component m_Component; - public readonly List PropertiesWithMissingReferences = new List(); + readonly List m_PropertiesWithMissingReferences; - /// - /// Initialize a ComponentContainer to represent the given Component - /// This will scan the component for missing references and retain the information for display in - /// the given window. - /// - /// The Component to scan for missing references - /// User-configurable options for this view - public ComponentContainer(Component component, Options options) + public List PropertiesWithMissingReferences => m_PropertiesWithMissingReferences; + + public ComponentContainer(Component component, List propertiesWithMissingReferences) { m_Component = component; - CheckForMissingReferences(component, PropertiesWithMissingReferences, options); + m_PropertiesWithMissingReferences = propertiesWithMissingReferences; } /// @@ -70,9 +134,28 @@ public void Draw() return; } - DrawPropertiesWithMissingReferences(PropertiesWithMissingReferences); + DrawPropertiesWithMissingReferences(m_PropertiesWithMissingReferences); } } + + /// + /// Initialize a ComponentContainer to represent the given Component + /// This will scan the component for missing references and retain the information for display in + /// the given window. + /// + /// The Component to scan for missing references + /// User-configurable options for this view + public static ComponentContainer CreateIfNecessary(Component component, Options options) + { + var missingReferences = CheckForMissingReferences(component, options); + + // If the component is null, this is a missing script. Otherwise, if there are missing references, + // create and return a container + if (component == null || missingReferences != null) + return new ComponentContainer(component, missingReferences); + + return null; + } } const int k_PingButtonWidth = 35; @@ -83,24 +166,38 @@ public void Draw() const string k_ChildrenGroupLabelFormat = "Children: {0}"; readonly GameObject m_GameObject; - readonly List m_Children = new List(); - readonly List m_Components = new List(); - internal List Children => m_Children; - - bool m_IsMissingPrefab; - int m_MissingReferencesInChildren; - int m_MissingReferencesInComponents; + readonly bool m_IsMissingPrefab; + readonly List m_Children; + readonly List m_Components; + readonly int m_MissingReferencesInChildren; + readonly int m_MissingReferencesInComponents; + readonly int m_MissingReferences; internal bool HasMissingReferences => m_IsMissingPrefab || m_MissingReferencesInComponents > 0; + internal List Children => m_Children; bool m_Visible; bool m_ShowComponents; bool m_ShowChildren; - public int Count { get; private set; } public override UnityObject Object => m_GameObject; + public int Count => m_MissingReferences; - public GameObjectContainer() { } + // Local method use only -- created here to reduce garbage collection. Collections must be cleared before use + static readonly List k_Components = new List(); + + public GameObjectContainer(GameObject gameObject, bool isMissingPrefab, + int missingReferencesInChildren, int missingReferencesInComponents, + List children, List components) + { + m_GameObject = gameObject; + m_IsMissingPrefab = isMissingPrefab; + m_MissingReferencesInChildren = missingReferencesInChildren; + m_MissingReferencesInComponents = missingReferencesInComponents; + m_Children = children; + m_Components = components; + m_MissingReferences = missingReferencesInComponents + missingReferencesInChildren; + } /// /// Initialize a GameObjectContainer to represent the given GameObject @@ -109,60 +206,66 @@ public GameObjectContainer() { } /// /// The GameObject to scan for missing references /// User-configurable options for this view - internal GameObjectContainer(GameObject gameObject, Options options) + public static GameObjectContainer CreateIfNecessary(GameObject gameObject, Options options) { - m_GameObject = gameObject; - + var isMissingPrefab = false; if (PrefabUtility.IsAnyPrefabInstanceRoot(gameObject)) - m_IsMissingPrefab = PrefabUtility.IsPrefabAssetMissing(gameObject); + isMissingPrefab = PrefabUtility.IsPrefabAssetMissing(gameObject); + + List components = null; + var missingReferencesInComponents = 0; + var missingReferencesInChildren = 0; - foreach (var component in gameObject.GetComponents()) + // GetComponents will clear the list, so we don't have to + gameObject.GetComponents(k_Components); + foreach (var component in k_Components) { - var container = new ComponentContainer(component, options); + var container = ComponentContainer.CreateIfNecessary(component, options); + if (container == null) + continue; + if (component == null) { - m_Components.Add(container); - Count++; - m_MissingReferencesInComponents++; + components ??= new List(); + components.Add(container); + missingReferencesInComponents++; continue; } - var count = container.PropertiesWithMissingReferences.Count; + var missingReferences = container.PropertiesWithMissingReferences; + if (missingReferences == null) + continue; + + var count = missingReferences.Count; if (count > 0) { - m_Components.Add(container); - Count += count; - m_MissingReferencesInComponents += count; + components ??= new List(); + components.Add(container); + missingReferencesInComponents += count; } } + List children = null; foreach (Transform child in gameObject.transform) { - AddChild(child.gameObject, options); + var childContainer = CreateIfNecessary(child.gameObject, options); + if (childContainer != null) + { + children ??= new List(); + children.Add(childContainer); + missingReferencesInChildren += childContainer.m_MissingReferences; + if (childContainer.m_IsMissingPrefab) + missingReferencesInChildren++; + } } - } - - /// - /// Add a child GameObject to this GameObjectContainer - /// - /// The GameObject to scan for missing references - /// User-configurable options for this view - public void AddChild(GameObject gameObject, Options options) - { - var child = new GameObjectContainer(gameObject, options); - var childCount = child.Count; - Count += childCount; - m_MissingReferencesInChildren += childCount; - var isMissingPrefab = child.m_IsMissingPrefab; - if (isMissingPrefab) + if (isMissingPrefab || missingReferencesInComponents > 0 || missingReferencesInChildren > 0) { - m_MissingReferencesInChildren++; - Count++; + return new GameObjectContainer(gameObject, isMissingPrefab, missingReferencesInChildren, + missingReferencesInComponents, children, components); } - if (childCount > 0 || isMissingPrefab) - m_Children.Add(child); + return null; } /// @@ -171,12 +274,12 @@ public void AddChild(GameObject gameObject, Options options) public override void Draw() { var wasVisible = m_Visible; - var label = string.Format(k_LabelFormat, m_GameObject ? m_GameObject.name : "Scene Root", Count); + var label = string.Format(k_LabelFormat, m_GameObject.name, m_MissingReferences); if (m_IsMissingPrefab) label = string.Format(k_MissingPrefabLabelFormat, label); // If this object has 0 missing references but is being drawn, it is a missing prefab with no overrides - if (Count == 0) + if (m_MissingReferences == 0) { using (new EditorGUILayout.HorizontalScope()) { @@ -259,11 +362,21 @@ public override void SetVisibleRecursively(bool visible) m_Visible = visible; m_ShowComponents = visible; m_ShowChildren = visible; + if (m_Children == null) + return; + foreach (var child in m_Children) { child.SetVisibleRecursively(visible); } } + + public void Clear() + { + + m_Children.Clear(); + m_Components.Clear(); + } } static class Styles @@ -343,20 +456,26 @@ protected virtual void DrawItem(Rect selectionRect) /// Check a UnityObject for missing serialized references /// /// The UnityObject to be scanned - /// A list to which properties with missing references will be added /// User-configurable options for this view - /// True if the object has any missing references - protected static void CheckForMissingReferences(UnityObject obj, List properties, Options options) + /// A list of properties with missing references. + /// Null if no properties have missing references. + protected static List CheckForMissingReferences(UnityObject obj, Options options) { if (obj == null) - return; + return null; + List properties = null; var property = new SerializedObject(obj).GetIterator(); while (property.NextVisible(true)) // enterChildren = true to scan all properties { if (CheckForMissingReferences(property, options)) + { + properties ??= new List(); properties.Add(property.Copy()); // Use a copy of this property because we are iterating on it + } } + + return properties; } static bool CheckForMissingReferences(SerializedProperty property, Options options) diff --git a/Editor/MissingReferences/MissingSceneReferences.cs b/Editor/MissingReferences/MissingSceneReferences.cs index d326fb5..691e360 100644 --- a/Editor/MissingReferences/MissingSceneReferences.cs +++ b/Editor/MissingReferences/MissingSceneReferences.cs @@ -22,14 +22,13 @@ sealed class MissingSceneReferences : MissingReferencesWindow "WARNING: For large scenes, this may take a long time and/or crash the Editor."; const string k_NoMissingReferences = "No missing references in active scene"; - const string k_UntitledSceneName = "Untitled"; // Bool fields will be serialized to maintain state between domain reloads, but our list of GameObjects will not [NonSerialized] bool m_Scanned; Vector2 m_ScrollPosition; - readonly List> m_SceneRoots = new List>(); + readonly List m_SceneContainers = new List(); ILookup m_AllMissingReferences; @@ -43,7 +42,7 @@ sealed class MissingSceneReferences : MissingReferencesWindow protected override void Scan(Options options) { m_Scanned = true; - m_SceneRoots.Clear(); + m_SceneContainers.Clear(); // If we are in prefab isolation mode, scan the prefab stage instead of the active scene var prefabStage = PrefabStageUtility.GetCurrentPrefabStage(); @@ -67,16 +66,23 @@ protected override void Scan(Options options) void AddToList(List list, GameObjectContainer container) { - list.AddRange(container.Children.Where(x => x.HasMissingReferences)); - foreach (var child in container.Children) + var children = container.Children; + if (children == null) + return; + + list.AddRange(children.Where(x => x.HasMissingReferences)); + foreach (var child in children) { AddToList(list, child); } } - foreach (var kvp in m_SceneRoots) + foreach (var container in m_SceneContainers) { - AddToList(allMissingReferencesContainers, kvp.Value); + foreach (var gameObjectContainer in container.Roots) + { + AddToList(allMissingReferencesContainers, gameObjectContainer); + } } m_AllMissingReferences = allMissingReferencesContainers.ToLookup(x => x.Object.GetInstanceID()); @@ -91,20 +97,9 @@ void AddToList(List list, GameObjectContainer container) void ScanScene(Scene scene, Options options) { - var rootObjectContainer = new GameObjectContainer(); - foreach (var gameObject in scene.GetRootGameObjects()) - { - rootObjectContainer.AddChild(gameObject, options); - } - - if (rootObjectContainer.Count > 0) - { - var sceneName = scene.name; - if (string.IsNullOrEmpty(sceneName)) - sceneName = k_UntitledSceneName; - - m_SceneRoots.Add(new KeyValuePair(sceneName, rootObjectContainer)); - } + var sceneContainer = SceneContainer.CreateIfNecessary(scene, options); + if (sceneContainer != null) + m_SceneContainers.Add(sceneContainer); } protected override void OnGUI() @@ -117,7 +112,7 @@ protected override void OnGUI() GUIUtility.ExitGUI(); } - if (m_SceneRoots.Count == 0) + if (m_SceneContainers.Count == 0) { GUILayout.Label(k_NoMissingReferences); } @@ -126,9 +121,9 @@ protected override void OnGUI() using (var scrollView = new GUILayout.ScrollViewScope(m_ScrollPosition)) { m_ScrollPosition = scrollView.scrollPosition; - foreach (var kvp in m_SceneRoots) + foreach (var container in m_SceneContainers) { - kvp.Value.Draw(); + container.Draw(); } } } diff --git a/Editor/PrefabLayerUsers.cs b/Editor/PrefabLayerUsers.cs index d2927ed..ce23eed 100644 --- a/Editor/PrefabLayerUsers.cs +++ b/Editor/PrefabLayerUsers.cs @@ -263,7 +263,7 @@ public void Draw(Dictionary layerToName, int layerFilter = k_Invali { if (layerFilter != k_InvalidLayer) { - var gameObjectLayerMatchesFilter = layerUser.PrefabGameObject.layer == layerFilter; + var gameObjectLayerMatchesFilter = layerUser.GameObject.layer == layerFilter; var layerMasksMatchFilter = includeLayerMaskFields && layerUser.LayerMaskLayers.Contains(layerFilter); if (!(gameObjectLayerMatchesFilter || layerMasksMatchFilter)) continue; @@ -278,7 +278,7 @@ public void Draw(Dictionary layerToName, int layerFilter = k_Invali class GameObjectRow { public string TransformPath; - public GameObject PrefabGameObject; + public GameObject GameObject; public List LayerMaskComponents; public SortedSet LayerMaskLayers; @@ -286,7 +286,7 @@ class GameObjectRow public void Draw(Dictionary layerToName, int layerFilter = k_InvalidLayer, bool includeLayerMaskFields = true) { - var layer = PrefabGameObject.layer; + var layer = GameObject.layer; var layerName = GetLayerNameString(layerToName, layer); k_StringBuilder.Length = 0; @@ -302,11 +302,11 @@ public void Draw(Dictionary layerToName, int layerFilter = k_Invali if (includeLayerMaskFields && GetComponentCount(layerFilter) > 0) { m_Expanded = EditorGUILayout.Foldout(m_Expanded, label, true); - EditorGUILayout.ObjectField(PrefabGameObject, typeof(GameObject), true); + EditorGUILayout.ObjectField(GameObject, typeof(GameObject), true); } else { - EditorGUILayout.ObjectField(label, PrefabGameObject, typeof(GameObject), true); + EditorGUILayout.ObjectField(label, GameObject, typeof(GameObject), true); } if (!m_Expanded || !includeLayerMaskFields) @@ -342,7 +342,7 @@ int GetComponentCount(int layerFilter = k_InvalidLayer) struct ComponentRow { - public Component PrefabComponent; + public Component Component; public SortedSet UsedLayers; public void Draw(Dictionary layerToName, int layerFilter = k_InvalidLayer) @@ -350,7 +350,7 @@ public void Draw(Dictionary layerToName, int layerFilter = k_Invali using (new GUILayout.HorizontalScope()) { var layerNameList = GetLayerNameList(UsedLayers, layerToName, layerFilter); - EditorGUILayout.ObjectField($"{PrefabComponent.name} ({PrefabComponent.GetType().Name}) {{{layerNameList}}}", PrefabComponent, typeof(Component), true); + EditorGUILayout.ObjectField($"{Component.name} ({Component.GetType().Name}) {{{layerNameList}}}", Component, typeof(Component), true); } } } @@ -389,7 +389,7 @@ static class Styles const string k_MenuItemName = "Window/SuperScience/Prefab Layer Users"; const string k_WindowTitle = "Prefab Layer Users"; - const string k_NoMissingReferences = "No prefabs using a non-default layer"; + const string k_NoLayerUsers = "No prefabs using a non-default layer"; const string k_ProjectFolderName = "Project"; const int k_FilterPanelWidth = 180; const int k_ObjectFieldWidth = 150; @@ -410,7 +410,7 @@ static class Styles static readonly Stopwatch k_StopWatch = new Stopwatch(); - Vector2 m_ColorListScrollPosition; + Vector2 m_FilterListScrollPosition; Vector2 m_FolderTreeScrollPosition; readonly Folder m_ParentFolder = new Folder(); readonly SortedDictionary m_FilterRows = new SortedDictionary(); @@ -480,7 +480,7 @@ void OnGUI() if (m_ParentFolder.GetCount(m_LayerFilter, m_IncludeLayerMaskFields) == 0) { - GUILayout.Label(k_NoMissingReferences); + GUILayout.Label(k_NoLayerUsers); } else { @@ -524,9 +524,9 @@ void DrawFilters() if (GUILayout.Button($"All ({count})", style)) m_LayerFilter = k_InvalidLayer; - using (var scrollView = new GUILayout.ScrollViewScope(m_ColorListScrollPosition)) + using (var scrollView = new GUILayout.ScrollViewScope(m_FilterListScrollPosition)) { - m_ColorListScrollPosition = scrollView.scrollPosition; + m_FilterListScrollPosition = scrollView.scrollPosition; foreach (var kvp in m_FilterRows) { var layer = kvp.Key; @@ -671,7 +671,7 @@ void FindLayerUsersRecursively(GameObject gameObject, SortedSet prefabGameO layerMaskLayers.UnionWith(usedLayers); componentRows.Add(new ComponentRow { - PrefabComponent = component, + Component = component, UsedLayers = usedLayers }); } @@ -685,7 +685,7 @@ void FindLayerUsersRecursively(GameObject gameObject, SortedSet prefabGameO layerUsers.Add(new GameObjectRow { TransformPath = GetTransformPath(gameObject.transform), - PrefabGameObject = gameObject, + GameObject = gameObject, LayerMaskComponents = componentRows, LayerMaskLayers = layerMaskLayers }); diff --git a/Editor/PrefabTagUsers.cs b/Editor/PrefabTagUsers.cs index f762b56..d1c14cc 100644 --- a/Editor/PrefabTagUsers.cs +++ b/Editor/PrefabTagUsers.cs @@ -224,7 +224,7 @@ public void Draw(string tagFilter = null) { foreach (var user in TagUsers) { - if (!string.IsNullOrEmpty(tagFilter) && !user.PrefabGameObject.CompareTag(tagFilter)) + if (!string.IsNullOrEmpty(tagFilter) && !user.GameObject.CompareTag(tagFilter)) continue; user.Draw(); @@ -236,11 +236,11 @@ public void Draw(string tagFilter = null) struct GameObjectRow { public string TransformPath; - public GameObject PrefabGameObject; + public GameObject GameObject; public void Draw() { - EditorGUILayout.ObjectField($"{TransformPath} - Tag: {PrefabGameObject.tag}", PrefabGameObject, typeof(GameObject), true); + EditorGUILayout.ObjectField($"{TransformPath} - Tag: {GameObject.tag}", GameObject, typeof(GameObject), true); } } @@ -272,7 +272,7 @@ static class Styles const string k_MenuItemName = "Window/SuperScience/Prefab Tag Users"; const string k_WindowTitle = "Prefab Tag Users"; - const string k_NoMissingReferences = "No prefabs using any tags"; + const string k_NoTagUsers = "No prefabs using any tags"; const string k_ProjectFolderName = "Project"; const int k_FilterPanelWidth = 180; const int k_ObjectFieldWidth = 150; @@ -290,7 +290,7 @@ static class Styles static readonly Stopwatch k_StopWatch = new Stopwatch(); - Vector2 m_ColorListScrollPosition; + Vector2 m_FilterListScrollPosition; Vector2 m_FolderTreeScrollPosition; readonly Folder m_ParentFolder = new Folder(); readonly SortedDictionary> m_FilterRows = new SortedDictionary>(); @@ -353,7 +353,7 @@ void OnGUI() if (m_ParentFolder.GetCount(m_TagFilter) == 0) { - GUILayout.Label(k_NoMissingReferences); + GUILayout.Label(k_NoTagUsers); } else { @@ -389,9 +389,9 @@ void DrawFilters() if (GUILayout.Button($"All ({count})", style)) m_TagFilter = null; - using (var scrollView = new GUILayout.ScrollViewScope(m_ColorListScrollPosition)) + using (var scrollView = new GUILayout.ScrollViewScope(m_FilterListScrollPosition)) { - m_ColorListScrollPosition = scrollView.scrollPosition; + m_FilterListScrollPosition = scrollView.scrollPosition; foreach (var kvp in m_FilterRows) { var tag = kvp.Key; @@ -490,7 +490,7 @@ static void FindTagUsersRecursively(GameObject gameObject, SortedSet tag tagUsers.Add(new GameObjectRow { TransformPath = GetTransformPath(gameObject.transform), - PrefabGameObject = gameObject + GameObject = gameObject }); } diff --git a/Runtime/Unity.Labs.SuperScience.api b/Runtime/Unity.Labs.SuperScience.api index 03345ec..513dbc8 100644 --- a/Runtime/Unity.Labs.SuperScience.api +++ b/Runtime/Unity.Labs.SuperScience.api @@ -26,6 +26,7 @@ namespace Unity.Labs.SuperScience public class ExampleSceneMetadata : UnityEngine.ScriptableObject { + public int TotalComponents { get; } public ExampleSceneMetadata() {} public void UpdateFromScene(UnityEngine.SceneManagement.Scene scene); } From 70cd348ce5760485f8fc080fbee7d2db79b707a7 Mon Sep 17 00:00:00 2001 From: Matt Schoen Date: Mon, 30 May 2022 02:13:04 -0700 Subject: [PATCH 06/14] Visible -> Expanded --- Editor/.plan | 3 +- .../MissingProjectReferences.cs | 36 +++++++-------- .../MissingReferencesWindow.cs | 44 +++++++++---------- Editor/PrefabLayerUsers.cs | 24 +++++----- Editor/PrefabTagUsers.cs | 24 +++++----- 5 files changed, 66 insertions(+), 65 deletions(-) diff --git a/Editor/.plan b/Editor/.plan index 6de4c1f..8d6aa3b 100644 --- a/Editor/.plan +++ b/Editor/.plan @@ -1,2 +1,3 @@ Update scene scanning to be async -Rename to ProjectAnalysisWindow to accomodate Prefab/Tag users and Solid Color Textures \ No newline at end of file +Refactor scanning options to be display options so we don't require a re-scan +Rename to ProjectAnalysisWindow to accommodate Prefab/Tag users and Solid Color Textures \ No newline at end of file diff --git a/Editor/MissingReferences/MissingProjectReferences.cs b/Editor/MissingReferences/MissingProjectReferences.cs index 42d5393..51d4e7f 100644 --- a/Editor/MissingReferences/MissingProjectReferences.cs +++ b/Editor/MissingReferences/MissingProjectReferences.cs @@ -36,7 +36,7 @@ class AssetContainer : MissingReferencesContainer readonly List m_SubAssets; readonly List m_PropertiesWithMissingReferences; - bool m_SubAssetsVisible; + bool m_SubAssetsExpanded; public override UnityObject Object => m_Object; @@ -111,8 +111,8 @@ public override void Draw() if (count == 0) return; - m_SubAssetsVisible = EditorGUILayout.Foldout(m_SubAssetsVisible, string.Format(k_SubAssetsLabelFormat, count)); - if (!m_SubAssetsVisible) + m_SubAssetsExpanded = EditorGUILayout.Foldout(m_SubAssetsExpanded, string.Format(k_SubAssetsLabelFormat, count)); + if (!m_SubAssetsExpanded) return; foreach (var asset in m_SubAssets) @@ -122,16 +122,16 @@ public override void Draw() } } - public override void SetVisibleRecursively(bool visible) + public override void SetExpandedRecursively(bool expanded) { - m_SubAssetsVisible = visible; + m_SubAssetsExpanded = expanded; } } const string k_LabelFormat = "{0}: {1}"; readonly SortedDictionary m_Subfolders = new SortedDictionary(); readonly List m_Assets = new List(); - bool m_Visible; + bool m_Expanded; internal List Assets => m_Assets; internal SortedDictionary Subfolders => m_Subfolders; @@ -215,14 +215,14 @@ Folder GetOrCreateFolderForAssetPath(string path) /// The name of the folder public void Draw(string name) { - var wasVisible = m_Visible; - m_Visible = EditorGUILayout.Foldout(m_Visible, string.Format(k_LabelFormat, name, Count), true); + var wasExpanded = m_Expanded; + m_Expanded = EditorGUILayout.Foldout(m_Expanded, string.Format(k_LabelFormat, name, Count), true); - // Hold alt to apply visibility state to all children (recursively) - if (m_Visible != wasVisible && Event.current.alt) - SetVisibleRecursively(m_Visible); + // Hold alt to apply expanded state to all children (recursively) + if (m_Expanded != wasExpanded && Event.current.alt) + SetExpandedRecursively(m_Expanded); - if (!m_Visible) + if (!m_Expanded) return; using (new EditorGUI.IndentLevelScope()) @@ -245,20 +245,20 @@ public void Draw(string name) } /// - /// Set the visibility state of this folder, its contents and their children and all of its subfolders and their contents and children + /// Set the expanded state of this folder, its contents and their children and all of its subfolders and their contents and children /// - /// Whether this object and its children should be visible in the GUI - void SetVisibleRecursively(bool visible) + /// Whether this object should be expanded in the GUI + void SetExpandedRecursively(bool expanded) { - m_Visible = visible; + m_Expanded = expanded; foreach (var asset in m_Assets) { - asset.SetVisibleRecursively(visible); + asset.SetExpandedRecursively(expanded); } foreach (var kvp in m_Subfolders) { - kvp.Value.SetVisibleRecursively(visible); + kvp.Value.SetExpandedRecursively(expanded); } } diff --git a/Editor/MissingReferences/MissingReferencesWindow.cs b/Editor/MissingReferences/MissingReferencesWindow.cs index 3f6aa21..7085a73 100644 --- a/Editor/MissingReferences/MissingReferencesWindow.cs +++ b/Editor/MissingReferences/MissingReferencesWindow.cs @@ -21,7 +21,7 @@ protected abstract class MissingReferencesContainer { public abstract void Draw(); public abstract UnityObject Object { get; } - public abstract void SetVisibleRecursively(bool visible); + public abstract void SetExpandedRecursively(bool expanded); } protected class SceneContainer @@ -32,7 +32,7 @@ protected class SceneContainer readonly List m_Roots; readonly int m_Count; - bool m_Visible; + bool m_Expanded; public List Roots => m_Roots; @@ -45,19 +45,19 @@ public SceneContainer(string sceneName, List roots, int cou public void Draw() { - var wasVisible = m_Visible; - m_Visible = EditorGUILayout.Foldout(m_Visible, $"{m_SceneName} ({m_Count})", true, Styles.RichTextFoldout); + var wasExpanded = m_Expanded; + m_Expanded = EditorGUILayout.Foldout(m_Expanded, $"{m_SceneName} ({m_Count})", true, Styles.RichTextFoldout); - // Hold alt to apply visibility state to all children (recursively) - if (m_Visible != wasVisible && Event.current.alt) + // Hold alt to apply expanded state to all children (recursively) + if (m_Expanded != wasExpanded && Event.current.alt) { foreach (var gameObjectContainer in m_Roots) { - gameObjectContainer.SetVisibleRecursively(m_Visible); + gameObjectContainer.SetExpandedRecursively(m_Expanded); } } - if (!m_Visible) + if (!m_Expanded) return; using (new EditorGUI.IndentLevelScope()) @@ -176,7 +176,7 @@ public static ComponentContainer CreateIfNecessary(Component component, Options internal bool HasMissingReferences => m_IsMissingPrefab || m_MissingReferencesInComponents > 0; internal List Children => m_Children; - bool m_Visible; + bool m_Expanded; bool m_ShowComponents; bool m_ShowChildren; @@ -273,7 +273,7 @@ public static GameObjectContainer CreateIfNecessary(GameObject gameObject, Optio /// public override void Draw() { - var wasVisible = m_Visible; + var wasExpanded = m_Expanded; var label = string.Format(k_LabelFormat, m_GameObject.name, m_MissingReferences); if (m_IsMissingPrefab) label = string.Format(k_MissingPrefabLabelFormat, label); @@ -291,13 +291,13 @@ public override void Draw() return; } - m_Visible = EditorGUILayout.Foldout(m_Visible, label, true, Styles.RichTextFoldout); + m_Expanded = EditorGUILayout.Foldout(m_Expanded, label, true, Styles.RichTextFoldout); - // Hold alt to apply visibility state to all children (recursively) - if (m_Visible != wasVisible && Event.current.alt) - SetVisibleRecursively(m_Visible); + // Hold alt to apply expanded state to all children (recursively) + if (m_Expanded != wasExpanded && Event.current.alt) + SetExpandedRecursively(m_Expanded); - if (!m_Visible) + if (!m_Expanded) return; using (new EditorGUI.IndentLevelScope()) @@ -354,20 +354,20 @@ void DrawChildren() } /// - /// Set the visibility state of this object and all of its children + /// Set the expanded state of this object and all of its children /// - /// Whether this object and its children should be visible in the GUI - public override void SetVisibleRecursively(bool visible) + /// Whether this object should be expanded in the GUI + public override void SetExpandedRecursively(bool expanded) { - m_Visible = visible; - m_ShowComponents = visible; - m_ShowChildren = visible; + m_Expanded = expanded; + m_ShowComponents = expanded; + m_ShowChildren = expanded; if (m_Children == null) return; foreach (var child in m_Children) { - child.SetVisibleRecursively(visible); + child.SetExpandedRecursively(expanded); } } diff --git a/Editor/PrefabLayerUsers.cs b/Editor/PrefabLayerUsers.cs index ce23eed..bf9df01 100644 --- a/Editor/PrefabLayerUsers.cs +++ b/Editor/PrefabLayerUsers.cs @@ -34,7 +34,7 @@ class Folder readonly SortedDictionary m_TotalWithoutLayerMasksPerLayer = new SortedDictionary(); int m_TotalCount; int m_TotalWithoutLayerMasks; - bool m_Visible; + bool m_Expanded; /// /// Clear the contents of this container. @@ -100,18 +100,18 @@ Folder GetOrCreateFolderForAssetPath(PrefabRow prefabRow) /// (Optional) Whether to include layers from LayerMask fields in the results. public void Draw(string name, Dictionary layerToName, int layerFilter = k_InvalidLayer, bool includeLayerMaskFields = true) { - var wasVisible = m_Visible; + var wasExpanded = m_Expanded; var layerNameList = GetLayerNameList(m_TotalCountPerLayer.Keys, m_TotalWithoutLayerMasksPerLayer.Keys, layerToName, layerFilter, includeLayerMaskFields); var label = $"{name}: {GetCount(layerFilter, includeLayerMaskFields)} {{{layerNameList}}}"; - m_Visible = EditorGUILayout.Foldout(m_Visible, label, true); + m_Expanded = EditorGUILayout.Foldout(m_Expanded, label, true); DrawLineSeparator(); - // Hold alt to apply visibility state to all children (recursively) - if (m_Visible != wasVisible && Event.current.alt) - SetVisibleRecursively(m_Visible); + // Hold alt to apply expanded state to all children (recursively) + if (m_Expanded != wasExpanded && Event.current.alt) + SetExpandedRecursively(m_Expanded); - if (!m_Visible) + if (!m_Expanded) return; using (new EditorGUI.IndentLevelScope()) @@ -158,15 +158,15 @@ static void DrawLineSeparator() } /// - /// Set the visibility state of this folder, its contents and their children and all of its subfolders and their contents and children. + /// Set the expanded state of this folder, its contents and their children and all of its subfolders and their contents and children. /// - /// Whether this object and its children should be visible in the GUI. - void SetVisibleRecursively(bool visible) + /// Whether this object should be expanded in the GUI. + void SetExpandedRecursively(bool expanded) { - m_Visible = visible; + m_Expanded = expanded; foreach (var kvp in m_Subfolders) { - kvp.Value.SetVisibleRecursively(visible); + kvp.Value.SetExpandedRecursively(expanded); } } diff --git a/Editor/PrefabTagUsers.cs b/Editor/PrefabTagUsers.cs index d1c14cc..feec259 100644 --- a/Editor/PrefabTagUsers.cs +++ b/Editor/PrefabTagUsers.cs @@ -32,7 +32,7 @@ class Folder readonly List m_Prefabs = new List(); readonly SortedDictionary m_CountPerTag = new SortedDictionary(); int m_TotalCount; - bool m_Visible; + bool m_Expanded; /// /// Clear the contents of this container. @@ -92,18 +92,18 @@ Folder GetOrCreateFolderForAssetPath(PrefabRow prefabRow) /// (Optional) Tag used to filter results. public void Draw(string name, string tagFilter = null) { - var wasVisible = m_Visible; + var wasExpanded = m_Expanded; var tagList = GetTagList(m_CountPerTag.Keys, tagFilter); var label = $"{name}: {m_TotalCount} {{{tagList}}}"; - m_Visible = EditorGUILayout.Foldout(m_Visible, label, true); + m_Expanded = EditorGUILayout.Foldout(m_Expanded, label, true); DrawLineSeparator(); - // Hold alt to apply visibility state to all children (recursively) - if (m_Visible != wasVisible && Event.current.alt) - SetVisibleRecursively(m_Visible); + // Hold alt to apply expanded state to all children (recursively) + if (m_Expanded != wasExpanded && Event.current.alt) + SetExpandedRecursively(m_Expanded); - if (!m_Visible) + if (!m_Expanded) return; using (new EditorGUI.IndentLevelScope()) @@ -149,15 +149,15 @@ static void DrawLineSeparator() } /// - /// Set the visibility state of this folder, its contents and their children and all of its subfolders and their contents and children. + /// Set the expanded state of this folder, its contents and their children and all of its subfolders and their contents and children. /// - /// Whether this object and its children should be visible in the GUI. - void SetVisibleRecursively(bool visible) + /// Whether this object should be expanded in the GUI. + void SetExpandedRecursively(bool expanded) { - m_Visible = visible; + m_Expanded = expanded; foreach (var kvp in m_Subfolders) { - kvp.Value.SetVisibleRecursively(visible); + kvp.Value.SetExpandedRecursively(expanded); } } From 36920ee135336411e6c05e8ed0fa69bdc7ccfd7c Mon Sep 17 00:00:00 2001 From: Matt Schoen Date: Tue, 31 May 2022 01:05:58 -0700 Subject: [PATCH 07/14] Add Scene Layer Users window; Misc cleanup; Update .plan file --- Editor/.plan | 9 +- .../MissingReferencesWindow.cs | 28 +- Editor/PrefabLayerUsers.cs | 39 +- Editor/SceneLayerUsers.cs | 965 ++++++++++++++++++ Editor/SceneLayerUsers.cs.meta | 11 + Editor/Unity.Labs.SuperScience.Editor.api | 5 + 6 files changed, 1016 insertions(+), 41 deletions(-) create mode 100644 Editor/SceneLayerUsers.cs create mode 100644 Editor/SceneLayerUsers.cs.meta diff --git a/Editor/.plan b/Editor/.plan index 8d6aa3b..4ce52d1 100644 --- a/Editor/.plan +++ b/Editor/.plan @@ -1,3 +1,8 @@ -Update scene scanning to be async +Refactor "Layer Users" to "Layer Usages" +Upgrade Prefab Layer Users to display GameObjects the same way Scene Layer Users does +Update scene scanning to be async in MissingReferences and Layer Usages Refactor scanning options to be display options so we don't require a re-scan -Rename to ProjectAnalysisWindow to accommodate Prefab/Tag users and Solid Color Textures \ No newline at end of file +Rename MissingReferenceWindow base class to AnalysisWindow and refactor to share code Prefab/Tag usages and Solid Color Textures + Split out Folder, GameObjectContainer, etc. types into their own files, share among all analysis windows +Create options to allow Layer Usages windows to skip scanning for layer masks (SerializedProperty access is the bottleneck) +Allow filtering Layer Usages with multiple layers diff --git a/Editor/MissingReferences/MissingReferencesWindow.cs b/Editor/MissingReferences/MissingReferencesWindow.cs index 7085a73..19ba81a 100644 --- a/Editor/MissingReferences/MissingReferencesWindow.cs +++ b/Editor/MissingReferences/MissingReferencesWindow.cs @@ -302,13 +302,6 @@ public override void Draw() using (new EditorGUI.IndentLevelScope()) { - // If m_GameObject is null, this is a scene - if (m_GameObject == null) - { - DrawChildren(); - return; - } - if (m_MissingReferencesInComponents > 0) { EditorGUILayout.ObjectField(m_GameObject, typeof(GameObject), true); @@ -334,23 +327,18 @@ public override void Draw() { using (new EditorGUI.IndentLevelScope()) { - DrawChildren(); + foreach (var child in m_Children) + { + var childObject = child.m_GameObject; + + // Check for null in case of destroyed object + if (childObject) + child.Draw(); + } } } } } - - void DrawChildren() - { - foreach (var child in m_Children) - { - var childObject = child.m_GameObject; - - // Check for null in case of destroyed object - if (childObject) - child.Draw(); - } - } } /// diff --git a/Editor/PrefabLayerUsers.cs b/Editor/PrefabLayerUsers.cs index bf9df01..f498ab8 100644 --- a/Editor/PrefabLayerUsers.cs +++ b/Editor/PrefabLayerUsers.cs @@ -478,26 +478,27 @@ void OnGUI() return; } - if (m_ParentFolder.GetCount(m_LayerFilter, m_IncludeLayerMaskFields) == 0) - { - GUILayout.Label(k_NoLayerUsers); - } - else + + using (new GUILayout.HorizontalScope()) { - using (new GUILayout.HorizontalScope()) + using (new GUILayout.VerticalScope(k_FilterPanelWidthOption)) { - using (new GUILayout.VerticalScope(k_FilterPanelWidthOption)) + DrawFilters(); + } + + using (new GUILayout.VerticalScope()) + { + using (new EditorGUI.DisabledScope(m_LayerWithNoName == k_InvalidLayer)) { - DrawFilters(); + m_IncludeLayerMaskFields = EditorGUILayout.Toggle(k_IncludeLayerMaskFieldsGUIContent, m_IncludeLayerMaskFields); } - using (new GUILayout.VerticalScope()) + if (m_ParentFolder.GetCount(m_LayerFilter, m_IncludeLayerMaskFields) == 0) + { + GUILayout.Label(k_NoLayerUsers); + } + else { - using (new EditorGUI.DisabledScope(m_LayerWithNoName == k_InvalidLayer)) - { - m_IncludeLayerMaskFields = EditorGUILayout.Toggle(k_IncludeLayerMaskFieldsGUIContent, m_IncludeLayerMaskFields); - } - using (var scrollView = new GUILayout.ScrollViewScope(m_FolderTreeScrollPosition)) { m_FolderTreeScrollPosition = scrollView.scrollPosition; @@ -505,12 +506,12 @@ void OnGUI() } } } - } - if (m_ScanCount > 0 && m_ScanCount - m_ScanProgress > 0) - { - var rect = GUILayoutUtility.GetRect(0, float.PositiveInfinity, k_ProgressBarHeight, k_ProgressBarHeight); - EditorGUI.ProgressBar(rect, (float)m_ScanProgress / m_ScanCount, $"{m_ScanProgress} / {m_ScanCount}"); + if (m_ScanCount > 0 && m_ScanCount - m_ScanProgress > 0) + { + var rect = GUILayoutUtility.GetRect(0, float.PositiveInfinity, k_ProgressBarHeight, k_ProgressBarHeight); + EditorGUI.ProgressBar(rect, (float) m_ScanProgress / m_ScanCount, $"{m_ScanProgress} / {m_ScanCount}"); + } } } diff --git a/Editor/SceneLayerUsers.cs b/Editor/SceneLayerUsers.cs new file mode 100644 index 0000000..c5f37c7 --- /dev/null +++ b/Editor/SceneLayerUsers.cs @@ -0,0 +1,965 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UnityEditor; +using UnityEditor.Experimental.SceneManagement; +using UnityEngine; +using UnityEngine.SceneManagement; + +namespace Unity.Labs.SuperScience +{ + /// + /// Scans all loaded scenes for GameObjects which use non-default layers and displays the results in an EditorWindow. + /// + public class SceneLayerUsers : EditorWindow + { + class SceneContainer + { + const string k_UntitledSceneName = "Untitled"; + + readonly string m_SceneName; + readonly List m_Roots; + readonly int m_TotalCount; + readonly int m_TotalWithoutLayerMasks; + readonly SortedDictionary m_CountPerLayer; + readonly SortedDictionary m_CountWithoutLayerMasksPerLayer; + readonly int[] m_Usages; + readonly int[] m_UsagesWithoutLayerMasks; + + bool m_Expanded; + + SceneContainer(string sceneName, List roots) + { + m_SceneName = sceneName; + m_Roots = roots; + m_CountPerLayer = new SortedDictionary(); + m_CountWithoutLayerMasksPerLayer = new SortedDictionary(); + foreach (var container in roots) + { + var layer = container.GameObject.layer; + if (layer != 0) + { + m_TotalCount++; + IncrementCountForLayer(layer, m_CountPerLayer); + IncrementCountForLayer(layer, m_CountWithoutLayerMasksPerLayer); + } + + m_TotalCount += container.TotalUsagesInChildren; + m_TotalCount += container.TotalUsagesInComponents; + m_TotalWithoutLayerMasks += container.TotalUsagesInChildrenWithoutLayerMasks; + AggregateCountPerLayer(container.UsagesInChildrenPerLayer, m_CountPerLayer); + AggregateCountPerLayer(container.UsagesInComponentsPerLayer, m_CountPerLayer); + AggregateCountPerLayer(container.UsagesInChildrenWithoutLayerMasksPerLayer, m_CountWithoutLayerMasksPerLayer); + } + + m_Usages = m_CountPerLayer.Keys.ToArray(); + m_UsagesWithoutLayerMasks = m_CountPerLayer.Keys.ToArray(); + } + + public void Draw(Dictionary layerToName, int layerFilter, bool includeLayerMaskFields) + { + var count = GetCount(layerFilter, includeLayerMaskFields); + if (count == 0) + return; + + k_StringBuilder.Length = 0; + k_StringBuilder.Append(m_SceneName); + k_StringBuilder.Append(" ("); + k_StringBuilder.Append(count.ToString()); + k_StringBuilder.Append(") {"); + AppendLayerNameList(k_StringBuilder, m_Usages, m_UsagesWithoutLayerMasks, layerToName, layerFilter, includeLayerMaskFields); + k_StringBuilder.Append("}"); + var label = k_StringBuilder.ToString(); + var wasExpanded = m_Expanded; + m_Expanded = EditorGUILayout.Foldout(m_Expanded, label, true); + + // Hold alt to apply expanded state to all children (recursively) + if (m_Expanded != wasExpanded && Event.current.alt) + { + foreach (var gameObjectContainer in m_Roots) + { + gameObjectContainer.SetExpandedRecursively(m_Expanded); + } + } + + if (!m_Expanded) + return; + + using (new EditorGUI.IndentLevelScope()) + { + foreach (var gameObjectContainer in m_Roots) + { + gameObjectContainer.Draw(layerToName, layerFilter, includeLayerMaskFields); + } + } + } + + public static SceneContainer CreateIfNecessary(Scene scene, SortedDictionary filterRows, + Dictionary layerToName, int layerWithNoName) + { + var sceneName = scene.name; + if (string.IsNullOrEmpty(sceneName)) + sceneName = k_UntitledSceneName; + + List roots = null; + foreach (var gameObject in scene.GetRootGameObjects()) + { + var rootContainer = GameObjectContainer.CreateIfNecessary(gameObject, filterRows, layerToName, layerWithNoName); + if (rootContainer != null) + { + roots ??= new List(); + roots.Add(rootContainer); + } + } + + return roots != null ? new SceneContainer(sceneName, roots) : null; + } + + public int GetCount(int layerFilter = k_InvalidLayer, bool includeLayerMaskFields = true) + { + var unfiltered = layerFilter == k_InvalidLayer; + if (unfiltered && includeLayerMaskFields) + return m_TotalCount; + + if (unfiltered) + return m_TotalWithoutLayerMasks; + + if (includeLayerMaskFields) + { + m_CountPerLayer.TryGetValue(layerFilter, out var totalCount); + return totalCount; + } + + m_CountWithoutLayerMasksPerLayer.TryGetValue(layerFilter, out var totalWithoutLayerMasks); + return totalWithoutLayerMasks; + } + } + + /// + /// Tree structure for GameObject scan results + /// When the Scan method encounters a GameObject in a scene or a prefab in the project, we initialize one of + /// these using the GameObject as an argument. This scans the object and its components/children, retaining + /// the results for display in the GUI. The window calls into these helper objects to draw them, as well. + /// + class GameObjectContainer + { + /// + /// Container for component scan results. Just as with GameObjectContainer, we initialize one of these + /// using a component to scan it for missing references and retain the results + /// + internal class ComponentContainer + { + readonly Component m_Component; + readonly List m_LayerMaskFields; + readonly SortedDictionary m_UsagesPerLayer; + readonly int[] m_Usages; + + bool m_Expanded; + public int Count => m_LayerMaskFields.Count; + public SortedDictionary UsagesPerLayer => m_UsagesPerLayer; + public bool Expanded { set { m_Expanded = value; } } + + ComponentContainer(Component component, SortedDictionary usagesPerLayer, List layerMaskFields) + { + m_Component = component; + m_UsagesPerLayer = usagesPerLayer; + m_LayerMaskFields = layerMaskFields; + m_Usages = m_UsagesPerLayer.Keys.ToArray(); + } + + /// + /// Draw the missing references UI for this component + /// + public void Draw(Dictionary layerToName, int layerFilter = k_InvalidLayer) + { + if (layerFilter != k_InvalidLayer && !m_UsagesPerLayer.ContainsKey(layerFilter)) + return; + + // Because we can potentially draw a lot of rows, the efficiency using a StringBuilder is worth the messy code + k_StringBuilder.Length = 0; + k_StringBuilder.Append(m_Component.GetType().Name); + k_StringBuilder.Append(" {"); + k_StringBuilder.Append(m_LayerMaskFields.Count.ToString()); + k_StringBuilder.Append(") {"); + AppendLayerNameList(k_StringBuilder, m_Usages, layerToName, layerFilter); + k_StringBuilder.Append("}"); + var label = k_StringBuilder.ToString(); + m_Expanded = EditorGUILayout.Foldout(m_Expanded, label, true); + if (!m_Expanded) + return; + + using (new EditorGUI.IndentLevelScope()) + { + EditorGUILayout.ObjectField(m_Component, typeof(Component), true); + foreach (var property in m_LayerMaskFields) + { + using (var check = new EditorGUI.ChangeCheckScope()) + { + EditorGUILayout.PropertyField(property); + if (check.changed) + property.serializedObject.ApplyModifiedProperties(); + } + } + } + } + + /// + /// Initialize a ComponentContainer to represent the given Component + /// This will scan the component for missing references and retain the information for display in + /// the given window. + /// + /// The Component to scan for missing references + /// Layer to Name dictionary for fast lookup of layer names. + /// An unnamed layer to use for layer mask checks. + public static ComponentContainer CreateIfNecessary(Component component, Dictionary layerToName, int layerWithNoName) + { + var serializedObject = new SerializedObject(component); + var iterator = serializedObject.GetIterator(); + SortedDictionary usagesPerLayer = null; + List layerMaskFields = null; + while (iterator.Next(true)) + { + if (iterator.propertyType != SerializedPropertyType.LayerMask) + continue; + + var layerMask = iterator.intValue; + usagesPerLayer = GetLayersFromLayerMask(usagesPerLayer, layerToName, layerWithNoName, layerMask); + layerMaskFields ??= new List(); + layerMaskFields.Add(iterator.Copy()); + } + + return usagesPerLayer != null ? new ComponentContainer(component, usagesPerLayer, layerMaskFields) : null; + } + + static SortedDictionary GetLayersFromLayerMask(SortedDictionary usagesPerLayer, + Dictionary layerToName, int layerWithNoName, int layerMask) + { + // If all layers are named, it is not possible to infer whether layer mask fields "use" a layer + if (layerWithNoName == k_InvalidLayer) + return null; + + // Exclude the special cases where every layer is included or excluded + if (layerMask == k_InvalidLayer || layerMask == 0) + return null; + + // Depending on whether or not the mask started out as "Everything" or "Nothing", a layer will count as "used" when the user toggles its state. + // We use the layer without a name to check whether or not the starting point is "Everything" or "Nothing." If this layer's bit is 0, we assume + // the mask started with "Nothing." Otherwise, if its bit is 1, we assume the mask started with "Everything." + var defaultValue = (layerMask & 1 << layerWithNoName) != 0; + + foreach (var kvp in layerToName) + { + var layer = kvp.Key; + + // Skip layer 0 since we only want non-default layers + if (layer == 0) + continue; + + // We compare (using xor) this layer's bit value with the default value. If they are different, the layer counts as "used." + if ((layerMask & (1 << layer)) != 0 ^ defaultValue) + { + usagesPerLayer ??= new SortedDictionary(); + IncrementCountForLayer(layer, usagesPerLayer); + } + } + + return usagesPerLayer; + } + } + + readonly GameObject m_GameObject; + readonly List m_Children; + readonly List m_Components; + + readonly int m_TotalUsagesInComponents; + readonly SortedDictionary m_UsagesInComponentsPerLayer; + + //TODO: Rename Users -> Usages + readonly int m_TotalUsagesInChildren; + readonly int m_TotalUsagesInChildrenWithoutLayerMasks; + readonly SortedDictionary m_UsagesInChildrenPerLayer; + readonly SortedDictionary m_UsagesInChildrenWithoutLayerMasksPerLayer; + + readonly int[] m_Usages; + readonly int[] m_UsagesWithoutLayerMasks; + readonly int[] m_UsagesInComponents; + readonly int[] m_UsagesInChildren; + readonly int[] m_UsagesInChildrenWithoutLayerMasks; + + bool m_Expanded; + bool m_ShowComponents; + bool m_ShowChildren; + + public GameObject GameObject { get { return m_GameObject; } } + public int TotalUsagesInComponents => m_TotalUsagesInComponents; + public SortedDictionary UsagesInComponentsPerLayer => m_UsagesInComponentsPerLayer; + public int TotalUsagesInChildren => m_TotalUsagesInChildren; + public int TotalUsagesInChildrenWithoutLayerMasks => m_TotalUsagesInChildrenWithoutLayerMasks; + public SortedDictionary UsagesInChildrenPerLayer => m_UsagesInChildrenPerLayer; + public SortedDictionary UsagesInChildrenWithoutLayerMasksPerLayer => m_UsagesInChildrenWithoutLayerMasksPerLayer; + + // Local method use only -- created here to reduce garbage collection. Collections must be cleared before use + static readonly SortedSet k_Usages = new SortedSet(); + static readonly SortedSet k_UsagesWithoutLayerMasks = new SortedSet(); + + GameObjectContainer(GameObject gameObject, List components, List children) + { + m_GameObject = gameObject; + m_Components = components; + m_Children = children; + k_Usages.Clear(); + k_UsagesWithoutLayerMasks.Clear(); + + var layer = gameObject.layer; + if (layer != 0) + { + k_Usages.Add(layer); + k_UsagesWithoutLayerMasks.Add(layer); + } + + if (components != null) + { + m_UsagesInComponentsPerLayer = new SortedDictionary(); + foreach (var container in components) + { + AggregateCount(container, ref m_TotalUsagesInComponents); + } + + m_UsagesInComponents = m_UsagesInComponentsPerLayer.Keys.ToArray(); + k_Usages.UnionWith(m_UsagesInComponents); + } + + if (children != null) + { + m_UsagesInChildrenPerLayer = new SortedDictionary(); + m_UsagesInChildrenWithoutLayerMasksPerLayer = new SortedDictionary(); + foreach (var container in children) + { + var componentUsages = container.m_UsagesInComponentsPerLayer; + if (componentUsages != null) + m_UsagesInComponentsPerLayer ??= new SortedDictionary(); + + AggregateCount(container, ref m_TotalUsagesInChildren, ref m_TotalUsagesInChildrenWithoutLayerMasks); + } + + m_UsagesInChildren = m_UsagesInChildrenPerLayer.Keys.ToArray(); + m_UsagesInChildrenWithoutLayerMasks = m_UsagesInChildrenWithoutLayerMasksPerLayer.Keys.ToArray(); + k_Usages.UnionWith(m_UsagesInChildren); + k_UsagesWithoutLayerMasks.UnionWith(m_UsagesInChildrenWithoutLayerMasks); + } + + m_Usages = k_Usages.ToArray(); + m_UsagesWithoutLayerMasks = k_UsagesWithoutLayerMasks.ToArray(); + } + + /// + /// Initialize a GameObjectContainer to represent the given GameObject + /// This will scan the component for missing references and retain the information for display in + /// the given window. + /// + /// The GameObject to scan for missing references + /// Dictionary of FilterRow objects for counting usages per layer. + /// Layer to Name dictionary for fast lookup of layer names. + /// An unnamed layer to use for layer mask checks. + public static GameObjectContainer CreateIfNecessary(GameObject gameObject, SortedDictionary filterRows, + Dictionary layerToName, int layerWithNoName) + { + // GetComponents will clear the list, so we don't havWe to + gameObject.GetComponents(k_Components); + List components = null; + foreach (var component in k_Components) + { + var container = ComponentContainer.CreateIfNecessary(component, layerToName, layerWithNoName); + if (container != null) + { + components ??= new List(); + components.Add(container); + } + } + + // Clear the list after we're done to avoid lingering references + k_Components.Clear(); + + List children = null; + foreach (Transform child in gameObject.transform) + { + var childContainer = CreateIfNecessary(child.gameObject, filterRows, layerToName, layerWithNoName); + if (childContainer != null) + { + children ??= new List(); + children.Add(childContainer); + } + } + + var layer = gameObject.layer; + var isLayerUser = layer != 0; + if (isLayerUser || components != null || children != null) + { + if (isLayerUser) + { + var filterRow = GetOrCreateFilterRowSetForLayer(filterRows, layer); + filterRow.UsersWithoutLayerMasks.Add(gameObject); + filterRow.AllUsers.Add(gameObject); + } + + var newContainer = new GameObjectContainer(gameObject, components, children); + if (components != null) + { + foreach (var kvp in newContainer.m_UsagesInComponentsPerLayer) + { + var filterRow = GetOrCreateFilterRowSetForLayer(filterRows, kvp.Key); + filterRow.AllUsers.Add(gameObject); + } + } + + return newContainer; + } + + return null; + } + + /// + /// Draw layer user information for this GameObjectContainer + /// + public void Draw(Dictionary layerToName, int layerFilter, bool includeLayerMaskFields) + { + var layer = m_GameObject.layer; + var isLayerUser = layerFilter == k_InvalidLayer ? layer != 0 : layer == layerFilter; + + var componentCount = 0; + var hasComponents = false; + if (includeLayerMaskFields) + { + componentCount = GetComponentCount(layerFilter); + hasComponents = componentCount > 0; + } + + var childCount = GetChildCount(layerFilter, includeLayerMaskFields); + var hasChildren = childCount > 0; + if (!(isLayerUser || hasChildren || hasComponents)) + return; + + var count = componentCount + childCount; + var label = GetLabel(layerToName, layerFilter, includeLayerMaskFields, count, isLayerUser, layer); + var wasExpanded = m_Expanded; + if (hasChildren || hasComponents) + m_Expanded = EditorGUILayout.Foldout(m_Expanded, label, true); + else + EditorGUILayout.LabelField(label); + + + // Hold alt to apply expanded state to all children (recursively) + if (m_Expanded != wasExpanded && Event.current.alt) + { + if (m_Components != null) + { + foreach (var gameObjectContainer in m_Components) + { + gameObjectContainer.Expanded = m_Expanded; + } + } + + if (m_Children != null) + { + foreach (var gameObjectContainer in m_Children) + { + gameObjectContainer.SetExpandedRecursively(m_Expanded); + } + } + } + + using (new EditorGUI.IndentLevelScope()) + { + if (isLayerUser) + EditorGUILayout.ObjectField(m_GameObject, typeof(GameObject), true); + + if (!m_Expanded) + return; + + if (hasComponents && m_Components != null) + DrawComponents(layerToName, layerFilter, componentCount); + + if (hasChildren && m_Children != null) + DrawChildren(layerToName, layerFilter, includeLayerMaskFields, childCount); + } + } + + string GetLabel(Dictionary layerToName, int layerFilter, bool includeLayerMaskFields, int count, bool isLayerUser, int layer) + { + k_StringBuilder.Length = 0; + k_StringBuilder.Append(m_GameObject.name); + k_StringBuilder.Append(" ("); + k_StringBuilder.Append(count.ToString()); + if (isLayerUser) + { + var layerName = GetLayerNameString(layerToName, layer); + k_StringBuilder.Append(") - Layer: "); + k_StringBuilder.Append(layerName); + k_StringBuilder.Append(" {"); + } + else + { + k_StringBuilder.Append(") {"); + } + + AppendLayerNameList(k_StringBuilder, m_Usages, m_UsagesWithoutLayerMasks, layerToName, layerFilter, includeLayerMaskFields); + k_StringBuilder.Append("}"); + + return k_StringBuilder.ToString(); + } + + void DrawComponents(Dictionary layerToName, int layerFilter, int componentCount) + { + k_StringBuilder.Length = 0; + k_StringBuilder.Append("Components ("); + k_StringBuilder.Append(componentCount.ToString()); + k_StringBuilder.Append(") {"); + AppendLayerNameList(k_StringBuilder, m_UsagesInComponents, layerToName, layerFilter); + k_StringBuilder.Append("}"); + var label = k_StringBuilder.ToString(); + m_ShowComponents = EditorGUILayout.Foldout(m_ShowComponents, label, true); + if (m_ShowComponents) + { + using (new EditorGUI.IndentLevelScope()) + { + foreach (var component in m_Components) + { + component.Draw(layerToName, layerFilter); + } + } + } + } + + void DrawChildren(Dictionary layerToName, int layerFilter, bool includeLayerMaskFields, int childCount) + { + k_StringBuilder.Length = 0; + k_StringBuilder.Append("Children ("); + k_StringBuilder.Append(childCount.ToString()); + k_StringBuilder.Append(") {"); + AppendLayerNameList(k_StringBuilder, m_UsagesInChildren, m_UsagesInChildrenWithoutLayerMasks, layerToName, layerFilter, includeLayerMaskFields); + k_StringBuilder.Append("}"); + var label = k_StringBuilder.ToString(); + m_ShowChildren = EditorGUILayout.Foldout(m_ShowChildren, label, true); + if (m_ShowChildren) + { + using (new EditorGUI.IndentLevelScope()) + { + foreach (var child in m_Children) + { + child.Draw(layerToName, layerFilter, includeLayerMaskFields); + } + } + } + } + + /// + /// Set the expanded state of this object and all of its children + /// + /// Whether this object should be expanded in the GUI + public void SetExpandedRecursively(bool expanded) + { + m_Expanded = expanded; + m_ShowComponents = expanded; + m_ShowChildren = expanded; + + if (m_Components != null) + { + foreach (var component in m_Components) + { + component.Expanded = expanded; + } + } + + if (m_Children != null) + { + foreach (var child in m_Children) + { + child.SetExpandedRecursively(expanded); + } + } + } + + int GetComponentCount(int layerFilter = k_InvalidLayer) + { + if (m_Components == null) + return 0; + + if (layerFilter == k_InvalidLayer) + return m_TotalUsagesInComponents; + + m_UsagesInComponentsPerLayer.TryGetValue(layerFilter, out var count); + return count; + } + + int GetChildCount(int layerFilter = k_InvalidLayer, bool includeLayerMaskFields = true) + { + if (m_Children == null) + return 0; + + var unfiltered = layerFilter == k_InvalidLayer; + if (unfiltered && includeLayerMaskFields) + return m_TotalUsagesInChildren; + + if (unfiltered) + return m_TotalUsagesInChildrenWithoutLayerMasks; + + if (includeLayerMaskFields) + { + m_UsagesInChildrenPerLayer.TryGetValue(layerFilter, out var totalCount); + return totalCount; + } + + m_UsagesInChildrenWithoutLayerMasksPerLayer.TryGetValue(layerFilter, out var totalWithoutLayerMasks); + return totalWithoutLayerMasks; + } + + void AggregateCount(GameObjectContainer child, ref int layerUsagesInChildren, + ref int layerUsagesInChildrenWithoutLayerMasks) + { + var layer = child.GameObject.layer; + if (layer != 0) + { + layerUsagesInChildren++; + layerUsagesInChildrenWithoutLayerMasks++; + IncrementCountForLayer(layer, m_UsagesInChildrenPerLayer); + IncrementCountForLayer(layer, m_UsagesInChildrenWithoutLayerMasksPerLayer); + } + + layerUsagesInChildren += child.m_TotalUsagesInChildren; + layerUsagesInChildrenWithoutLayerMasks += child.m_TotalUsagesInChildrenWithoutLayerMasks; + AggregateCountPerLayer(child.m_UsagesInChildrenPerLayer, m_UsagesInChildrenPerLayer); + AggregateCountPerLayer(child.m_UsagesInChildrenWithoutLayerMasksPerLayer, m_UsagesInChildrenWithoutLayerMasksPerLayer); + + layerUsagesInChildren += child.m_TotalUsagesInComponents; + AggregateCountPerLayer(child.m_UsagesInComponentsPerLayer, m_UsagesInChildrenPerLayer); + } + + void AggregateCount(ComponentContainer container, ref int layerUsagesInComponents) + { + layerUsagesInComponents += container.Count; + AggregateCountPerLayer(container.UsagesPerLayer, m_UsagesInComponentsPerLayer); + } + } + + class FilterRow + { + public readonly HashSet AllUsers = new HashSet(); + public readonly HashSet UsersWithoutLayerMasks = new HashSet(); + } + + static class Styles + { + internal static readonly GUIStyle ActiveFilterButton = new GUIStyle(GUI.skin.button) + { + alignment = TextAnchor.MiddleLeft, + fontStyle = FontStyle.Bold + }; + + internal static readonly GUIStyle InactiveFilterButton = new GUIStyle(GUI.skin.button) + { + alignment = TextAnchor.MiddleLeft + }; + } + + const string k_MenuItemName = "Window/SuperScience/Scene Layer Users"; + const string k_WindowTitle = "Scene Layer Users"; + const string k_NoUsages = "No scene objects using a non-default layer"; + const int k_FilterPanelWidth = 180; + const int k_ObjectFieldWidth = 150; + const string k_Instructions = "Click the Scan button to scan your project for users of non-default layers. WARNING: " + + "This will load every prefab in your project. For large projects, this may take a long time and/or crash the Editor."; + const int k_ProgressBarHeight = 15; + const int k_InvalidLayer = -1; + + static readonly GUIContent k_IncludeLayerMaskFieldsGUIContent = new GUIContent("Include LayerMask Fields", + "Include layers from layer mask fields in the results. This is only possible if there is at least one layer without a name."); + + static readonly GUIContent k_ScanGUIContent = new GUIContent("Scan", "Scan the project for users of non-default layers"); + static readonly GUIContent k_CancelGUIContent = new GUIContent("Cancel", "Cancel the current scan"); + static readonly GUILayoutOption k_FilterPanelWidthOption = GUILayout.Width(k_FilterPanelWidth); + static readonly Vector2 k_MinSize = new Vector2(400, 200); + + Vector2 m_ColorListScrollPosition; + Vector2 m_FolderTreeScrollPosition; + readonly List m_SceneContainers = new List(); + readonly SortedDictionary m_FilterRows = new SortedDictionary(); + int m_ScanCount; + int m_ScanProgress; + IEnumerator m_ScanEnumerator; + readonly Dictionary m_LayerToName = new Dictionary(); + int m_LayerWithNoName = k_InvalidLayer; + + [SerializeField] + bool m_IncludeLayerMaskFields = true; + + [SerializeField] + int m_LayerFilter = k_InvalidLayer; + + // Local method use only -- created here to reduce garbage collection. Collections must be cleared before use + static readonly StringBuilder k_StringBuilder = new StringBuilder(4096); + static readonly List k_Components = new List(); + + /// + /// Initialize the window + /// + [MenuItem(k_MenuItemName)] + static void Init() + { + GetWindow(k_WindowTitle).Show(); + } + + void OnEnable() + { + minSize = k_MinSize; + m_ScanCount = 0; + m_ScanProgress = 0; + } + + void OnDisable() + { + m_ScanEnumerator = null; + } + + void OnGUI() + { + EditorGUIUtility.labelWidth = position.width - k_FilterPanelWidth - k_ObjectFieldWidth; + + if (m_ScanEnumerator == null) + { + if (GUILayout.Button(k_ScanGUIContent)) + Scan(); + } + else + { + if (GUILayout.Button(k_CancelGUIContent)) + m_ScanEnumerator = null; + } + + // If m_LayerToName hasn't been set up, we haven't scanned yet + // This dictionary will always at least include the built-in layer names + if (m_LayerToName.Count == 0) + { + EditorGUILayout.HelpBox(k_Instructions, MessageType.Info); + GUIUtility.ExitGUI(); + return; + } + + using (new GUILayout.HorizontalScope()) + { + using (new GUILayout.VerticalScope(k_FilterPanelWidthOption)) + { + DrawFilters(); + } + + using (new GUILayout.VerticalScope()) + { + using (new EditorGUI.DisabledScope(m_LayerWithNoName == k_InvalidLayer)) + { + m_IncludeLayerMaskFields = EditorGUILayout.Toggle(k_IncludeLayerMaskFieldsGUIContent, m_IncludeLayerMaskFields); + } + + var count = 0; + foreach (var container in m_SceneContainers) + { + count += container.GetCount(m_LayerFilter, m_IncludeLayerMaskFields); + } + + if (count == 0) + { + GUILayout.Label(k_NoUsages); + } + else + { + using (var scrollView = new GUILayout.ScrollViewScope(m_FolderTreeScrollPosition)) + { + m_FolderTreeScrollPosition = scrollView.scrollPosition; + foreach (var container in m_SceneContainers) + { + container.Draw(m_LayerToName, m_LayerFilter, m_IncludeLayerMaskFields); + } + } + } + } + } + + if (m_ScanCount > 0 && m_ScanCount - m_ScanProgress > 0) + { + var rect = GUILayoutUtility.GetRect(0, float.PositiveInfinity, k_ProgressBarHeight, k_ProgressBarHeight); + EditorGUI.ProgressBar(rect, (float) m_ScanProgress / m_ScanCount, $"{m_ScanProgress} / {m_ScanCount}"); + } + } + + /// + /// Draw a list buttons for filtering based on layer. + /// + void DrawFilters() + { + var count = 0; + foreach (var container in m_SceneContainers) + { + count += container.GetCount(includeLayerMaskFields: m_IncludeLayerMaskFields); + } + + var style = m_LayerFilter == k_InvalidLayer ? Styles.ActiveFilterButton : Styles.InactiveFilterButton; + if (GUILayout.Button($"All ({count})", style)) + m_LayerFilter = k_InvalidLayer; + + using (var scrollView = new GUILayout.ScrollViewScope(m_ColorListScrollPosition)) + { + m_ColorListScrollPosition = scrollView.scrollPosition; + foreach (var kvp in m_FilterRows) + { + var layer = kvp.Key; + + // Skip the default layer + if (layer == 0) + continue; + + count = 0; + if (m_FilterRows.TryGetValue(layer, out var filterRow)) + count = m_IncludeLayerMaskFields ? filterRow.AllUsers.Count : filterRow.UsersWithoutLayerMasks.Count; + + var layerName = GetLayerNameString(m_LayerToName, layer); + style = m_LayerFilter == layer ? Styles.ActiveFilterButton : Styles.InactiveFilterButton; + if (GUILayout.Button($"{layer}: {layerName} ({count})", style)) + m_LayerFilter = layer; + } + } + } + + /// + /// Scan the project for layer users and populate the data structures for UI. + /// + void Scan() + { + m_LayerWithNoName = k_InvalidLayer; + m_LayerToName.Clear(); + m_FilterRows.Clear(); + for (var i = 0; i < 32; i++) + { + var layerName = LayerMask.LayerToName(i); + if (!string.IsNullOrEmpty(layerName)) + { + m_LayerToName.Add(i, layerName); + m_FilterRows.Add(i, new FilterRow()); + } + else + { + m_LayerWithNoName = i; + } + } + + // LayerMask field scanning requires at least one layer without a name + if (m_LayerWithNoName == k_InvalidLayer) + m_IncludeLayerMaskFields = false; + + m_SceneContainers.Clear(); + + // If we are in prefab isolation mode, scan the prefab stage instead of the active scene + var prefabStage = PrefabStageUtility.GetCurrentPrefabStage(); + if (prefabStage != null) + { + ScanScene(prefabStage.scene); + return; + } + + var loadedSceneCount = SceneManager.sceneCount; + for (var i = 0; i < loadedSceneCount; i++) + { + var scene = SceneManager.GetSceneAt(i); + if (!scene.IsValid()) + continue; + + ScanScene(scene); + } + } + + void ScanScene(Scene scene) + { + var sceneContainer = SceneContainer.CreateIfNecessary(scene, m_FilterRows, m_LayerToName, m_LayerWithNoName); + if (sceneContainer != null) + m_SceneContainers.Add(sceneContainer); + } + + /// + /// Get or create a for a given layer value. + /// + /// Dictionary of FilterRow objects for counting usages per layer. + /// The layer value to use for this row. + /// The row for the layer value. + static FilterRow GetOrCreateFilterRowSetForLayer(SortedDictionary filterRows, int layer) + { + if (filterRows.TryGetValue(layer, out var filterRow)) + return filterRow; + + filterRow = new FilterRow(); + filterRows[layer] = filterRow; + return filterRow; + } + + static void AppendLayerNameList(StringBuilder stringBuilder, int[] usages, int[] usagesWithoutLayerMasks, + Dictionary layerToName, int layerFilter = k_InvalidLayer, bool includeLayerMaskFields = true) + { + if (layerFilter >= 0) + { + stringBuilder.Append(GetLayerNameString(layerToName, layerFilter)); + return; + } + + AppendLayerNameList(stringBuilder, includeLayerMaskFields ? usages : usagesWithoutLayerMasks, layerToName, layerFilter); + } + + static void AppendLayerNameList(StringBuilder stringBuilder, int[] layers, Dictionary layerToName, int layerFilter = k_InvalidLayer) + { + if (layerFilter >= 0) + { + stringBuilder.Append(GetLayerNameString(layerToName, layerFilter)); + return; + } + + var length = layers.Length; + if (length == 0) + return; + + var lengthMinusOne = length - 1; + for (var i = 0; i < lengthMinusOne; i++) + { + stringBuilder.Append(GetLayerNameString(layerToName, layers[i])); + stringBuilder.Append(", "); + } + + stringBuilder.Append(GetLayerNameString(layerToName, layers[lengthMinusOne])); + } + + static string GetLayerNameString(Dictionary layerToName, int layer) + { + layerToName.TryGetValue(layer, out var layerName); + if (string.IsNullOrEmpty(layerName)) + layerName = layer.ToString(); + + return layerName; + } + + static void IncrementCountForLayer(int layer, SortedDictionary countPerLayer) + { + countPerLayer.TryGetValue(layer, out var count); + count++; + countPerLayer[layer] = count; + } + + static void AggregateCountPerLayer(SortedDictionary source, SortedDictionary destination) + { + if (source == null) + return; + + foreach (var kvp in source) + { + var layer = kvp.Key; + destination.TryGetValue(layer, out var count); + count += kvp.Value; + destination[layer] = count; + } + } + } +} diff --git a/Editor/SceneLayerUsers.cs.meta b/Editor/SceneLayerUsers.cs.meta new file mode 100644 index 0000000..dd93ad6 --- /dev/null +++ b/Editor/SceneLayerUsers.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 55d2b3e96ab4fd94b8d3195eff448abf +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Unity.Labs.SuperScience.Editor.api b/Editor/Unity.Labs.SuperScience.Editor.api index 5a90bf1..9c512be 100644 --- a/Editor/Unity.Labs.SuperScience.Editor.api +++ b/Editor/Unity.Labs.SuperScience.Editor.api @@ -43,4 +43,9 @@ namespace Unity.Labs.SuperScience { public RunInEditHelper() {} } + + public class SceneLayerUsers : UnityEditor.EditorWindow + { + public SceneLayerUsers() {} + } } From 333cd571205e9b36d107060de2a0ed5bdb5629b8 Mon Sep 17 00:00:00 2001 From: Matt Schoen Date: Tue, 31 May 2022 01:07:10 -0700 Subject: [PATCH 08/14] Move layer users windows into their own folder --- Editor/LayerUsers.meta | 8 ++++++++ Editor/{ => LayerUsers}/PrefabLayerUsers.cs | 0 Editor/{ => LayerUsers}/PrefabLayerUsers.cs.meta | 0 Editor/{ => LayerUsers}/PrefabTagUsers.cs | 0 Editor/{ => LayerUsers}/PrefabTagUsers.cs.meta | 0 Editor/{ => LayerUsers}/SceneLayerUsers.cs | 0 Editor/{ => LayerUsers}/SceneLayerUsers.cs.meta | 0 7 files changed, 8 insertions(+) create mode 100644 Editor/LayerUsers.meta rename Editor/{ => LayerUsers}/PrefabLayerUsers.cs (100%) rename Editor/{ => LayerUsers}/PrefabLayerUsers.cs.meta (100%) rename Editor/{ => LayerUsers}/PrefabTagUsers.cs (100%) rename Editor/{ => LayerUsers}/PrefabTagUsers.cs.meta (100%) rename Editor/{ => LayerUsers}/SceneLayerUsers.cs (100%) rename Editor/{ => LayerUsers}/SceneLayerUsers.cs.meta (100%) diff --git a/Editor/LayerUsers.meta b/Editor/LayerUsers.meta new file mode 100644 index 0000000..b911809 --- /dev/null +++ b/Editor/LayerUsers.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 059244c3e45aac5438c1428abd575aaa +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/PrefabLayerUsers.cs b/Editor/LayerUsers/PrefabLayerUsers.cs similarity index 100% rename from Editor/PrefabLayerUsers.cs rename to Editor/LayerUsers/PrefabLayerUsers.cs diff --git a/Editor/PrefabLayerUsers.cs.meta b/Editor/LayerUsers/PrefabLayerUsers.cs.meta similarity index 100% rename from Editor/PrefabLayerUsers.cs.meta rename to Editor/LayerUsers/PrefabLayerUsers.cs.meta diff --git a/Editor/PrefabTagUsers.cs b/Editor/LayerUsers/PrefabTagUsers.cs similarity index 100% rename from Editor/PrefabTagUsers.cs rename to Editor/LayerUsers/PrefabTagUsers.cs diff --git a/Editor/PrefabTagUsers.cs.meta b/Editor/LayerUsers/PrefabTagUsers.cs.meta similarity index 100% rename from Editor/PrefabTagUsers.cs.meta rename to Editor/LayerUsers/PrefabTagUsers.cs.meta diff --git a/Editor/SceneLayerUsers.cs b/Editor/LayerUsers/SceneLayerUsers.cs similarity index 100% rename from Editor/SceneLayerUsers.cs rename to Editor/LayerUsers/SceneLayerUsers.cs diff --git a/Editor/SceneLayerUsers.cs.meta b/Editor/LayerUsers/SceneLayerUsers.cs.meta similarity index 100% rename from Editor/SceneLayerUsers.cs.meta rename to Editor/LayerUsers/SceneLayerUsers.cs.meta From befc32b929db8ba68de2e8c8e0da31dc7485e40a Mon Sep 17 00:00:00 2001 From: Matt Schoen Date: Tue, 31 May 2022 02:02:08 -0700 Subject: [PATCH 09/14] Add Scene Tag Users window; Misc cleanup; Update .plan --- Editor/.plan | 1 + Editor/LayerUsers/PrefabLayerUsers.cs | 6 +- Editor/LayerUsers/SceneLayerUsers.cs | 20 +- Editor/LayerUsers/SceneTagUsers.cs | 579 ++++++++++++++++++++++ Editor/LayerUsers/SceneTagUsers.cs.meta | 11 + Editor/Unity.Labs.SuperScience.Editor.api | 5 + 6 files changed, 609 insertions(+), 13 deletions(-) create mode 100644 Editor/LayerUsers/SceneTagUsers.cs create mode 100644 Editor/LayerUsers/SceneTagUsers.cs.meta diff --git a/Editor/.plan b/Editor/.plan index 4ce52d1..fe80a17 100644 --- a/Editor/.plan +++ b/Editor/.plan @@ -6,3 +6,4 @@ Rename MissingReferenceWindow base class to AnalysisWindow and refactor to share Split out Folder, GameObjectContainer, etc. types into their own files, share among all analysis windows Create options to allow Layer Usages windows to skip scanning for layer masks (SerializedProperty access is the bottleneck) Allow filtering Layer Usages with multiple layers +Use rich text for GameObject labels (bold tag or layer, etc.) \ No newline at end of file diff --git a/Editor/LayerUsers/PrefabLayerUsers.cs b/Editor/LayerUsers/PrefabLayerUsers.cs index f498ab8..07a3ed9 100644 --- a/Editor/LayerUsers/PrefabLayerUsers.cs +++ b/Editor/LayerUsers/PrefabLayerUsers.cs @@ -622,13 +622,13 @@ void FindLayerUsersInPrefab(string path, GameObject prefabAsset) k_LayerUnionHashSet.UnionWith(layerMaskLayers); foreach (var layer in k_LayerUnionHashSet) { - var filterRow = GetOrCreateFilterRowSetForLayer(layer); + var filterRow = GetOrCreateFilterRowForLayer(layer); filterRow.AllUsers.Add(prefabAsset); } foreach (var layer in gameObjectLayers) { - var filterRow = GetOrCreateFilterRowSetForLayer(layer); + var filterRow = GetOrCreateFilterRowForLayer(layer); filterRow.UsersWithoutLayerMasks.Add(prefabAsset); } } @@ -794,7 +794,7 @@ void Scan() /// /// The layer value to use for this row. /// The row for the layer value. - FilterRow GetOrCreateFilterRowSetForLayer(int layer) + FilterRow GetOrCreateFilterRowForLayer(int layer) { if (m_FilterRows.TryGetValue(layer, out var filterRow)) return filterRow; diff --git a/Editor/LayerUsers/SceneLayerUsers.cs b/Editor/LayerUsers/SceneLayerUsers.cs index c5f37c7..7aba868 100644 --- a/Editor/LayerUsers/SceneLayerUsers.cs +++ b/Editor/LayerUsers/SceneLayerUsers.cs @@ -398,7 +398,7 @@ public static GameObjectContainer CreateIfNecessary(GameObject gameObject, Sorte { if (isLayerUser) { - var filterRow = GetOrCreateFilterRowSetForLayer(filterRows, layer); + var filterRow = GetOrCreateFilterRowForLayer(filterRows, layer); filterRow.UsersWithoutLayerMasks.Add(gameObject); filterRow.AllUsers.Add(gameObject); } @@ -408,7 +408,7 @@ public static GameObjectContainer CreateIfNecessary(GameObject gameObject, Sorte { foreach (var kvp in newContainer.m_UsagesInComponentsPerLayer) { - var filterRow = GetOrCreateFilterRowSetForLayer(filterRows, kvp.Key); + var filterRow = GetOrCreateFilterRowForLayer(filterRows, kvp.Key); filterRow.AllUsers.Add(gameObject); } } @@ -614,24 +614,24 @@ int GetChildCount(int layerFilter = k_InvalidLayer, bool includeLayerMaskFields return totalWithoutLayerMasks; } - void AggregateCount(GameObjectContainer child, ref int layerUsagesInChildren, - ref int layerUsagesInChildrenWithoutLayerMasks) + void AggregateCount(GameObjectContainer child, ref int usagesInChildren, + ref int usagesInChildrenWithoutLayerMasks) { var layer = child.GameObject.layer; if (layer != 0) { - layerUsagesInChildren++; - layerUsagesInChildrenWithoutLayerMasks++; + usagesInChildren++; + usagesInChildrenWithoutLayerMasks++; IncrementCountForLayer(layer, m_UsagesInChildrenPerLayer); IncrementCountForLayer(layer, m_UsagesInChildrenWithoutLayerMasksPerLayer); } - layerUsagesInChildren += child.m_TotalUsagesInChildren; - layerUsagesInChildrenWithoutLayerMasks += child.m_TotalUsagesInChildrenWithoutLayerMasks; + usagesInChildren += child.m_TotalUsagesInChildren; + usagesInChildrenWithoutLayerMasks += child.m_TotalUsagesInChildrenWithoutLayerMasks; AggregateCountPerLayer(child.m_UsagesInChildrenPerLayer, m_UsagesInChildrenPerLayer); AggregateCountPerLayer(child.m_UsagesInChildrenWithoutLayerMasksPerLayer, m_UsagesInChildrenWithoutLayerMasksPerLayer); - layerUsagesInChildren += child.m_TotalUsagesInComponents; + usagesInChildren += child.m_TotalUsagesInComponents; AggregateCountPerLayer(child.m_UsagesInComponentsPerLayer, m_UsagesInChildrenPerLayer); } @@ -888,7 +888,7 @@ void ScanScene(Scene scene) /// Dictionary of FilterRow objects for counting usages per layer. /// The layer value to use for this row. /// The row for the layer value. - static FilterRow GetOrCreateFilterRowSetForLayer(SortedDictionary filterRows, int layer) + static FilterRow GetOrCreateFilterRowForLayer(SortedDictionary filterRows, int layer) { if (filterRows.TryGetValue(layer, out var filterRow)) return filterRow; diff --git a/Editor/LayerUsers/SceneTagUsers.cs b/Editor/LayerUsers/SceneTagUsers.cs new file mode 100644 index 0000000..a2df48a --- /dev/null +++ b/Editor/LayerUsers/SceneTagUsers.cs @@ -0,0 +1,579 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UnityEditor; +using UnityEditor.Experimental.SceneManagement; +using UnityEngine; +using UnityEngine.SceneManagement; + +namespace Unity.Labs.SuperScience +{ + /// + /// Scans all loaded scenes for GameObjects which use custom tags and displays the results in an EditorWindow. + /// + public class SceneTagUsers : EditorWindow + { + class SceneContainer + { + const string k_UntitledSceneName = "Untitled"; + + readonly string m_SceneName; + readonly List m_Roots; + readonly int m_TotalCount; + readonly SortedDictionary m_CountPerTag; + readonly string[] m_Usages; + + bool m_Expanded; + + SceneContainer(string sceneName, List roots) + { + m_SceneName = sceneName; + m_Roots = roots; + m_CountPerTag = new SortedDictionary(); + foreach (var container in roots) + { + var gameObject = container.GameObject; + var tag = gameObject.tag; + if (!gameObject.CompareTag(k_UntaggedString)) + { + m_TotalCount++; + IncrementCountForTag(tag, m_CountPerTag); + } + + m_TotalCount += container.TotalUsagesInChildren; + AggregateCountPerTag(container.UsagesInChildrenPerTag, m_CountPerTag); + } + + m_Usages = m_CountPerTag.Keys.ToArray(); + } + + public void Draw(string tagFilter) + { + var count = GetCount(tagFilter); + if (count == 0) + return; + + k_StringBuilder.Length = 0; + k_StringBuilder.Append(m_SceneName); + k_StringBuilder.Append(" ("); + k_StringBuilder.Append(count.ToString()); + k_StringBuilder.Append(") {"); + AppendTagNameList(k_StringBuilder, m_Usages, tagFilter); + k_StringBuilder.Append("}"); + var label = k_StringBuilder.ToString(); + var wasExpanded = m_Expanded; + m_Expanded = EditorGUILayout.Foldout(m_Expanded, label, true); + + // Hold alt to apply expanded state to all children (recursively) + if (m_Expanded != wasExpanded && Event.current.alt) + { + foreach (var gameObjectContainer in m_Roots) + { + gameObjectContainer.SetExpandedRecursively(m_Expanded); + } + } + + if (!m_Expanded) + return; + + using (new EditorGUI.IndentLevelScope()) + { + foreach (var gameObjectContainer in m_Roots) + { + gameObjectContainer.Draw(tagFilter); + } + } + } + + public static SceneContainer CreateIfNecessary(Scene scene, SortedDictionary> filterRows) + { + var sceneName = scene.name; + if (string.IsNullOrEmpty(sceneName)) + sceneName = k_UntitledSceneName; + + List roots = null; + foreach (var gameObject in scene.GetRootGameObjects()) + { + var rootContainer = GameObjectContainer.CreateIfNecessary(gameObject, filterRows); + if (rootContainer != null) + { + roots ??= new List(); + roots.Add(rootContainer); + } + } + + return roots != null ? new SceneContainer(sceneName, roots) : null; + } + + public int GetCount(string tagFilter = null) + { + var unfiltered = string.IsNullOrEmpty(tagFilter); + if (unfiltered) + return m_TotalCount; + + m_CountPerTag.TryGetValue(tagFilter, out var count); + return count; + } + } + + /// + /// Tree structure for GameObject scan results + /// When the Scan method encounters a GameObject in a scene or a prefab in the project, we initialize one of + /// these using the GameObject as an argument. This scans the object and its components/children, retaining + /// the results for display in the GUI. The window calls into these helper objects to draw them, as well. + /// + class GameObjectContainer + { + readonly GameObject m_GameObject; + readonly List m_Children; + + //TODO: Rename Users -> Usages + readonly int m_TotalUsagesInChildren; + readonly SortedDictionary m_UsagesInChildrenPerTag; + + readonly string[] m_Usages; + + bool m_Expanded; + + public GameObject GameObject { get { return m_GameObject; } } + public int TotalUsagesInChildren => m_TotalUsagesInChildren; + public SortedDictionary UsagesInChildrenPerTag => m_UsagesInChildrenPerTag; + + // Local method use only -- created here to reduce garbage collection. Collections must be cleared before use + static readonly SortedSet k_Usages = new SortedSet(); + + GameObjectContainer(GameObject gameObject, List children) + { + m_GameObject = gameObject; + m_Children = children; + k_Usages.Clear(); + + var tag = gameObject.tag; + if (!gameObject.CompareTag(k_UntaggedString)) + k_Usages.Add(tag); + + if (children != null) + { + m_UsagesInChildrenPerTag = new SortedDictionary(); + foreach (var container in children) + { + AggregateCount(container, ref m_TotalUsagesInChildren); + } + + k_Usages.UnionWith(m_UsagesInChildrenPerTag.Keys); + } + + m_Usages = k_Usages.ToArray(); + } + + /// + /// Initialize a GameObjectContainer to represent the given GameObject + /// This will scan the component for missing references and retain the information for display in + /// the given window. + /// + /// The GameObject to scan for missing references + /// Dictionary of HashSet>GameObject< objects for counting usages per tag. + public static GameObjectContainer CreateIfNecessary(GameObject gameObject, SortedDictionary> filterRows) + { + List children = null; + foreach (Transform child in gameObject.transform) + { + var childContainer = CreateIfNecessary(child.gameObject, filterRows); + if (childContainer != null) + { + children ??= new List(); + children.Add(childContainer); + } + } + + var tag = gameObject.tag; + var isTagUser = !gameObject.CompareTag(k_UntaggedString); + if (isTagUser || children != null) + { + if (isTagUser) + { + var hashSet = GetOrCreateHashSetForTag(filterRows, tag); + hashSet.Add(gameObject); + } + + return new GameObjectContainer(gameObject, children); + } + + return null; + } + + /// + /// Draw tag usage information for this GameObjectContainer + /// + public void Draw(string tagFilter = null) + { + var tag = m_GameObject.tag; + var isTagUser = string.IsNullOrEmpty(tagFilter) ? !m_GameObject.CompareTag(k_UntaggedString) : m_GameObject.CompareTag(tagFilter); + + var childCount = GetChildCount(tagFilter); + var hasChildren = childCount > 0; + if (!(isTagUser || hasChildren)) + return; + + var label = GetLabel(tagFilter, childCount, isTagUser, tag); + var wasExpanded = m_Expanded; + if (hasChildren) + m_Expanded = EditorGUILayout.Foldout(m_Expanded, label, true); + else + EditorGUILayout.LabelField(label); + + + // Hold alt to apply expanded state to all children (recursively) + if (m_Expanded != wasExpanded && Event.current.alt) + { + if (m_Children != null) + { + foreach (var gameObjectContainer in m_Children) + { + gameObjectContainer.SetExpandedRecursively(m_Expanded); + } + } + } + + using (new EditorGUI.IndentLevelScope()) + { + if (isTagUser) + EditorGUILayout.ObjectField(m_GameObject, typeof(GameObject), true); + + if (!m_Expanded) + return; + + if (hasChildren && m_Children != null) + { + foreach (var child in m_Children) + { + child.Draw(tagFilter); + } + } + } + } + + string GetLabel(string tagFilter, int count, bool isTagUser, string tag) + { + k_StringBuilder.Length = 0; + k_StringBuilder.Append(m_GameObject.name); + k_StringBuilder.Append(" ("); + k_StringBuilder.Append(count.ToString()); + if (isTagUser) + { + k_StringBuilder.Append(") - Tag: "); + k_StringBuilder.Append(tag); + k_StringBuilder.Append(" {"); + } + else + { + k_StringBuilder.Append(") {"); + } + + AppendTagNameList(k_StringBuilder, m_Usages, tagFilter); + k_StringBuilder.Append("}"); + + return k_StringBuilder.ToString(); + } + + /// + /// Set the expanded state of this object and all of its children + /// + /// Whether this object should be expanded in the GUI + public void SetExpandedRecursively(bool expanded) + { + m_Expanded = expanded; + if (m_Children != null) + { + foreach (var child in m_Children) + { + child.SetExpandedRecursively(expanded); + } + } + } + + int GetChildCount(string tagFilter = null) + { + if (m_Children == null) + return 0; + + var unfiltered = string.IsNullOrEmpty(tagFilter); + if (unfiltered) + return m_TotalUsagesInChildren; + + m_UsagesInChildrenPerTag.TryGetValue(tagFilter, out var totalCount); + return totalCount; + } + + void AggregateCount(GameObjectContainer child, ref int usagesInChildren) + { + var gameObject = child.m_GameObject; + var tag = gameObject.tag; + if (!gameObject.CompareTag(k_UntaggedString)) + { + usagesInChildren++; + IncrementCountForTag(tag, m_UsagesInChildrenPerTag); + } + + usagesInChildren += child.m_TotalUsagesInChildren; + AggregateCountPerTag(child.m_UsagesInChildrenPerTag, m_UsagesInChildrenPerTag); + } + } + + static class Styles + { + internal static readonly GUIStyle ActiveFilterButton = new GUIStyle(GUI.skin.button) + { + alignment = TextAnchor.MiddleLeft, + fontStyle = FontStyle.Bold + }; + + internal static readonly GUIStyle InactiveFilterButton = new GUIStyle(GUI.skin.button) + { + alignment = TextAnchor.MiddleLeft + }; + } + + const string k_MenuItemName = "Window/SuperScience/Scene Tag Users"; + const string k_WindowTitle = "Scene Tag Users"; + const string k_NoUsages = "No scene objects using any custom tags"; + const int k_FilterPanelWidth = 180; + const int k_ObjectFieldWidth = 150; + const string k_Instructions = "Click the Scan button to scan your project for users of custom tags. WARNING: " + + "This will load every prefab in your project. For large projects, this may take a long time and/or crash the Editor."; + const int k_ProgressBarHeight = 15; + const string k_UntaggedString = "Untagged"; + + static readonly GUIContent k_ScanGUIContent = new GUIContent("Scan", "Scan the loaded scenes for usages of custom tags"); + static readonly GUIContent k_CancelGUIContent = new GUIContent("Cancel", "Cancel the current scan"); + static readonly GUILayoutOption k_FilterPanelWidthOption = GUILayout.Width(k_FilterPanelWidth); + static readonly Vector2 k_MinSize = new Vector2(400, 200); + + Vector2 m_ColorListScrollPosition; + Vector2 m_FolderTreeScrollPosition; + readonly List m_SceneContainers = new List(); + readonly SortedDictionary> m_FilterRows = new SortedDictionary>(); + int m_ScanCount; + int m_ScanProgress; + IEnumerator m_ScanEnumerator; + + [NonSerialized] + bool m_Scanned; + + [SerializeField] + string m_TagFilter; + + // Local method use only -- created here to reduce garbage collection. Collections must be cleared before use + static readonly StringBuilder k_StringBuilder = new StringBuilder(4096); + + /// + /// Initialize the window + /// + [MenuItem(k_MenuItemName)] + static void Init() + { + GetWindow(k_WindowTitle).Show(); + } + + void OnEnable() + { + minSize = k_MinSize; + m_ScanCount = 0; + m_ScanProgress = 0; + } + + void OnDisable() + { + m_ScanEnumerator = null; + } + + void OnGUI() + { + EditorGUIUtility.labelWidth = position.width - k_FilterPanelWidth - k_ObjectFieldWidth; + + if (m_ScanEnumerator == null) + { + if (GUILayout.Button(k_ScanGUIContent)) + Scan(); + } + else + { + if (GUILayout.Button(k_CancelGUIContent)) + m_ScanEnumerator = null; + } + + if (!m_Scanned) + { + EditorGUILayout.HelpBox(k_Instructions, MessageType.Info); + GUIUtility.ExitGUI(); + return; + } + + using (new GUILayout.HorizontalScope()) + { + using (new GUILayout.VerticalScope(k_FilterPanelWidthOption)) + { + DrawFilters(); + } + + using (new GUILayout.VerticalScope()) + { + var count = 0; + foreach (var container in m_SceneContainers) + { + count += container.GetCount(m_TagFilter); + } + + if (count == 0) + { + GUILayout.Label(k_NoUsages); + } + else + { + using (var scrollView = new GUILayout.ScrollViewScope(m_FolderTreeScrollPosition)) + { + m_FolderTreeScrollPosition = scrollView.scrollPosition; + foreach (var container in m_SceneContainers) + { + container.Draw(m_TagFilter); + } + } + } + } + } + + if (m_ScanCount > 0 && m_ScanCount - m_ScanProgress > 0) + { + var rect = GUILayoutUtility.GetRect(0, float.PositiveInfinity, k_ProgressBarHeight, k_ProgressBarHeight); + EditorGUI.ProgressBar(rect, (float) m_ScanProgress / m_ScanCount, $"{m_ScanProgress} / {m_ScanCount}"); + } + } + + /// + /// Draw a list buttons for filtering based on tag. + /// + void DrawFilters() + { + var count = 0; + foreach (var container in m_SceneContainers) + { + count += container.GetCount(); + } + + var style = string.IsNullOrEmpty(m_TagFilter) ? Styles.ActiveFilterButton : Styles.InactiveFilterButton; + if (GUILayout.Button($"All ({count})", style)) + m_TagFilter = null; + + using (var scrollView = new GUILayout.ScrollViewScope(m_ColorListScrollPosition)) + { + m_ColorListScrollPosition = scrollView.scrollPosition; + foreach (var kvp in m_FilterRows) + { + var tag = kvp.Key; + count = 0; + if (m_FilterRows.TryGetValue(tag, out var hashSet)) + count = hashSet.Count; + + style = m_TagFilter == tag ? Styles.ActiveFilterButton : Styles.InactiveFilterButton; + if (GUILayout.Button($"{tag}: ({count})", style)) + m_TagFilter = tag; + } + } + } + + /// + /// Scan the project for tag usages and populate the data structures for UI. + /// + void Scan() + { + m_Scanned = true; + m_SceneContainers.Clear(); + + // If we are in prefab isolation mode, scan the prefab stage instead of the active scene + var prefabStage = PrefabStageUtility.GetCurrentPrefabStage(); + if (prefabStage != null) + { + ScanScene(prefabStage.scene); + return; + } + + var loadedSceneCount = SceneManager.sceneCount; + for (var i = 0; i < loadedSceneCount; i++) + { + var scene = SceneManager.GetSceneAt(i); + if (!scene.IsValid()) + continue; + + ScanScene(scene); + } + } + + void ScanScene(Scene scene) + { + var sceneContainer = SceneContainer.CreateIfNecessary(scene, m_FilterRows); + if (sceneContainer != null) + m_SceneContainers.Add(sceneContainer); + } + + /// + /// Get or create a HashSet>GameObject< for a given tag. + /// + /// Dictionary of HashSet>GameObject< objects for counting usages per tag. + /// The tag to use for this row. + /// The HashSet>GameObject< for the tag. + static HashSet GetOrCreateHashSetForTag(SortedDictionary> filterRows, string tag) + { + if (filterRows.TryGetValue(tag, out var filterRow)) + return filterRow; + + filterRow = new HashSet(); + filterRows[tag] = filterRow; + return filterRow; + } + + static void AppendTagNameList(StringBuilder stringBuilder, string[] tags, string tagFilter = null) + { + if (!string.IsNullOrEmpty(tagFilter)) + { + stringBuilder.Append(tagFilter); + return; + } + + var length = tags.Length; + if (length == 0) + return; + + var lengthMinusOne = length - 1; + for (var i = 0; i < lengthMinusOne; i++) + { + stringBuilder.Append(tags[i]); + stringBuilder.Append(", "); + } + + stringBuilder.Append(tags[lengthMinusOne]); + } + + static void IncrementCountForTag(string tag, SortedDictionary countPerTag) + { + countPerTag.TryGetValue(tag, out var count); + count++; + countPerTag[tag] = count; + } + + static void AggregateCountPerTag(SortedDictionary source, SortedDictionary destination) + { + if (source == null) + return; + + foreach (var kvp in source) + { + var tag = kvp.Key; + destination.TryGetValue(tag, out var count); + count += kvp.Value; + destination[tag] = count; + } + } + } +} diff --git a/Editor/LayerUsers/SceneTagUsers.cs.meta b/Editor/LayerUsers/SceneTagUsers.cs.meta new file mode 100644 index 0000000..c4bf978 --- /dev/null +++ b/Editor/LayerUsers/SceneTagUsers.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e30d9050684006c45ba69e4fa9d43e51 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Editor/Unity.Labs.SuperScience.Editor.api b/Editor/Unity.Labs.SuperScience.Editor.api index 9c512be..afcaafb 100644 --- a/Editor/Unity.Labs.SuperScience.Editor.api +++ b/Editor/Unity.Labs.SuperScience.Editor.api @@ -48,4 +48,9 @@ namespace Unity.Labs.SuperScience { public SceneLayerUsers() {} } + + public class SceneTagUsers : UnityEditor.EditorWindow + { + public SceneTagUsers() {} + } } From fd81044050340c80125ece26426589e8e9b8bb99 Mon Sep 17 00:00:00 2001 From: Matt Schoen Date: Tue, 31 May 2022 02:14:52 -0700 Subject: [PATCH 10/14] Fix an issue in PrefabTagUsers where the entire view would be hidden if showing a filter with no results --- Editor/LayerUsers/PrefabTagUsers.cs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/Editor/LayerUsers/PrefabTagUsers.cs b/Editor/LayerUsers/PrefabTagUsers.cs index feec259..a44d013 100644 --- a/Editor/LayerUsers/PrefabTagUsers.cs +++ b/Editor/LayerUsers/PrefabTagUsers.cs @@ -351,19 +351,19 @@ void OnGUI() return; } - if (m_ParentFolder.GetCount(m_TagFilter) == 0) + using (new GUILayout.HorizontalScope()) { - GUILayout.Label(k_NoTagUsers); - } - else - { - using (new GUILayout.HorizontalScope()) + using (new GUILayout.VerticalScope(k_FilterPanelWidthOption)) + { + DrawFilters(); + } + + if (m_ParentFolder.GetCount(m_TagFilter) == 0) + { + GUILayout.Label(k_NoTagUsers); + } + else { - using (new GUILayout.VerticalScope(k_FilterPanelWidthOption)) - { - DrawFilters(); - } - using (var scrollView = new GUILayout.ScrollViewScope(m_FolderTreeScrollPosition)) { m_FolderTreeScrollPosition = scrollView.scrollPosition; @@ -375,7 +375,7 @@ void OnGUI() if (m_ScanCount > 0 && m_ScanCount - m_ScanProgress > 0) { var rect = GUILayoutUtility.GetRect(0, float.PositiveInfinity, k_ProgressBarHeight, k_ProgressBarHeight); - EditorGUI.ProgressBar(rect, (float)m_ScanProgress / m_ScanCount, $"{m_ScanProgress} / {m_ScanCount}"); + EditorGUI.ProgressBar(rect, (float) m_ScanProgress / m_ScanCount, $"{m_ScanProgress} / {m_ScanCount}"); } } From 59166bea669397e007a0af3984b9ddff05a4379a Mon Sep 17 00:00:00 2001 From: Matt Schoen Date: Thu, 2 Jun 2022 18:41:14 -0700 Subject: [PATCH 11/14] Show unused project tags in the filter list --- Editor/LayerUsers/PrefabTagUsers.cs | 22 ++++++++++++++++++++++ Editor/LayerUsers/SceneTagUsers.cs | 21 +++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/Editor/LayerUsers/PrefabTagUsers.cs b/Editor/LayerUsers/PrefabTagUsers.cs index a44d013..66f4ba8 100644 --- a/Editor/LayerUsers/PrefabTagUsers.cs +++ b/Editor/LayerUsers/PrefabTagUsers.cs @@ -2,9 +2,11 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.Linq; using System.Text; using UnityEditor; using UnityEditor.Experimental.SceneManagement; +using UnityEditorInternal; using UnityEngine; using Debug = UnityEngine.Debug; @@ -287,6 +289,16 @@ static class Styles static readonly GUIContent k_CancelGUIContent = new GUIContent("Cancel", "Cancel the current scan"); static readonly GUILayoutOption k_FilterPanelWidthOption = GUILayout.Width(k_FilterPanelWidth); static readonly Vector2 k_MinSize = new Vector2(400, 200); + static readonly HashSet k_BuiltInTags = new HashSet + { + k_UntaggedString, + "Respawn", + "Finish", + "EditorOnly", + "MainCamera", + "Player", + "GameController", + }; static readonly Stopwatch k_StopWatch = new Stopwatch(); @@ -510,6 +522,16 @@ void Scan() return; m_FilterRows.Clear(); + + // Add all tags to FilterRows to include tags with no users + foreach (var tag in InternalEditorUtility.tags) + { + if (k_BuiltInTags.Contains(tag)) + continue; + + m_FilterRows[tag] = new HashSet(); + } + m_ParentFolder.Clear(); var prefabAssets = new List<(string, GameObject)>(); foreach (var guid in guids) diff --git a/Editor/LayerUsers/SceneTagUsers.cs b/Editor/LayerUsers/SceneTagUsers.cs index a2df48a..6367a36 100644 --- a/Editor/LayerUsers/SceneTagUsers.cs +++ b/Editor/LayerUsers/SceneTagUsers.cs @@ -5,6 +5,7 @@ using System.Text; using UnityEditor; using UnityEditor.Experimental.SceneManagement; +using UnityEditorInternal; using UnityEngine; using UnityEngine.SceneManagement; @@ -350,6 +351,16 @@ static class Styles static readonly GUIContent k_CancelGUIContent = new GUIContent("Cancel", "Cancel the current scan"); static readonly GUILayoutOption k_FilterPanelWidthOption = GUILayout.Width(k_FilterPanelWidth); static readonly Vector2 k_MinSize = new Vector2(400, 200); + static readonly HashSet k_BuiltInTags = new HashSet + { + k_UntaggedString, + "Respawn", + "Finish", + "EditorOnly", + "MainCamera", + "Player", + "GameController", + }; Vector2 m_ColorListScrollPosition; Vector2 m_FolderTreeScrollPosition; @@ -490,6 +501,16 @@ void Scan() { m_Scanned = true; m_SceneContainers.Clear(); + m_FilterRows.Clear(); + + // Add all tags to FilterRows to include tags with no users + foreach (var tag in InternalEditorUtility.tags) + { + if (k_BuiltInTags.Contains(tag)) + continue; + + m_FilterRows[tag] = new HashSet(); + } // If we are in prefab isolation mode, scan the prefab stage instead of the active scene var prefabStage = PrefabStageUtility.GetCurrentPrefabStage(); From eab4a6cd88281a9b279cbac1e0a4d15eec153ccb Mon Sep 17 00:00:00 2001 From: Matt Schoen Date: Sun, 5 Jun 2022 18:28:12 -0700 Subject: [PATCH 12/14] Improvements to GlobalNamespaceWatcher --- Editor/GlobalNamespaceWatcher.cs | 91 +++++++++++++++++++++++++++++--- 1 file changed, 84 insertions(+), 7 deletions(-) diff --git a/Editor/GlobalNamespaceWatcher.cs b/Editor/GlobalNamespaceWatcher.cs index 37526a1..b61b153 100644 --- a/Editor/GlobalNamespaceWatcher.cs +++ b/Editor/GlobalNamespaceWatcher.cs @@ -21,6 +21,11 @@ public class GlobalNamespaceWatcher : EditorWindow /// class AssemblyRow { + static readonly GUIContent k_OpenGUIContent = new GUIContent("Open", "Open this script in the default script editor."); + static readonly GUIContent k_OpenAllGUIContent = new GUIContent("Open All", "Open all scripts in this assembly's " + + "global namespace in the default script editor.\nWARNING: This process can lock the Editor for a long time and cannot be canceled."); + static readonly GUILayoutOption k_OpenButtonWidth = GUILayout.Width(100); + /// /// The path to the assembly, sourced from Assembly.Location. /// @@ -46,6 +51,11 @@ class AssemblyRow /// public int MonoScriptTypeCount => m_MonoScriptCount; + /// + /// The types in this assembly. + /// + public SortedList Types => m_Types; + /// /// Draw this assembly row to the GUI. /// @@ -54,7 +64,25 @@ class AssemblyRow public void Draw(string assemblyName, bool showOnlyMonoScriptTypes = false) { var count = showOnlyMonoScriptTypes ? m_MonoScriptCount : m_Types.Count; - m_Expanded = EditorGUILayout.Foldout(m_Expanded, $"{assemblyName}: ({count})", true); + using (new EditorGUILayout.HorizontalScope()) + { + m_Expanded = EditorGUILayout.Foldout(m_Expanded, $"{assemblyName}: ({count})", true); + using (new EditorGUI.DisabledScope(m_MonoScriptCount == 0)) + { + if (GUILayout.Button(k_OpenAllGUIContent, k_OpenButtonWidth)) + { + foreach (var kvp in m_Types) + { + var monoScript = kvp.Value; + if (monoScript == null) + continue; + + AssetDatabase.OpenAsset(monoScript); + } + } + } + } + if (m_Expanded) { using (new EditorGUI.IndentLevelScope()) @@ -64,18 +92,33 @@ public void Draw(string assemblyName, bool showOnlyMonoScriptTypes = false) { foreach (var kvp in m_Types) { + var label = kvp.Key; var monoScript = kvp.Value; - if (showOnlyMonoScriptTypes && monoScript == null) - continue; - - EditorGUILayout.LabelField(kvp.Key); - EditorGUILayout.ObjectField(monoScript, typeof(MonoScript), false); + DrawScript(showOnlyMonoScriptTypes, monoScript, label); } } } } } + static void DrawScript(bool showOnlyMonoScriptTypes, MonoScript monoScript, string label) + { + if (showOnlyMonoScriptTypes && monoScript == null) + return; + + using (new EditorGUILayout.HorizontalScope()) + { + EditorGUILayout.LabelField(label); + using (new EditorGUI.DisabledScope(monoScript == null)) + { + if (GUILayout.Button(k_OpenGUIContent, k_OpenButtonWidth)) + AssetDatabase.OpenAsset(monoScript); + } + } + + EditorGUILayout.ObjectField(monoScript, typeof(MonoScript), false); + } + /// /// Add a type to this assembly row. /// The type will be stored in a dictionary, and if there is a MonoScript, we increment a counter to show if only MonoScript types will be shown. @@ -84,10 +127,16 @@ public void Draw(string assemblyName, bool showOnlyMonoScriptTypes = false) /// An associated MonoScript, if one exists. public void AddType(string typeName, MonoScript monoScript) { + var label = typeName; if (monoScript != null) + { m_MonoScriptCount++; + var path = AssetDatabase.GetAssetPath(monoScript); + if (!string.IsNullOrEmpty(path)) + label = path; + } - m_Types.Add(typeName, monoScript); + m_Types.Add(label, monoScript); } } @@ -99,7 +148,11 @@ public void AddType(string typeName, MonoScript monoScript) const int k_LabelWidth = 200; const string k_CongratulationsLabel = "Congratulations! There are no types in the global namespace. :)"; + static readonly GUIContent k_OpenEverythingGUIContent = new GUIContent("Open Everything", "Open all scripts in the " + + "global namespace in the default script editor.\nWARNING: This process can lock the Editor for a long time and cannot be canceled."); + static SortedList s_Assemblies; + static int s_TotalMonoScriptCount; [SerializeField] Vector2 m_ScrollPosition; @@ -120,6 +173,9 @@ void OnEnable() { if (s_Assemblies == null) { + // Reset total count, just in case it's gone out of sync with s_Assemblies. + s_TotalMonoScriptCount = 0; + // Prepare a map of MonoScript types for fast access. var monoScripts = MonoImporter.GetAllRuntimeMonoScripts(); var monoScriptDictionary = new Dictionary(monoScripts.Length); @@ -190,7 +246,10 @@ void OnEnable() } if (addedType) + { s_Assemblies.Add(assemblyName, row); + s_TotalMonoScriptCount += row.MonoScriptTypeCount; + } } catch { @@ -209,6 +268,24 @@ void OnGUI() m_ShowOnlyProjectAssemblies = EditorGUILayout.Toggle(k_ShowOnlyProjectAssembliesLabel, m_ShowOnlyProjectAssemblies); m_ShowOnlyMonoScriptTypes = EditorGUILayout.Toggle(k_ShowOnlyMonoScriptTypesLabel, m_ShowOnlyMonoScriptTypes); + using (new EditorGUI.DisabledScope(s_TotalMonoScriptCount == 0)) + { + if (GUILayout.Button(k_OpenEverythingGUIContent)) + { + foreach (var kvp in s_Assemblies) + { + foreach (var kvp2 in kvp.Value.Types) + { + var monoScript = kvp2.Value; + if (monoScript != null) + continue; + + AssetDatabase.OpenAsset(monoScript); + } + } + } + } + // Give users convenient buttons to expand/collapse the assembly rows using (new EditorGUILayout.HorizontalScope()) { From 683b3cc1a8663566dbb628096cc4a4a25975bb07 Mon Sep 17 00:00:00 2001 From: Matt Schoen Date: Mon, 6 Jun 2022 17:13:16 -0700 Subject: [PATCH 13/14] Fix null reference exceptions in MissingProjectReferences --- Editor/MissingReferences/MissingProjectReferences.cs | 3 +++ Editor/MissingReferences/MissingReferencesWindow.cs | 3 +++ 2 files changed, 6 insertions(+) diff --git a/Editor/MissingReferences/MissingProjectReferences.cs b/Editor/MissingReferences/MissingProjectReferences.cs index 51d4e7f..c5efb4f 100644 --- a/Editor/MissingReferences/MissingProjectReferences.cs +++ b/Editor/MissingReferences/MissingProjectReferences.cs @@ -107,6 +107,9 @@ public override void Draw() { DrawPropertiesWithMissingReferences(m_PropertiesWithMissingReferences); + if (m_SubAssets == null) + return; + var count = m_SubAssets.Count; if (count == 0) return; diff --git a/Editor/MissingReferences/MissingReferencesWindow.cs b/Editor/MissingReferences/MissingReferencesWindow.cs index 19ba81a..e204d03 100644 --- a/Editor/MissingReferences/MissingReferencesWindow.cs +++ b/Editor/MissingReferences/MissingReferencesWindow.cs @@ -547,6 +547,9 @@ static bool CheckForMissingReferences(SerializedProperty property, Options optio /// A list of SerializedProperty objects known to have missing references internal static void DrawPropertiesWithMissingReferences(List properties) { + if (properties == null) + return; + foreach (var property in properties) { switch (property.propertyType) From 8a388df822a70de78b041c495253919041e11216 Mon Sep 17 00:00:00 2001 From: Matt Schoen Date: Mon, 6 Jun 2022 17:45:35 -0700 Subject: [PATCH 14/14] Early-out in Draw if m_GameObject is null (can happen on unloading a scene) --- Editor/MissingReferences/MissingReferencesWindow.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Editor/MissingReferences/MissingReferencesWindow.cs b/Editor/MissingReferences/MissingReferencesWindow.cs index e204d03..95114cb 100644 --- a/Editor/MissingReferences/MissingReferencesWindow.cs +++ b/Editor/MissingReferences/MissingReferencesWindow.cs @@ -273,6 +273,9 @@ public static GameObjectContainer CreateIfNecessary(GameObject gameObject, Optio /// public override void Draw() { + if (m_GameObject == null) + return; + var wasExpanded = m_Expanded; var label = string.Format(k_LabelFormat, m_GameObject.name, m_MissingReferences); if (m_IsMissingPrefab)