diff --git a/Editor/.plan b/Editor/.plan new file mode 100644 index 0000000..fe80a17 --- /dev/null +++ b/Editor/.plan @@ -0,0 +1,9 @@ +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 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 +Use rich text for GameObject labels (bold tag or layer, etc.) \ No newline at end of file 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()) { 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/LayerUsers/PrefabLayerUsers.cs b/Editor/LayerUsers/PrefabLayerUsers.cs new file mode 100644 index 0000000..07a3ed9 --- /dev/null +++ b/Editor/LayerUsers/PrefabLayerUsers.cs @@ -0,0 +1,868 @@ +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 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 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 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_TotalCountPerLayer = new SortedDictionary(); + readonly SortedDictionary m_TotalWithoutLayerMasksPerLayer = new SortedDictionary(); + int m_TotalCount; + int m_TotalWithoutLayerMasks; + bool m_Expanded; + + /// + /// Clear the contents of this container. + /// + public void Clear() + { + m_Subfolders.Clear(); + m_Prefabs.Clear(); + m_TotalCountPerLayer.Clear(); + m_TotalWithoutLayerMasksPerLayer.Clear(); + m_TotalCount = 0; + m_TotalWithoutLayerMasks = 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 layer user. + /// + /// A struct containing the prefab asset reference and its metadata + /// 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); + 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++) + { + 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(gameObjectLayers, layerMaskLayers); + } + + return folder; + } + + /// + /// Draw GUI for this Folder. + /// + /// The name of the folder. + /// 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 wasExpanded = m_Expanded; + var layerNameList = GetLayerNameList(m_TotalCountPerLayer.Keys, m_TotalWithoutLayerMasksPerLayer.Keys, layerToName, layerFilter, includeLayerMaskFields); + var label = $"{name}: {GetCount(layerFilter, includeLayerMaskFields)} {{{layerNameList}}}"; + m_Expanded = EditorGUILayout.Foldout(m_Expanded, label, true); + + DrawLineSeparator(); + + // Hold alt to apply expanded state to all children (recursively) + if (m_Expanded != wasExpanded && Event.current.alt) + SetExpandedRecursively(m_Expanded); + + if (!m_Expanded) + return; + + using (new EditorGUI.IndentLevelScope()) + { + foreach (var kvp in m_Subfolders) + { + var folder = kvp.Value; + if (folder.GetCount(layerFilter, includeLayerMaskFields) == 0) + continue; + + folder.Draw(kvp.Key, layerToName, layerFilter, includeLayerMaskFields); + } + + var showedPrefab = false; + foreach (var prefabRow in m_Prefabs) + { + var gameObjectLayers = prefabRow.GameObjectLayers; + var layerMaskLayers = prefabRow.LayerMaskLayers; + if (layerFilter == k_InvalidLayer || gameObjectLayers.Contains(layerFilter) || layerMaskLayers.Contains(layerFilter)) + { + prefabRow.Draw(layerToName, layerFilter, includeLayerMaskFields); + 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 expanded state of this folder, its contents and their children and all of its subfolders and their contents and children. + /// + /// Whether this object should be expanded in the GUI. + void SetExpandedRecursively(bool expanded) + { + m_Expanded = expanded; + foreach (var kvp in m_Subfolders) + { + kvp.Value.SetExpandedRecursively(expanded); + } + } + + /// + /// 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(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_TotalCountPerLayer.TryGetValue(layerFilter, out var totalCount); + return totalCount; + } + + m_TotalWithoutLayerMasksPerLayer.TryGetValue(layerFilter, out var totalWithoutLayerMasks); + return totalWithoutLayerMasks; + } + + void AggregateCount(SortedSet gameObjectLayers, SortedSet layerMaskLayers) + { + 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_TotalCountPerLayer.TryGetValue(layer, out var count); + count++; + m_TotalCountPerLayer[layer] = count; + } + + foreach (var layer in gameObjectLayers) + { + m_TotalWithoutLayerMasksPerLayer.TryGetValue(layer, out var count); + count++; + m_TotalWithoutLayerMasksPerLayer[layer] = count; + } + } + } + + 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.GameObject.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 GameObject; + public List LayerMaskComponents; + public SortedSet LayerMaskLayers; + + bool m_Expanded; + + public void Draw(Dictionary layerToName, int layerFilter = k_InvalidLayer, bool includeLayerMaskFields = true) + { + var layer = GameObject.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(GameObject, typeof(GameObject), true); + } + else + { + EditorGUILayout.ObjectField(label, GameObject, 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 Component; + public SortedSet UsedLayers; + + public void Draw(Dictionary layerToName, int layerFilter = k_InvalidLayer) + { + using (new GUILayout.HorizontalScope()) + { + var layerNameList = GetLayerNameList(UsedLayers, layerToName, layerFilter); + EditorGUILayout.ObjectField($"{Component.name} ({Component.GetType().Name}) {{{layerNameList}}}", Component, typeof(Component), true); + } + } + } + + 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 + }; + + 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 Layer Users"; + const string k_WindowTitle = "Prefab Layer Users"; + 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; + 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 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_FilterListScrollPosition; + 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; + 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(); + static readonly SortedSet k_LayerUnionHashSet = new SortedSet(); + 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; + } + + 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); + } + + if (m_ParentFolder.GetCount(m_LayerFilter, m_IncludeLayerMaskFields) == 0) + { + GUILayout.Label(k_NoLayerUsers); + } + else + { + 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 buttons for filtering based on layer. + /// + void DrawFilters() + { + 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_FilterListScrollPosition)) + { + m_FilterListScrollPosition = 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; + } + } + } + + /// + /// 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_ScanCount = prefabAssets.Count; + m_ScanProgress = 0; + foreach (var (path, prefabAsset) in prefabAssets) + { + FindLayerUsersInPrefab(path, prefabAsset); + m_ScanProgress++; + yield return null; + } + + m_ScanEnumerator = null; + EditorApplication.update -= UpdateScan; + } + + void FindLayerUsersInPrefab(string path, GameObject prefabAsset) + { + var gameObjectLayers = new SortedSet(); + var layerMaskLayers = new SortedSet(); + 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) + { + 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); + foreach (var layer in k_LayerUnionHashSet) + { + var filterRow = GetOrCreateFilterRowForLayer(layer); + filterRow.AllUsers.Add(prefabAsset); + } + + foreach (var layer in gameObjectLayers) + { + var filterRow = GetOrCreateFilterRowForLayer(layer); + filterRow.UsersWithoutLayerMasks.Add(prefabAsset); + } + } + } + + void FindLayerUsersRecursively(GameObject gameObject, SortedSet prefabGameObjectLayers, SortedSet prefabLayerMaskLayers, List layerUsers, ref int layerUsersWithoutLayerMasks) + { + var isLayerUser = false; + var layer = gameObject.layer; + if (layer != 0) + { + 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); + foreach (var component in k_Components) + { + 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; + + componentUsesLayers |= GetLayersFromLayerMask(usedLayers, iterator.intValue); + } + + isLayerUser |= componentUsesLayers; + + if (componentUsesLayers) + { + prefabLayerMaskLayers.UnionWith(usedLayers); + layerMaskLayers.UnionWith(usedLayers); + componentRows.Add(new ComponentRow + { + Component = 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), + GameObject = gameObject, + LayerMaskComponents = componentRows, + LayerMaskLayers = layerMaskLayers + }); + } + + foreach (Transform child in gameObject.transform) + { + FindLayerUsersRecursively(child.gameObject, prefabGameObjectLayers, prefabLayerMaskLayers, layerUsers, ref layerUsersWithoutLayerMasks); + } + } + + 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 false; + + // Exclude the special cases where every layer is included or excluded + if (layerMask == k_InvalidLayer || layerMask == 0) + 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; + + // 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) + { + isLayerMaskUser = true; + layers.Add(layer); + } + } + + return isLayerMaskUser; + } + + /// + /// Scan the project for layer 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(); + + m_LayerWithNoName = k_InvalidLayer; + m_LayerToName.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; + + 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 for a given layer value. + /// + /// The layer value to use for this row. + /// The row for the layer value. + FilterRow GetOrCreateFilterRowForLayer(int layer) + { + if (m_FilterRows.TryGetValue(layer, out var filterRow)) + return filterRow; + + 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_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(); + } + } +} diff --git a/Editor/LayerUsers/PrefabLayerUsers.cs.meta b/Editor/LayerUsers/PrefabLayerUsers.cs.meta new file mode 100644 index 0000000..e4eb1bf --- /dev/null +++ b/Editor/LayerUsers/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/LayerUsers/PrefabTagUsers.cs b/Editor/LayerUsers/PrefabTagUsers.cs new file mode 100644 index 0000000..66f4ba8 --- /dev/null +++ b/Editor/LayerUsers/PrefabTagUsers.cs @@ -0,0 +1,616 @@ +using System.Collections; +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; + +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_Expanded; + + /// + /// 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 wasExpanded = m_Expanded; + var tagList = GetTagList(m_CountPerTag.Keys, tagFilter); + var label = $"{name}: {m_TotalCount} {{{tagList}}}"; + m_Expanded = EditorGUILayout.Foldout(m_Expanded, label, true); + + DrawLineSeparator(); + + // Hold alt to apply expanded state to all children (recursively) + if (m_Expanded != wasExpanded && Event.current.alt) + SetExpandedRecursively(m_Expanded); + + if (!m_Expanded) + 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 expanded state of this folder, its contents and their children and all of its subfolders and their contents and children. + /// + /// Whether this object should be expanded in the GUI. + void SetExpandedRecursively(bool expanded) + { + m_Expanded = expanded; + foreach (var kvp in m_Subfolders) + { + kvp.Value.SetExpandedRecursively(expanded); + } + } + + /// + /// 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.GameObject.CompareTag(tagFilter)) + continue; + + user.Draw(); + } + } + } + } + + struct GameObjectRow + { + public string TransformPath; + public GameObject GameObject; + + public void Draw() + { + EditorGUILayout.ObjectField($"{TransformPath} - Tag: {GameObject.tag}", GameObject, 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_NoTagUsers = "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 HashSet k_BuiltInTags = new HashSet + { + k_UntaggedString, + "Respawn", + "Finish", + "EditorOnly", + "MainCamera", + "Player", + "GameController", + }; + + static readonly Stopwatch k_StopWatch = new Stopwatch(); + + Vector2 m_FilterListScrollPosition; + 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; + } + + using (new GUILayout.HorizontalScope()) + { + using (new GUILayout.VerticalScope(k_FilterPanelWidthOption)) + { + DrawFilters(); + } + + if (m_ParentFolder.GetCount(m_TagFilter) == 0) + { + GUILayout.Label(k_NoTagUsers); + } + else + { + 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_FilterListScrollPosition)) + { + m_FilterListScrollPosition = 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), + GameObject = 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(); + + // 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) + { + 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/LayerUsers/PrefabTagUsers.cs.meta b/Editor/LayerUsers/PrefabTagUsers.cs.meta new file mode 100644 index 0000000..5821642 --- /dev/null +++ b/Editor/LayerUsers/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/LayerUsers/SceneLayerUsers.cs b/Editor/LayerUsers/SceneLayerUsers.cs new file mode 100644 index 0000000..7aba868 --- /dev/null +++ b/Editor/LayerUsers/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 = GetOrCreateFilterRowForLayer(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 = GetOrCreateFilterRowForLayer(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 usagesInChildren, + ref int usagesInChildrenWithoutLayerMasks) + { + var layer = child.GameObject.layer; + if (layer != 0) + { + usagesInChildren++; + usagesInChildrenWithoutLayerMasks++; + IncrementCountForLayer(layer, m_UsagesInChildrenPerLayer); + IncrementCountForLayer(layer, m_UsagesInChildrenWithoutLayerMasksPerLayer); + } + + usagesInChildren += child.m_TotalUsagesInChildren; + usagesInChildrenWithoutLayerMasks += child.m_TotalUsagesInChildrenWithoutLayerMasks; + AggregateCountPerLayer(child.m_UsagesInChildrenPerLayer, m_UsagesInChildrenPerLayer); + AggregateCountPerLayer(child.m_UsagesInChildrenWithoutLayerMasksPerLayer, m_UsagesInChildrenWithoutLayerMasksPerLayer); + + usagesInChildren += 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 GetOrCreateFilterRowForLayer(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/LayerUsers/SceneLayerUsers.cs.meta b/Editor/LayerUsers/SceneLayerUsers.cs.meta new file mode 100644 index 0000000..dd93ad6 --- /dev/null +++ b/Editor/LayerUsers/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/LayerUsers/SceneTagUsers.cs b/Editor/LayerUsers/SceneTagUsers.cs new file mode 100644 index 0000000..6367a36 --- /dev/null +++ b/Editor/LayerUsers/SceneTagUsers.cs @@ -0,0 +1,600 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using UnityEditor; +using UnityEditor.Experimental.SceneManagement; +using UnityEditorInternal; +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); + static readonly HashSet k_BuiltInTags = new HashSet + { + k_UntaggedString, + "Respawn", + "Finish", + "EditorOnly", + "MainCamera", + "Player", + "GameController", + }; + + 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(); + 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(); + 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/MissingReferences/MissingProjectReferences.cs b/Editor/MissingReferences/MissingProjectReferences.cs index cf4f928..c5efb4f 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; - bool m_SubAssetsVisible; - public readonly List SubAssets = new List(); - public readonly List PropertiesWithMissingReferences = new List(); + readonly List m_SubAssets; + readonly List m_PropertiesWithMissingReferences; + + bool m_SubAssetsExpanded; 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,33 +105,36 @@ public override void Draw() { using (new EditorGUI.IndentLevelScope()) { - DrawPropertiesWithMissingReferences(PropertiesWithMissingReferences); + DrawPropertiesWithMissingReferences(m_PropertiesWithMissingReferences); + + if (m_SubAssets == null) + return; - var count = SubAssets.Count; + var count = m_SubAssets.Count; 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 SubAssets) + foreach (var asset in m_SubAssets) { asset.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; @@ -142,14 +167,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); } } @@ -193,14 +218,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()) @@ -223,20 +248,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 018337e..95114cb 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 @@ -20,7 +21,75 @@ 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 + { + const string k_UntitledSceneName = "Untitled"; + + readonly string m_SceneName; + readonly List m_Roots; + readonly int m_Count; + + bool m_Expanded; + + 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 wasExpanded = m_Expanded; + m_Expanded = EditorGUILayout.Foldout(m_Expanded, $"{m_SceneName} ({m_Count})", true, Styles.RichTextFoldout); + + // 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 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; + } } /// @@ -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_Expanded; 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; } /// @@ -170,13 +273,16 @@ 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); + if (m_GameObject == null) + return; + + var wasExpanded = m_Expanded; + 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()) { @@ -188,24 +294,17 @@ 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()) { - // 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); @@ -231,39 +330,44 @@ 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(); - } - } } /// - /// 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); } } + + public void Clear() + { + + m_Children.Clear(); + m_Components.Clear(); + } } static class Styles @@ -343,20 +447,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) @@ -440,6 +550,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) 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/Unity.Labs.SuperScience.Editor.api b/Editor/Unity.Labs.SuperScience.Editor.api index 800e665..ad6f312 100644 --- a/Editor/Unity.Labs.SuperScience.Editor.api +++ b/Editor/Unity.Labs.SuperScience.Editor.api @@ -34,11 +34,31 @@ namespace Unity.Labs.SuperScience [System.Runtime.CompilerServices.Extension] public static void StopRunInEditMode(UnityEngine.MonoBehaviour behaviour); } + public class PrefabLayerUsers : UnityEditor.EditorWindow + { + public PrefabLayerUsers() {} + } + + public class PrefabTagUsers : UnityEditor.EditorWindow + { + public PrefabTagUsers() {} + } + public class RunInEditHelper : UnityEditor.EditorWindow { public RunInEditHelper() {} } + public class SceneLayerUsers : UnityEditor.EditorWindow + { + public SceneLayerUsers() {} + } + + public class SceneTagUsers : UnityEditor.EditorWindow + { + public SceneTagUsers() {} + } + public class SolidColorTextures : UnityEditor.EditorWindow { public SolidColorTextures() {} diff --git a/Runtime/Unity.Labs.SuperScience.api b/Runtime/Unity.Labs.SuperScience.api index be64dfa..d552016 100644 --- a/Runtime/Unity.Labs.SuperScience.api +++ b/Runtime/Unity.Labs.SuperScience.api @@ -34,6 +34,7 @@ namespace Unity.Labs.SuperScience public class ExampleSceneMetadata : UnityEngine.ScriptableObject { + public int TotalComponents { get; } public ExampleSceneMetadata() {} public void UpdateFromScene(UnityEngine.SceneManagement.Scene scene); }