using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Text; using UnityEditor; using UnityEditor.IMGUI.Controls; using UnityEditor.SceneManagement; using UnityEngine; using UnityEngine.SceneManagement; using Object = UnityEngine.Object; namespace AssetUsageDetectorNamespace { // Custom class to hold search results [Serializable] public class SearchResult : IEnumerable, ISerializationCallbackReceiver { [Serializable] internal class SerializableResultGroup { public string title; public SearchResultGroup.GroupType type; public bool isExpanded; public bool pendingSearch; public SearchResultTreeViewState treeViewState; public List initialSerializedNodes; } [Serializable] internal class SerializableNode { [Serializable] public class SerializableLinkDescriptions { public List value; } public string label; public int instanceId; public bool isUnityObject, isMainReference; public ReferenceNode.UsedState usedState; public List links; public List linkDescriptions; public List linkWeakStates; } internal class SortedEntry : IComparable { public readonly string assetPath, subAssetName; public readonly bool isMainAsset; public readonly Transform transform; public readonly object entry; public SortedEntry( ReferenceNode node ) : this( node.nodeObject as Object ) { entry = node; } public SortedEntry( ReferenceNode.Link link ) : this( link.targetNode.nodeObject as Object ) { entry = link; } private SortedEntry( Object obj ) { if( obj ) { assetPath = AssetDatabase.GetAssetPath( obj ); if( string.IsNullOrEmpty( assetPath ) ) { assetPath = null; if( obj is Component ) transform = ( (Component) obj ).transform; else if( obj is GameObject ) transform = ( (GameObject) obj ).transform; } else { isMainAsset = AssetDatabase.IsMainAsset( ( obj is Component ) ? ( (Component) obj ).gameObject : obj ); if( !isMainAsset ) subAssetName = obj.name; } } } // Sorting order: // 1) Scene objects come first and are sorted by their absolute sibling indices in Hierarchy // 2) Assets come later and are sorted by their asset paths (for assets sharing the same path, main assets come first and sub-assets are then sorted by their names) // 3) Regular C# objects come last int IComparable.CompareTo( SortedEntry other ) { if( this == other ) return 0; else if( assetPath == null ) { if( !transform ) return 1; else if( other.assetPath == null ) return other.transform ? Utilities.CompareHierarchySiblingIndices( transform, other.transform ) : -1; else return -1; } else if( other.assetPath == null ) return other.transform ? 1 : -1; else { int assetPathComparison = EditorUtility.NaturalCompare( assetPath, other.assetPath ); if( assetPathComparison != 0 ) return assetPathComparison; else if( isMainAsset ) return -1; else if( other.isMainAsset ) return 1; else return subAssetName.CompareTo( other.subAssetName ); } } } private bool success; // This isn't readonly so that it can be serialized private List result; private SceneSetup[] initialSceneSetup; private AssetUsageDetector searchHandler; private AssetUsageDetector.Parameters m_searchParameters; // Each TreeView in the drawn search results must use unique ids for their TreeViewItems. Otherwise, strange things happen: https://forum.unity.com/threads/multiple-editor-treeviews-selection-issue.601471/ internal int nextTreeViewId = 1; private List serializedNodes; private List serializedGroups; private Object[] serializedUsedObjects; public int NumberOfGroups { get { return result.Count; } } public SearchResultGroup this[int index] { get { return result[index]; } } public HashSet UsedObjects { get; private set; } public bool SearchCompletedSuccessfully { get { return success; } } public bool InitialSceneSetupConfigured { get { return initialSceneSetup != null && initialSceneSetup.Length > 0; } } public bool HasPendingLazySceneSearchResults { get { return result.Find( ( group ) => group.PendingSearch ) != null; } } public AssetUsageDetector.Parameters SearchParameters { get { return m_searchParameters; } } public SearchResult( bool success, List result, HashSet usedObjects, SceneSetup[] initialSceneSetup, AssetUsageDetector searchHandler, AssetUsageDetector.Parameters searchParameters ) { if( result == null ) result = new List( 0 ); this.success = success; this.result = result; this.UsedObjects = usedObjects ?? new HashSet(); this.initialSceneSetup = initialSceneSetup; this.searchHandler = searchHandler; this.m_searchParameters = searchParameters; } public void RemoveSearchResultGroup( SearchResultGroup searchResultGroup ) { result.Remove( searchResultGroup ); } public void RefreshSearchResultGroup( SearchResultGroup searchResultGroup, bool noAssetDatabaseChanges ) { if( searchResultGroup == null ) { Debug.LogError( "SearchResultGroup is null!" ); return; } int searchResultGroupIndex = result.IndexOf( searchResultGroup ); if( searchResultGroupIndex < 0 ) { Debug.LogError( "SearchResultGroup is not a part of SearchResult!" ); return; } if( searchResultGroup.Type == SearchResultGroup.GroupType.Scene && EditorApplication.isPlaying && !EditorSceneManager.GetSceneByPath( searchResultGroup.ScenePath ).isLoaded ) { Debug.LogError( "Can't search unloaded scene while in Play Mode!" ); return; } if( searchHandler == null ) searchHandler = new AssetUsageDetector(); SceneSearchMode searchInScenes = m_searchParameters.searchInScenes; Object[] searchInScenesSubset = m_searchParameters.searchInScenesSubset; bool searchInAssetsFolder = m_searchParameters.searchInAssetsFolder; Object[] searchInAssetsSubset = m_searchParameters.searchInAssetsSubset; bool searchInProjectSettings = m_searchParameters.searchInProjectSettings; bool lazySceneSearch = m_searchParameters.lazySceneSearch; bool calculateUnusedObjects = m_searchParameters.calculateUnusedObjects; bool _noAssetDatabaseChanges = m_searchParameters.noAssetDatabaseChanges; try { if( searchResultGroup.Type == SearchResultGroup.GroupType.Assets ) { m_searchParameters.searchInScenes = SceneSearchMode.None; m_searchParameters.searchInScenesSubset = null; m_searchParameters.searchInProjectSettings = false; } else if( searchResultGroup.Type == SearchResultGroup.GroupType.ProjectSettings ) { m_searchParameters.searchInScenes = SceneSearchMode.None; m_searchParameters.searchInScenesSubset = null; m_searchParameters.searchInAssetsFolder = false; m_searchParameters.searchInAssetsSubset = null; m_searchParameters.searchInProjectSettings = true; } else if( searchResultGroup.Type == SearchResultGroup.GroupType.Scene ) { m_searchParameters.searchInScenes = SceneSearchMode.None; m_searchParameters.searchInScenesSubset = new Object[1] { AssetDatabase.LoadAssetAtPath( searchResultGroup.ScenePath ) }; m_searchParameters.searchInAssetsFolder = false; m_searchParameters.searchInAssetsSubset = null; m_searchParameters.searchInProjectSettings = false; } else if( searchResultGroup.Type == SearchResultGroup.GroupType.DontDestroyOnLoad ) { m_searchParameters.searchInScenes = (SceneSearchMode) 1024; // A unique value to search only the DontDestroyOnLoad scene m_searchParameters.searchInScenesSubset = null; m_searchParameters.searchInAssetsFolder = false; m_searchParameters.searchInAssetsSubset = null; m_searchParameters.searchInProjectSettings = false; } else { Debug.LogError( "Can't refresh group: " + searchResultGroup.Type ); return; } m_searchParameters.lazySceneSearch = false; m_searchParameters.calculateUnusedObjects = result.Find( ( group ) => group.Type == SearchResultGroup.GroupType.UnusedObjects ) != null; m_searchParameters.noAssetDatabaseChanges = noAssetDatabaseChanges; // Make sure the AssetDatabase is up-to-date AssetDatabase.SaveAssets(); SearchResult searchResult = searchHandler.Run( m_searchParameters ); if( !searchResult.success ) { EditorUtility.DisplayDialog( "Error", "Couldn't refresh, check console for more info.", "OK" ); return; } if( searchResult.result != null ) { SearchResultGroup newSearchResultGroup = searchResult.result.Find( ( group ) => group.Title == searchResultGroup.Title ); if( newSearchResultGroup != null ) result[searchResultGroupIndex] = newSearchResultGroup; else searchResultGroup.Clear(); UsedObjects.UnionWith( searchResult.UsedObjects ); SearchResultGroup unusedObjectsSearchResultGroup = result.Find( ( group ) => group.Type == SearchResultGroup.GroupType.UnusedObjects ); if( unusedObjectsSearchResultGroup != null ) { SearchResultGroup newUnusedObjectsSearchResultGroup = searchResult.result.Find( ( group ) => group.Type == SearchResultGroup.GroupType.UnusedObjects ); if( newUnusedObjectsSearchResultGroup == null ) { // UnusedObjects search result group doesn't exist in 2 cases: // - When there are no search results found (NumberOfGroups == 0) // - When all searched objects are referenced (NumberOfGroups > 0) if( searchResult.result.Count > 0 ) unusedObjectsSearchResultGroup.Clear(); } else { // NOTE: We can process UnusedObjects graphs iteratively (instead of recursively) because for the time being, these graphs have a maximum depth of 1 bool unusedObjectsGraphChanged = false; HashSet newUnusedObjectsSet = new HashSet(); for( int i = newUnusedObjectsSearchResultGroup.NumberOfReferences - 1; i >= 0; i-- ) { ReferenceNode node = newUnusedObjectsSearchResultGroup[i]; newUnusedObjectsSet.Add( node.UnityObject ); for( int j = node.NumberOfOutgoingLinks - 1; j >= 0; j-- ) newUnusedObjectsSet.Add( node[j].targetNode.UnityObject ); } for( int i = unusedObjectsSearchResultGroup.NumberOfReferences - 1; i >= 0; i-- ) { ReferenceNode node = unusedObjectsSearchResultGroup[i]; bool parentNodeRemoved = false; Object obj = node.UnityObject; if( !obj || !newUnusedObjectsSet.Contains( obj ) ) { unusedObjectsSearchResultGroup.RemoveReference( i ); unusedObjectsGraphChanged = parentNodeRemoved = true; } bool hasUnusedSubObjects = false, hasUsedSubObjects = false; bool hadSubObjects = node.NumberOfOutgoingLinks > 0; for( int j = 0; j < node.NumberOfOutgoingLinks; j++ ) { if( node[j].targetNode.usedState == ReferenceNode.UsedState.Used ) // User has explicitly displayed this used child object/sub-asset in the TreeView continue; Object _obj = node[j].targetNode.UnityObject; if( newUnusedObjectsSet.Contains( _obj ) ) { hasUnusedSubObjects = true; if( parentNodeRemoved ) unusedObjectsSearchResultGroup.AddReference( node[j].targetNode ); } else if( !parentNodeRemoved ) { node.RemoveLink( j-- ); unusedObjectsGraphChanged = hasUsedSubObjects = true; } } if( !parentNodeRemoved ) { // When all sub-assets of a main asset are used, consider the main asset as used, as well if( !hasUnusedSubObjects && hadSubObjects && AssetDatabase.IsMainAsset( obj ) ) { unusedObjectsSearchResultGroup.RemoveReference( i ); unusedObjectsGraphChanged = true; } if( hasUsedSubObjects && node.usedState == ReferenceNode.UsedState.Unused ) node.usedState = ReferenceNode.UsedState.MixedCollapsed; } } if( unusedObjectsGraphChanged && unusedObjectsSearchResultGroup.treeView != null ) { unusedObjectsSearchResultGroup.treeView.SetSelection( new int[0], TreeViewSelectionOptions.FireSelectionChanged ); unusedObjectsSearchResultGroup.treeViewState.preSearchExpandedIds = null; unusedObjectsSearchResultGroup.treeView.Reload(); unusedObjectsSearchResultGroup.treeView.ExpandAll(); } } if( unusedObjectsSearchResultGroup.NumberOfReferences == 0 ) result.Remove( unusedObjectsSearchResultGroup ); } } } finally { m_searchParameters.searchInScenes = searchInScenes; m_searchParameters.searchInScenesSubset = searchInScenesSubset; m_searchParameters.searchInAssetsFolder = searchInAssetsFolder; m_searchParameters.searchInAssetsSubset = searchInAssetsSubset; m_searchParameters.searchInProjectSettings = searchInProjectSettings; m_searchParameters.lazySceneSearch = lazySceneSearch; m_searchParameters.calculateUnusedObjects = calculateUnusedObjects; m_searchParameters.noAssetDatabaseChanges = _noAssetDatabaseChanges; } } public float DrawOnGUI( EditorWindow window, float scrollPosition, bool noAssetDatabaseChanges ) { for( int i = 0; i < result.Count; i++ ) { scrollPosition = result[i].DrawOnGUI( this, window, scrollPosition, noAssetDatabaseChanges ); if( i < result.Count - 1 ) GUILayout.Space( 10f ); } return scrollPosition; } public int IndexOf( SearchResultGroup searchResultGroup ) { return result.IndexOf( searchResultGroup ); } public void CollapseAllSearchResultGroups() { for( int i = 0; i < result.Count; i++ ) result[i].Collapse(); } public void CancelDelayedTreeViewTooltip() { for( int i = 0; i < result.Count; i++ ) { if( result[i].treeView != null ) result[i].treeView.CancelDelayedTooltip(); } } // Returns if RestoreInitialSceneSetup will have any effect on the current scene setup public bool IsSceneSetupDifferentThanCurrentSetup() { if( initialSceneSetup == null ) return false; SceneSetup[] sceneFinalSetup = EditorSceneManager.GetSceneManagerSetup(); if( initialSceneSetup.Length != sceneFinalSetup.Length ) return true; for( int i = 0; i < sceneFinalSetup.Length; i++ ) { bool sceneIsOneOfInitials = false; for( int j = 0; j < initialSceneSetup.Length; j++ ) { if( sceneFinalSetup[i].path == initialSceneSetup[j].path ) { if( sceneFinalSetup[i].isLoaded != initialSceneSetup[j].isLoaded ) return true; sceneIsOneOfInitials = true; break; } } if( !sceneIsOneOfInitials ) return true; } return false; } // Close the scenes that were not part of the initial scene setup // Returns true if initial scene setup is restored successfully public bool RestoreInitialSceneSetup() { if( initialSceneSetup == null || initialSceneSetup.Length == 0 ) return true; if( EditorApplication.isPlaying ) return false; if( !IsSceneSetupDifferentThanCurrentSetup() ) return true; StringBuilder sb = Utilities.stringBuilder; sb.Length = 0; sb.AppendLine( "Restore initial scene setup?" ); for( int i = 0; i < initialSceneSetup.Length; i++ ) sb.AppendLine().Append( "- " ).Append( initialSceneSetup[i].path ); switch( EditorUtility.DisplayDialogComplex( "Asset Usage Detector", sb.ToString(), "Yes", "Cancel", "Leave it as is" ) ) { case 1: return false; case 2: return true; } if( !EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo() ) return false; for( int i = 0; i < initialSceneSetup.Length; i++ ) { Scene scene = EditorSceneManager.GetSceneByPath( initialSceneSetup[i].path ); if( !scene.isLoaded ) scene = EditorSceneManager.OpenScene( initialSceneSetup[i].path, initialSceneSetup[i].isLoaded ? OpenSceneMode.Additive : OpenSceneMode.AdditiveWithoutLoading ); if( initialSceneSetup[i].isActive ) EditorSceneManager.SetActiveScene( scene ); } SceneSetup[] sceneFinalSetup = EditorSceneManager.GetSceneManagerSetup(); for( int i = 0; i < sceneFinalSetup.Length; i++ ) { bool sceneIsOneOfInitials = false; for( int j = 0; j < initialSceneSetup.Length; j++ ) { if( sceneFinalSetup[i].path == initialSceneSetup[j].path ) { sceneIsOneOfInitials = true; break; } } if( !sceneIsOneOfInitials ) EditorSceneManager.CloseScene( EditorSceneManager.GetSceneByPath( sceneFinalSetup[i].path ), true ); } for( int i = 0; i < initialSceneSetup.Length; i++ ) { if( !initialSceneSetup[i].isLoaded ) EditorSceneManager.CloseScene( EditorSceneManager.GetSceneByPath( initialSceneSetup[i].path ), false ); } initialSceneSetup = null; return true; } // Assembly reloading; serialize nodes in a way that Unity can serialize // Credit: https://docs.unity3d.com/Manual/script-Serialization-Custom.html void ISerializationCallbackReceiver.OnBeforeSerialize() { if( result == null ) return; if( serializedGroups == null ) serializedGroups = new List( result.Count ); else serializedGroups.Clear(); if( serializedNodes == null ) serializedNodes = new List( result.Count * 16 ); else serializedNodes.Clear(); Dictionary nodeToIndex = new Dictionary( result.Count * 16 ); for( int i = 0; i < result.Count; i++ ) serializedGroups.Add( result[i].Serialize( nodeToIndex, serializedNodes ) ); if( serializedUsedObjects == null || serializedUsedObjects.Length != UsedObjects.Count ) serializedUsedObjects = new Object[UsedObjects.Count]; UsedObjects.CopyTo( serializedUsedObjects ); } // Assembly reloaded; deserialize nodes to construct the original graph void ISerializationCallbackReceiver.OnAfterDeserialize() { if( serializedGroups == null || serializedNodes == null || serializedUsedObjects == null ) return; if( result == null ) result = new List( serializedGroups.Count ); else result.Clear(); List allNodes = new List( serializedNodes.Count ); for( int i = 0; i < serializedNodes.Count; i++ ) allNodes.Add( new ReferenceNode() ); for( int i = 0; i < serializedNodes.Count; i++ ) allNodes[i].Deserialize( serializedNodes[i], allNodes ); for( int i = 0; i < serializedGroups.Count; i++ ) { result.Add( new SearchResultGroup( serializedGroups[i].title, serializedGroups[i].type, serializedGroups[i].isExpanded, serializedGroups[i].pendingSearch ) ); result[i].Deserialize( serializedGroups[i], allNodes ); } if( UsedObjects == null ) UsedObjects = new HashSet( serializedUsedObjects ); else UsedObjects.UnionWith( serializedUsedObjects ); serializedNodes.Clear(); serializedGroups.Clear(); serializedUsedObjects = null; } IEnumerator IEnumerable.GetEnumerator() { return ( (IEnumerable) result ).GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return ( (IEnumerable) result ).GetEnumerator(); } } // Custom class to hold the results for a single scene or Assets folder public class SearchResultGroup : IEnumerable { public enum GroupType { Assets = 0, Scene = 1, DontDestroyOnLoad = 2, ProjectSettings = 3, UnusedObjects = 4 }; private readonly List references = new List(); internal SearchResultTreeView treeView; internal SearchResultTreeViewState treeViewState; private Rect lastTreeViewRect; private SearchField treeViewSearchField; public string Title { get; private set; } public GroupType Type { get; private set; } public string ScenePath { get; private set; } public bool IsExpanded { get; private set; } public bool PendingSearch { get; private set; } public int NumberOfReferences { get { return references.Count; } } public ReferenceNode this[int index] { get { return references[index]; } } public SearchResultGroup( string title, GroupType type, bool isExpanded = true, bool pendingSearch = false ) { Title = title.StartsWith( "" ) ? title : string.Concat( "", title, "" ); ScenePath = type != GroupType.Scene ? null : ( title.StartsWith( "" ) ? title.Substring( 3, title.Length - 7 ) : title ); Type = type; IsExpanded = isExpanded; PendingSearch = pendingSearch; } public void AddReference( ReferenceNode node ) { references.Add( node ); } public void RemoveReference( int index ) { references.RemoveAt( index ); } // Removes all nodes public void Clear() { PendingSearch = false; references.Clear(); treeView = null; treeViewState = null; treeViewSearchField = null; } public void Collapse() { IsExpanded = false; } // Initializes commonly used variables of the nodes public void InitializeNodes( HashSet objectsToSearchSet ) { List _references = new List( references ); references.Clear(); // Reverse the links of the search results graph so that the root ReferenceNodes are the searched objects Dictionary reverseGraphNodes = new Dictionary( references.Count * 16 ); for( int i = 0; i < _references.Count; i++ ) _references[i].CreateReverseGraphRecursively( this, references, reverseGraphNodes, objectsToSearchSet ); // Remove weak links if they aren't ultimately connected to a non-weak link HashSet visitedNodes = new HashSet(); for( int i = references.Count - 1; i >= 0; i-- ) references[i].RemoveRedundantLinksRecursively( visitedNodes ); // When a GameObject is a root node, then any components of that GameObject that are also root nodes should omit their links to the // GameObject's node because otherwise, search results are filled with redundant 'GameObject->Its Component' references HashSet rootGameObjectNodes = new HashSet(); for( int i = references.Count - 1; i >= 0; i-- ) { if( references[i].nodeObject as GameObject ) rootGameObjectNodes.Add( references[i] ); } for( int i = references.Count - 1; i >= 0; i-- ) { ReferenceNode node = references[i]; Component component = node.nodeObject as Component; if( component ) { for( int j = node.NumberOfOutgoingLinks - 1; j >= 0; j-- ) { if( ReferenceEquals( node[j].targetNode.nodeObject, component.gameObject ) && rootGameObjectNodes.Contains( node[j].targetNode ) ) { node.RemoveLink( j ); break; } } } } // Remove root nodes that don't have any outgoing links for( int i = references.Count - 1; i >= 0; i-- ) { if( references[i].NumberOfOutgoingLinks == 0 ) references.RemoveAt( i ); } // Sort root nodes if( references.Count > 1 ) { SearchResult.SortedEntry[] sortedEntries = new SearchResult.SortedEntry[references.Count]; for( int i = references.Count - 1; i >= 0; i-- ) sortedEntries[i] = new SearchResult.SortedEntry( references[i] ); Array.Sort( sortedEntries ); for( int i = 0; i < sortedEntries.Length; i++ ) references[i] = (ReferenceNode) sortedEntries[i].entry; } for( int i = references.Count - 1; i >= 0; i-- ) { references[i].SortLinks(); // Sort immediate links of the root nodes references[i].InitializeRecursively(); } } // Draw the results found for this container public float DrawOnGUI( SearchResult searchResult, EditorWindow window, float scrollPosition, bool noAssetDatabaseChanges ) { Event ev = Event.current; Color c = GUI.backgroundColor; float headerHeight = EditorGUIUtility.singleLineHeight * 2f; float refreshButtonWidth = 100f; GUI.backgroundColor = AssetUsageDetectorSettings.SearchResultGroupHeaderColor; Rect headerRect = EditorGUILayout.GetControlRect( false, headerHeight ); float width = headerRect.width; headerRect.width = headerHeight; if( GUI.Button( headerRect, IsExpanded ? "v" : ">" ) ) { IsExpanded = !IsExpanded; if( ev.alt && treeView != null ) { if( !IsExpanded ) treeView.CollapseAll(); else treeView.ExpandAll(); } window.Repaint(); GUIUtility.ExitGUI(); } headerRect.x += headerHeight; headerRect.width = width - ( searchResult != null ? ( refreshButtonWidth + headerHeight ) : headerHeight ); if( GUI.Button( headerRect, Title, Utilities.BoxGUIStyle ) ) { if( ev.button != 1 ) { if( Type == GroupType.Scene ) { // If the container (scene, usually) is left clicked, highlight it on Project view SceneAsset sceneAsset = AssetDatabase.LoadAssetAtPath( ScenePath ); if( sceneAsset ) { if( AssetUsageDetectorSettings.PingClickedObjects ) EditorGUIUtility.PingObject( sceneAsset ); if( AssetUsageDetectorSettings.SelectClickedObjects ) Selection.activeObject = sceneAsset; } } } else { GenericMenu contextMenu = new GenericMenu(); if( searchResult != null ) contextMenu.AddItem( new GUIContent( "Hide" ), false, () => searchResult.RemoveSearchResultGroup( this ) ); if( references.Count > 0 && treeView != null ) { if( contextMenu.GetItemCount() > 0 ) contextMenu.AddSeparator( "" ); if( Type != GroupType.UnusedObjects ) { contextMenu.AddItem( new GUIContent( "Expand Direct References Only" ), false, () => { treeView.ExpandDirectReferences(); IsExpanded = true; } ); contextMenu.AddItem( new GUIContent( "Expand Main References Only" ), false, () => { treeView.ExpandMainReferences(); IsExpanded = true; } ); } if( !string.IsNullOrEmpty( treeViewState.searchTerm ) && treeViewState.searchMode == SearchResultTreeView.SearchMode.ReferencesOnly ) { contextMenu.AddItem( new GUIContent( "Expand Matching Search Results Only" ), false, () => { treeView.ExpandMatchingSearchResults(); IsExpanded = true; } ); } contextMenu.AddItem( new GUIContent( "Expand All" ), false, () => { treeView.ExpandAll(); IsExpanded = true; } ); contextMenu.AddItem( new GUIContent( "Collapse All" ), false, () => { treeView.CollapseAll(); IsExpanded = true; } ); if( searchResult != null && searchResult.NumberOfGroups > 1 && !string.IsNullOrEmpty( treeViewState.searchTerm ) ) { if( contextMenu.GetItemCount() > 0 ) contextMenu.AddSeparator( "" ); contextMenu.AddItem( new GUIContent( "Apply Search to All Results" ), false, () => { for( int i = 0; i < searchResult.NumberOfGroups; i++ ) { if( searchResult[i].treeView == null ) continue; string previousSearchTerm = searchResult[i].treeViewState.searchTerm ?? ""; SearchResultTreeView.SearchMode previousSearchMode = searchResult[i].treeViewState.searchMode; searchResult[i].treeViewState.searchTerm = treeViewState.searchTerm ?? ""; searchResult[i].treeViewState.searchMode = treeViewState.searchMode; if( treeViewState.searchTerm != previousSearchTerm || treeViewState.searchMode != previousSearchMode ) searchResult[i].treeView.RefreshSearch( previousSearchTerm ); } } ); } } #if UNITY_2022_2_OR_NEWER int loadedSceneCount = SceneManager.loadedSceneCount; #else int loadedSceneCount = EditorSceneManager.loadedSceneCount; #endif if( Type == GroupType.Scene && !EditorApplication.isPlaying && loadedSceneCount > 1 ) { // Show context menu when SearchResultGroup's header is right clicked Scene scene = EditorSceneManager.GetSceneByPath( ScenePath ); if( scene.isLoaded ) { if( contextMenu.GetItemCount() > 0 ) contextMenu.AddSeparator( "" ); contextMenu.AddItem( new GUIContent( "Close Scene" ), false, () => { if( !scene.isDirty || EditorSceneManager.SaveModifiedScenesIfUserWantsTo( new Scene[1] { scene } ) ) EditorSceneManager.CloseScene( scene, true ); } ); } } contextMenu.ShowAsContext(); } } if( searchResult != null ) { bool guiEnabled = GUI.enabled; GUI.enabled = Type != GroupType.UnusedObjects; headerRect.x += width - ( refreshButtonWidth + headerHeight ); headerRect.width = refreshButtonWidth; if( GUI.Button( headerRect, "Refresh" ) ) { searchResult.RefreshSearchResultGroup( this, noAssetDatabaseChanges ); GUIUtility.ExitGUI(); } GUI.enabled = guiEnabled; } GUI.backgroundColor = c; if( IsExpanded ) { if( PendingSearch ) GUILayout.Box( "Lazy Search: this scene potentially has some references, hit Refresh to find them", Utilities.BoxGUIStyle ); else if( references.Count == 0 ) GUILayout.Box( ( Type == GroupType.UnusedObjects ) ? "No unused objects left..." : "No references found...", Utilities.BoxGUIStyle ); else { if( Type == GroupType.UnusedObjects ) { if( searchResult != null && searchResult.HasPendingLazySceneSearchResults ) EditorGUILayout.HelpBox( "Some scene(s) aren't searched yet (lazy scene search). Refreshing those scene(s) will automatically update this list.", MessageType.Warning ); if( searchResult != null && searchResult.SearchParameters.dontSearchInSourceAssets && searchResult.SearchParameters.objectsToSearch.Length > 1 ) EditorGUILayout.HelpBox( "'Don't search \"SEARCHED OBJECTS\" themselves for references' is enabled, some of these objects might be used by \"SEARCHED OBJECTS\".", MessageType.Warning ); if( !AssetUsageDetectorSettings.MarkUsedAssetsSubAssetsAsUsed ) EditorGUILayout.HelpBox( "'Hide unused sub-assets in \"Unused Objects\" list if their parent assets are used' is disabled, unused sub-assets' parent assets might be used.", MessageType.Warning ); EditorGUILayout.HelpBox( "Although no references to these objects are found, they might still be used somewhere (e.g. via Resources.Load). If you intend to delete these objects, consider creating a backup of your project first.", MessageType.Info ); } if( treeView == null ) { bool isFirstInitialization = ( treeViewState == null ); if( isFirstInitialization ) treeViewState = new SearchResultTreeViewState(); // This isn't inside isFirstInitialization because SearchResultTreeViewState might have been initialized by // Unity's serialization system after a domain reload bool shouldUpdateInitialTreeViewNodeId = ( treeViewState.initialNodeId == 0 && searchResult != null ); if( shouldUpdateInitialTreeViewNodeId ) treeViewState.initialNodeId = searchResult.nextTreeViewId; treeView = new SearchResultTreeView( treeViewState, references, ( Type == GroupType.UnusedObjects ) ? SearchResultTreeView.TreeType.UnusedObjects : SearchResultTreeView.TreeType.Normal, searchResult != null ? searchResult.UsedObjects : null, searchResult != null && searchResult.SearchParameters.hideDuplicateRows, searchResult != null && searchResult.SearchParameters.hideReduntantPrefabVariantLinks, true ); if( isFirstInitialization ) { if( Type != GroupType.UnusedObjects ) treeView.ExpandMainReferences(); else treeView.ExpandAll(); } if( shouldUpdateInitialTreeViewNodeId ) searchResult.nextTreeViewId = treeViewState.finalNodeId; } if( treeViewSearchField == null ) { treeViewSearchField = new SearchField() { autoSetFocusOnFindCommand = false }; treeViewSearchField.downOrUpArrowKeyPressed += () => treeView.SetFocusAndEnsureSelectedItem(); // Not assigning SetFocusAndEnsureSelectedItem directly in case treeView's value changes } Rect searchFieldRect = EditorGUILayout.GetControlRect( false, EditorGUIUtility.singleLineHeight ); string previousSearchTerm = treeViewState.searchTerm ?? ""; SearchResultTreeView.SearchMode previousSearchMode = treeViewState.searchMode; treeViewState.searchTerm = treeViewSearchField.OnToolbarGUI( new Rect( searchFieldRect.x, searchFieldRect.y, searchFieldRect.width - 100f, searchFieldRect.height ), treeViewState.searchTerm ) ?? ""; treeViewState.searchMode = (SearchResultTreeView.SearchMode) EditorGUI.EnumPopup( new Rect( searchFieldRect.xMax - 100f, searchFieldRect.y, 100f, searchFieldRect.height ), treeViewState.searchMode ); if( treeViewState.searchTerm != previousSearchTerm || treeViewState.searchMode != previousSearchMode ) treeView.RefreshSearch( previousSearchTerm ); KeyCode pressedKeyboardNavigationKey = KeyCode.None; bool treeViewKeyboardNavigation = false; if( ev.type == EventType.KeyDown ) { pressedKeyboardNavigationKey = ev.keyCode; switch( pressedKeyboardNavigationKey ) { case KeyCode.UpArrow: case KeyCode.DownArrow: case KeyCode.LeftArrow: case KeyCode.RightArrow: case KeyCode.PageUp: case KeyCode.PageDown: case KeyCode.Home: case KeyCode.End: case KeyCode.F: treeViewKeyboardNavigation = true; break; } SearchResultTooltip.Hide(); } else if( ( ev.type == EventType.ValidateCommand || ev.type == EventType.ExecuteCommand ) && ev.commandName == "Find" && treeView.HasFocus() ) { if( ev.type == EventType.ExecuteCommand ) { treeViewSearchField.SetFocus(); // Framed rect padding: Top = 2, Bottom = 2 + the first element in the TreeView scrollPosition = FrameRectInScrollView( scrollPosition, new Vector2( searchFieldRect.y - 2f, searchFieldRect.yMax + EditorGUIUtility.singleLineHeight + 2f ), window.position.height ); window.Repaint(); SearchResultTooltip.Hide(); } ev.Use(); } else if( ev.type == EventType.ScrollWheel ) SearchResultTooltip.Hide(); bool isFirstRowSelected = false, isLastRowSelected = false, isSelectedRowExpanded = false, canExpandSelectedRow = false; if( treeViewKeyboardNavigation && treeView.HasFocus() && treeView.HasSelection() ) treeView.GetRowStateWithId( treeViewState.lastClickedID, out isFirstRowSelected, out isLastRowSelected, out isSelectedRowExpanded, out canExpandSelectedRow ); Rect treeViewRect = EditorGUILayout.GetControlRect( false, treeView.totalHeight ); if( ev.type == EventType.Repaint ) { lastTreeViewRect = treeViewRect; #if !UNITY_2018_2_OR_NEWER // TreeView calls RowGUI for all rows instead of only the visible rows on early Unity versions which leads to performance issues. Do manual row culling on those versions // Credit: https://github.com/Unity-Technologies/UnityCsReference/blob/a048de916b23331bf6dfe92c4a6c205989b83b4f/Editor/Mono/GUI/TreeView/TreeViewGUI.cs#L273-L276 float topPixel = scrollPosition - treeViewRect.y; float heightInPixels = window.position.height; treeView.visibleRowTop = (int) Mathf.Floor( topPixel / treeView.rowHeight ); treeView.visibleRowBottom = treeView.visibleRowTop + (int) Mathf.Ceil( heightInPixels / treeView.rowHeight ); #endif } treeView.OnGUI( treeViewRect ); if( treeViewKeyboardNavigation && treeView.HasFocus() && treeView.HasSelection() ) { Rect targetTreeViewRowRect; if( treeView.GetRowRectWithId( treeViewState.lastClickedID, out targetTreeViewRowRect ) ) { // Allow keyboard navigation between different SearchResultGroups' TreeViews Rect targetTreeViewRect = lastTreeViewRect; if( !ev.control && !ev.command && !ev.shift ) { if( isFirstRowSelected && ( pressedKeyboardNavigationKey == KeyCode.UpArrow || pressedKeyboardNavigationKey == KeyCode.PageUp || pressedKeyboardNavigationKey == KeyCode.Home || ( pressedKeyboardNavigationKey == KeyCode.LeftArrow && !isSelectedRowExpanded ) ) ) { int searchResultGroupIndex = searchResult.IndexOf( this ); for( int i = searchResultGroupIndex - 1; i >= 0; i-- ) { if( !searchResult[i].PendingSearch && searchResult[i].IsExpanded && searchResult[i].references.Count > 0 ) { searchResult[i].treeView.SetFocus(); targetTreeViewRect = searchResult[i].lastTreeViewRect; targetTreeViewRowRect = searchResult[i].treeView.SelectLastRowAndReturnRect(); break; } } } else if( isLastRowSelected && ( pressedKeyboardNavigationKey == KeyCode.DownArrow || pressedKeyboardNavigationKey == KeyCode.PageDown || pressedKeyboardNavigationKey == KeyCode.End || ( pressedKeyboardNavigationKey == KeyCode.RightArrow && !canExpandSelectedRow ) ) ) { int searchResultGroupIndex = searchResult.IndexOf( this ); for( int i = searchResultGroupIndex + 1; i < searchResult.NumberOfGroups; i++ ) { if( !searchResult[i].PendingSearch && searchResult[i].IsExpanded && searchResult[i].references.Count > 0 ) { searchResult[i].treeView.SetFocus(); targetTreeViewRect = searchResult[i].lastTreeViewRect; targetTreeViewRowRect = searchResult[i].treeView.SelectFirstRowAndReturnRect(); break; } } } } // When key event isn't automatically used by the focused TreeView (happens when its search results are empty), if we navigate to // a new TreeView, key event will be consumed by that TreeView and hence, keyboard navigation will occur twice if( ev.type != EventType.Used ) ev.Use(); float scrollTop = targetTreeViewRect.y + targetTreeViewRowRect.y; float scrollBottom = targetTreeViewRect.y + targetTreeViewRowRect.yMax; scrollPosition = FrameRectInScrollView( scrollPosition, new Vector2( scrollTop, scrollBottom ), window.position.height ); window.Repaint(); } } } } return scrollPosition; } // Frame selection (it isn't handled automatically when using an external scroll view) // Credit: https://github.com/Unity-Technologies/UnityCsReference/blob/d0fe81a19ce788fd1d94f826cf797aafc37db8ea/Editor/Mono/GUI/TreeView/TreeViewController.cs#L1329-L1351 private float FrameRectInScrollView( float scrollPosition, Vector2 rectBounds, float windowHeight ) { return Mathf.Clamp( scrollPosition, rectBounds.y - windowHeight, rectBounds.x ); } // Serialize this result group internal SearchResult.SerializableResultGroup Serialize( Dictionary nodeToIndex, List serializedNodes ) { SearchResult.SerializableResultGroup serializedResultGroup = new SearchResult.SerializableResultGroup() { title = Title, type = Type, isExpanded = IsExpanded, pendingSearch = PendingSearch, treeViewState = treeViewState }; if( references != null ) { serializedResultGroup.initialSerializedNodes = new List( references.Count ); for( int i = 0; i < references.Count; i++ ) serializedResultGroup.initialSerializedNodes.Add( references[i].SerializeRecursively( nodeToIndex, serializedNodes ) ); } return serializedResultGroup; } // Deserialize this result group from the serialized data internal void Deserialize( SearchResult.SerializableResultGroup serializedResultGroup, List allNodes ) { treeViewState = serializedResultGroup.treeViewState; if( serializedResultGroup.initialSerializedNodes != null ) { for( int i = 0; i < serializedResultGroup.initialSerializedNodes.Count; i++ ) references.Add( allNodes[serializedResultGroup.initialSerializedNodes[i]] ); } } IEnumerator IEnumerable.GetEnumerator() { return ( (IEnumerable) references ).GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return ( (IEnumerable) references ).GetEnumerator(); } } // Custom class to hold an object in the path to a reference as a node public class ReferenceNode { internal enum UsedState { Unused, MixedCollapsed, MixedExpanded, Used }; public class Link { public readonly ReferenceNode targetNode; public readonly List descriptions; public bool isWeakLink; // Weak links can be omitted from search results if this ReferenceNode isn't referenced by any other node public Link( ReferenceNode targetNode, string description, bool isWeakLink ) { this.targetNode = targetNode; this.descriptions = string.IsNullOrEmpty( description ) ? new List() : new List( 1 ) { description }; this.isWeakLink = isWeakLink; } public Link( ReferenceNode targetNode, List descriptions, bool isWeakLink ) { this.targetNode = targetNode; this.descriptions = descriptions; this.isWeakLink = isWeakLink; } } // Unique identifier is used while serializing the node private static int uid_last = 0; private readonly int uid; public string Label { get; private set; } public bool IsMainReference { get; private set; } // True: if belongs to a scene search result group, then it's an object in that scene. If belongs to the assets search result group, then it's an asset internal object nodeObject; private int? instanceId; // instanceId of the nodeObject if it is a Unity object, null otherwise public Object UnityObject { get { return instanceId.HasValue ? EditorUtility.InstanceIDToObject( instanceId.Value ) : null; } } private readonly List links = new List( 2 ); public int NumberOfOutgoingLinks { get { return links.Count; } } public Link this[int index] { get { return links[index]; } } internal UsedState usedState; public ReferenceNode() { uid = uid_last++; usedState = UsedState.Used; } // Add a one-way connection to another node public void AddLinkTo( ReferenceNode nextNode, string description = null, bool isWeakLink = false ) { if( nextNode != null && nextNode != this ) { if( !string.IsNullOrEmpty( description ) ) description = "[" + description + "]"; // Avoid duplicate links for( int i = 0; i < links.Count; i++ ) { if( links[i].targetNode == nextNode ) { if( !string.IsNullOrEmpty( description ) && !links[i].descriptions.Contains( description ) ) links[i].descriptions.Add( description ); links[i].isWeakLink &= isWeakLink; return; } } links.Add( new Link( nextNode, description, isWeakLink ) ); } } public void RemoveLink( int index ) { links.RemoveAt( index ); } public bool RemoveLink( ReferenceNode nextNode ) { for( int i = links.Count - 1; i >= 0; i-- ) { if( links[i].targetNode == nextNode ) { links.RemoveAt( i ); return true; } } return false; } public void SortLinks() { if( links.Count > 1 ) { SearchResult.SortedEntry[] sortedEntries = new SearchResult.SortedEntry[links.Count]; for( int i = links.Count - 1; i >= 0; i-- ) sortedEntries[i] = new SearchResult.SortedEntry( links[i] ); Array.Sort( sortedEntries ); for( int i = 0; i < sortedEntries.Length; i++ ) links[i] = (Link) sortedEntries[i].entry; } } public void CopyReferencesTo( ReferenceNode other ) { other.links.Clear(); other.links.AddRange( links ); } // Clear this node so that it can be reused later public void Clear() { nodeObject = null; links.Clear(); } public void InitializeRecursively() { if( Label != null ) // Already initialized return; Object unityObject = nodeObject as Object; if( unityObject != null ) { instanceId = unityObject.GetInstanceID(); Label = unityObject.name + " (" + unityObject.GetType().Name + ")"; if( AssetUsageDetectorSettings.ShowRootAssetName && unityObject.IsAsset() && !AssetDatabase.IsMainAsset( unityObject ) ) { string mainAssetName = Path.GetFileNameWithoutExtension( AssetDatabase.GetAssetPath( unityObject ) ); if( unityObject.name != mainAssetName ) Label += " "; } } else if( nodeObject != null ) { instanceId = null; Label = nodeObject.GetType() + " object"; } else { instanceId = null; Label = "<>"; } nodeObject = null; // Don't hold Object reference, allow Unity to GC used memory for( int i = 0; i < links.Count; i++ ) links[i].targetNode.InitializeRecursively(); } public ReferenceNode CreateReverseGraphRecursively( SearchResultGroup searchResultGroup, List reverseGraphRoots, Dictionary reverseGraphNodes, HashSet objectsToSearchSet ) { ReferenceNode result; if( !reverseGraphNodes.TryGetValue( this, out result ) ) { reverseGraphNodes[this] = result = new ReferenceNode() { nodeObject = nodeObject }; Object obj = nodeObject as Object; if( obj && objectsToSearchSet.Contains( obj ) ) reverseGraphRoots.Add( result ); //else // When 'else' is uncommented, 'Don't search "Find referenced of" themselves for references" option simply does nothing. I am not entirely sure if commenting it out will have any side effects, so fingers crossed? { for( int i = 0; i < links.Count; i++ ) { ReferenceNode linkedNode = links[i].targetNode.CreateReverseGraphRecursively( searchResultGroup, reverseGraphRoots, reverseGraphNodes, objectsToSearchSet ); linkedNode.links.Add( new Link( result, links[i].descriptions, links[i].isWeakLink ) ); } } if( obj ) { if( obj is Component ) obj = ( (Component) obj ).gameObject; switch( searchResultGroup.Type ) { case SearchResultGroup.GroupType.Assets: result.IsMainReference = obj.IsAsset() && ( obj is GameObject || ( obj.hideFlags & ( HideFlags.HideInInspector | HideFlags.HideInHierarchy ) ) == HideFlags.None ); break; case SearchResultGroup.GroupType.ProjectSettings: result.IsMainReference = obj.IsAsset() && AssetDatabase.GetAssetPath( obj ).StartsWith( "ProjectSettings/" ); break; case SearchResultGroup.GroupType.Scene: case SearchResultGroup.GroupType.DontDestroyOnLoad: { if( obj is GameObject ) { Scene scene = ( (GameObject) obj ).scene; if( scene.IsValid() ) result.IsMainReference = ( searchResultGroup.Type == SearchResultGroup.GroupType.Scene ) ? scene.path == searchResultGroup.ScenePath : scene.name == "DontDestroyOnLoad"; } break; } } } } return result; } public void RemoveRedundantLinksRecursively( HashSet visitedNodes ) { if( !visitedNodes.Add( this ) ) return; List stack = null; for( int i = links.Count - 1; i >= 0; i-- ) { if( !links[i].isWeakLink ) continue; if( links[i].targetNode.links.Count == 0 ) links.RemoveAt( i ); else { if( stack == null ) stack = new List( 2 ); else stack.Clear(); if( !links[i].targetNode.CheckForNonWeakLinksRecursively( stack ) ) links.RemoveAt( i ); } } for( int i = links.Count - 1; i >= 0; i-- ) links[i].targetNode.RemoveRedundantLinksRecursively( visitedNodes ); } private bool CheckForNonWeakLinksRecursively( List stack ) { if( stack.Contains( this ) || links.Count == 0 ) return false; for( int i = links.Count - 1; i >= 0; i-- ) { if( !links[i].isWeakLink ) return true; } stack.Add( this ); for( int i = links.Count - 1; i >= 0; i-- ) { if( links[i].targetNode.CheckForNonWeakLinksRecursively( stack ) ) return true; } stack.RemoveAt( stack.Count - 1 ); return false; } // Serialize this node and its connected nodes recursively internal int SerializeRecursively( Dictionary nodeToIndex, List serializedNodes ) { int index; if( nodeToIndex.TryGetValue( this, out index ) ) return index; SearchResult.SerializableNode serializedNode = new SearchResult.SerializableNode() { label = Label, isMainReference = IsMainReference, instanceId = instanceId ?? 0, isUnityObject = instanceId.HasValue, usedState = usedState }; index = serializedNodes.Count; nodeToIndex[this] = index; serializedNodes.Add( serializedNode ); if( links.Count > 0 ) { serializedNode.links = new List( links.Count ); serializedNode.linkDescriptions = new List( links.Count ); serializedNode.linkWeakStates = new List( links.Count ); for( int i = 0; i < links.Count; i++ ) { serializedNode.links.Add( links[i].targetNode.SerializeRecursively( nodeToIndex, serializedNodes ) ); serializedNode.linkDescriptions.Add( new SearchResult.SerializableNode.SerializableLinkDescriptions() { value = links[i].descriptions } ); serializedNode.linkWeakStates.Add( links[i].isWeakLink ); } } return index; } // Deserialize this node and its links from the serialized data internal void Deserialize( SearchResult.SerializableNode serializedNode, List allNodes ) { if( serializedNode.isUnityObject ) instanceId = serializedNode.instanceId; else instanceId = null; Label = serializedNode.label; IsMainReference = serializedNode.isMainReference; usedState = serializedNode.usedState; if( serializedNode.links != null ) { for( int i = 0; i < serializedNode.links.Count; i++ ) links.Add( new Link( allNodes[serializedNode.links[i]], serializedNode.linkDescriptions[i].value, serializedNode.linkWeakStates[i] ) ); } } public override int GetHashCode() { return uid; } } }