using System.Collections.Generic; using System.Globalization; using System.Reflection; using System.Text; using UnityEditor; using UnityEditor.IMGUI.Controls; using UnityEngine; #if UNITY_2021_2_OR_NEWER using PrefabStage = UnityEditor.SceneManagement.PrefabStage; using PrefabStageUtility = UnityEditor.SceneManagement.PrefabStageUtility; #elif UNITY_2018_3_OR_NEWER using PrefabStage = UnityEditor.Experimental.SceneManagement.PrefabStage; using PrefabStageUtility = UnityEditor.Experimental.SceneManagement.PrefabStageUtility; #endif namespace AssetUsageDetectorNamespace { [System.Serializable] public class SearchResultTreeViewState : TreeViewState { // - initialNodeId is serialized because we want to preserve the expanded states of the TreeViewItems after domain reload and // it's only possible if TreeView is reconstructed with the same ids // - finalNodeId is serialized because if the same id used for multiple TreeViewItems across multiple TreeViews, strange issues occur. // Thus, each new TreeView will set its initialNodeId to the previous TreeView's finalNodeId // - Each TreeViewItem's id is different even if two TreeViewItems point to the exact same ReferenceNode. That's because TreeView // doesn't work well when some TreeViewItems share the same id (e.g. while navigating the tree with arrow keys) public int initialNodeId, finalNodeId; // Not using the built-in searchString and hasSearch properties of TreeView because: // - This search algorithm is a bit more complicated than usual, we don't flatten the tree during the search // - If code is recompiled while searchString wasn't empty, the tree isn't rebuilt and remains empty (at least on Unity 5.6) public string searchTerm; public SearchResultTreeView.SearchMode searchMode = SearchResultTreeView.SearchMode.All; public bool selectionChangedDuringSearch; public List preSearchExpandedIds; } public class SearchResultTreeView : TreeView { public enum TreeType { Normal, UnusedObjects, IsolatedView }; public enum SearchMode { SearchedObjectsOnly, ReferencesOnly, All }; private class ReferenceNodeData { public readonly TreeViewItem item; public readonly ReferenceNode node; public readonly ReferenceNodeData parent; public readonly int linkIndex; public bool isLastLink; public bool isDuplicate; public bool shouldExpandAfterSearch; private string m_tooltipText; public string tooltipText { get { if( m_tooltipText != null ) return m_tooltipText; return GetTooltipText( Utilities.stringBuilder ); } } public ReferenceNodeData( TreeViewItem item, ReferenceNode node, ReferenceNodeData parent, int linkIndex ) { this.item = item; this.node = node; this.parent = parent; this.linkIndex = linkIndex; } private string GetTooltipText( StringBuilder sb ) { sb.Length = 0; sb.Append( "- " ).Append( node.Label ); if( parent != null ) { sb.Append( "\n" ); if( parent.node[linkIndex].descriptions.Count > 0 ) { List linkDescriptions = parent.node[linkIndex].descriptions; for( int i = 0; i < linkDescriptions.Count; i++ ) sb.Append( " " ).Append( linkDescriptions[i] ).Append( "\n" ); } if( parent.m_tooltipText != null ) sb.Append( parent.m_tooltipText ); else // Cache parents' tooltips along the way because they'll likely be reused frequently. We need to use new StringBuilder instances for them sb.Append( parent.GetTooltipText( new StringBuilder( 256 ) ) ); } m_tooltipText = sb.ToString(); return m_tooltipText; } public void ResetTooltip() { m_tooltipText = null; } } private const float SEARCHED_OBJECTS_BORDER_THICKNESS = 1f; #if UNITY_2019_3_OR_NEWER private const float TREE_VIEW_LINES_THICKNESS = 1.5f; // There are inexplicable spaces between the vertical and horizontal lines if we don't change thickness by 0.5f on 2019.3+ #else private const float TREE_VIEW_LINES_THICKNESS = 2f; #endif private const float HIGHLIGHTED_TREE_VIEW_LINES_THICKNESS = TREE_VIEW_LINES_THICKNESS * 2f; private readonly new SearchResultTreeViewState state; private readonly List references; private readonly List idToNodeDataLookup = new List( 128 ); private readonly HashSet selectedReferenceNodes = new HashSet(); private readonly HashSet selectedReferenceNodesHierarchyIds = new HashSet(); private readonly HashSet selectedReferenceNodesHierarchyIndirectIds = new HashSet(); private readonly HashSet usedObjectsSet; private readonly TreeType treeType; private readonly bool hideDuplicateRows; private readonly bool hideReduntantPrefabVariantLinks; private bool isSearching; #if !UNITY_2018_2_OR_NEWER public int visibleRowTop = 0, visibleRowBottom = int.MaxValue; #endif private readonly CompareInfo textComparer = new CultureInfo( "en-US" ).CompareInfo; private readonly CompareOptions textCompareOptions = CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace; private readonly GUIContent sharedGUIContent = new GUIContent(); private GUIStyle foldoutLabelStyle; private Texture2D whiteGradientTexture; private string highlightedSearchTextColor; private ReferenceNodeData prevHoveredData, hoveredData; private Rect hoveredDataRect; private bool isTreeViewEmpty; private bool isLMBDown; private double customTooltipShowTime; public new float rowHeight { get { return base.rowHeight; } set { base.rowHeight = value; #if !UNITY_2019_3_OR_NEWER customFoldoutYOffset = ( value - EditorGUIUtility.singleLineHeight ) * 0.5f; #endif } } // Avoid using these properties of TreeView by mistake [System.Obsolete] private new string searchString { get; } [System.Obsolete] private new bool hasSearch { get; } public SearchResultTreeView( SearchResultTreeViewState state, List references, TreeType treeType, HashSet usedObjectsSet, bool hideDuplicateRows, bool hideReduntantPrefabVariantLinks, bool usesExternalScrollView ) : base( state ) { this.state = state; this.references = references; this.treeType = treeType; this.hideDuplicateRows = hideDuplicateRows; this.hideReduntantPrefabVariantLinks = hideReduntantPrefabVariantLinks; highlightedSearchTextColor = ""; rowHeight = EditorGUIUtility.singleLineHeight + AssetUsageDetectorSettings.ExtraRowHeight; if( treeType == TreeType.UnusedObjects ) { showBorder = true; this.usedObjectsSet = usedObjectsSet; } if( treeType != TreeType.IsolatedView ) { // Draw only the visible rows. This requires setting useScrollView to false because we are using an external scroll view: https://docs.unity3d.com/ScriptReference/IMGUI.Controls.TreeView-useScrollView.html #if UNITY_2018_2_OR_NEWER useScrollView = false; #else // In my tests, SetUseScrollView seems to have no effect unfortunately but let's keep this line in case it fixes some other issues with the external scroll view object treeViewController = typeof( TreeView ).GetField( "m_TreeView", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance ).GetValue( this ); treeViewController.GetType().GetMethod( "SetUseScrollView", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance ).Invoke( treeViewController, new object[1] { false } ); #endif } isSearching = !string.IsNullOrEmpty( state.searchTerm ); Reload(); if( HasSelection() ) RefreshSelectedNodes( GetSelection() ); } public void RefreshSearch( string prevSearchTerm ) { bool wasSearchTermEmpty = string.IsNullOrEmpty( prevSearchTerm ); bool isSearchTermEmpty = string.IsNullOrEmpty( state.searchTerm ); isSearching = !isSearchTermEmpty; if( !wasSearchTermEmpty || !isSearchTermEmpty ) { Reload(); if( !isSearchTermEmpty ) { if( wasSearchTermEmpty ) { state.preSearchExpandedIds = new List( GetExpanded() ?? new int[0] ); state.selectionChangedDuringSearch = false; } ExpandMatchingSearchResults(); } else if( !wasSearchTermEmpty && state.preSearchExpandedIds != null && state.preSearchExpandedIds.Count > 0 ) { List expandedIds = state.preSearchExpandedIds; HashSet expandedIdsSet = new HashSet( expandedIds ); if( state.selectionChangedDuringSearch ) { IList selection = GetSelection(); for( int i = 0; i < selection.Count; i++ ) { for( TreeViewItem item = GetDataFromId( selection[i] ).item; item != null; item = item.parent ) { if( expandedIdsSet.Add( item.id ) ) expandedIds.Add( item.id ); else break; } } } SetExpanded( state.preSearchExpandedIds ); expandedIds.Clear(); } if( HasSelection() ) RefreshSelectedNodes( GetSelection() ); } } protected override TreeViewItem BuildRoot() { TreeViewItem root = new TreeViewItem { id = state.initialNodeId, depth = -1, displayName = "Root" }; int id = state.initialNodeId + 1; idToNodeDataLookup.Clear(); List stack = new List( 8 ); HashSet processedNodes = null; if( hideDuplicateRows ) { processedNodes = new HashSet(); for( int i = references.Count - 1; i >= 0; i-- ) { // Don't mark root nodes as duplicates unless we're in ReferencesOnly search mode (in which case, it's just technically unfeasible to know which root nodes will be displayed in advance) if( !isSearching || ( state.searchMode != SearchMode.ReferencesOnly && textComparer.IndexOf( references[i].Label, state.searchTerm, textCompareOptions ) >= 0 ) ) processedNodes.Add( references[i] ); } } for( int i = 0; i < references.Count; i++ ) GenerateRowsRecursive( root, references[i], null, i, 0, null, stack, processedNodes, ref id ); isTreeViewEmpty = !root.hasChildren; if( isTreeViewEmpty ) // May happen if all items are hidden inside HideItems function or there are no matching search results. If we don't create a dummy child, Unity throws an exception root.AddChild( new TreeViewItem( state.initialNodeId + 1 ) ); // If we don't give it a valid id, some functions throw exceptions when there are no matching search results else GetDataFromId( root.children[root.children.Count - 1].id ).isLastLink = true; state.finalNodeId = id + 1; return root; } private bool GenerateRowsRecursive( TreeViewItem parent, ReferenceNode referenceNode, ReferenceNodeData parentData, int siblingIndex, int depth, bool? itemForcedVisibility, List stack, HashSet processedNodes, ref int id ) { TreeViewItem item = new TreeViewItem( id++, depth, "" ); ReferenceNodeData data = new ReferenceNodeData( item, referenceNode, parentData, siblingIndex ); bool shouldShowItem; if( itemForcedVisibility.HasValue ) shouldShowItem = itemForcedVisibility.Value; else { if( !isSearching ) shouldShowItem = true; else if( state.searchMode == SearchMode.All || ( ( depth == 0 ) == ( state.searchMode == SearchMode.SearchedObjectsOnly ) ) ) { shouldShowItem = textComparer.IndexOf( referenceNode.Label, state.searchTerm, textCompareOptions ) >= 0; if( !shouldShowItem && depth > 0 ) { List descriptions = parentData.node[siblingIndex].descriptions; for( int i = descriptions.Count - 1; i >= 0; i-- ) { if( textComparer.IndexOf( descriptions[i], state.searchTerm, textCompareOptions ) >= 0 ) { shouldShowItem = true; break; } } } data.shouldExpandAfterSearch = shouldShowItem; if( state.searchMode == SearchMode.SearchedObjectsOnly || ( state.searchMode == SearchMode.All && shouldShowItem ) ) itemForcedVisibility = shouldShowItem; } else shouldShowItem = false; } idToNodeDataLookup.Add( data ); // Disallow recursion (stack) because it would crash Unity if( referenceNode.NumberOfOutgoingLinks > 0 && !stack.ContainsFast( referenceNode ) ) { // Add children only if hideDuplicateRows is false (processedNodes == null) or this node hasn't been seen before if( processedNodes != null && !processedNodes.Add( referenceNode ) && depth > 0 ) // "depth > 0": Root nodes are either added to processedNodes prior to generating rows (so that they're never marked as duplicate), or they just shouldn't be trimmed data.isDuplicate = true; else { stack.Add( referenceNode ); // Generate child items even if they will be forced invisible so that each visible row's id is deterministic and doesn't change when some rows become invisible for( int i = 0; i < referenceNode.NumberOfOutgoingLinks; i++ ) shouldShowItem |= GenerateRowsRecursive( item, referenceNode[i].targetNode, data, i, depth + 1, itemForcedVisibility, stack, processedNodes, ref id ); stack.RemoveAt( stack.Count - 1 ); } } if( shouldShowItem ) { if( item.hasChildren ) GetDataFromId( item.children[item.children.Count - 1].id ).isLastLink = true; parent.AddChild( item ); return true; } return false; } private ReferenceNodeData GetDataFromId( int id ) { return idToNodeDataLookup[id - state.initialNodeId - 1]; } public override void OnGUI( Rect rect ) { // Disallow clicking on "No matching results" text when in search mode bool guiEnabled = GUI.enabled; if( isTreeViewEmpty ) GUI.enabled = false; // Mouse and special keyboard events are already in Used state in CommandEventHandling, so we need to process them here Event ev = Event.current; if( ev.type == EventType.MouseDown ) { if( ev.button == 0 ) isLMBDown = true; } else if( ev.type == EventType.MouseUp ) { if( ev.button == 0 ) isLMBDown = false; } else if( ev.type == EventType.MouseMove ) hoveredData = null; else if( ev.type == EventType.KeyDown ) { if( ( ev.keyCode == KeyCode.Return || ev.keyCode == KeyCode.KeypadEnter ) && HasSelection() && HasFocus() ) { DoubleClickedItem( state.lastClickedID ); ev.Use(); } } base.OnGUI( rect ); if( prevHoveredData != hoveredData ) { if( AssetUsageDetectorSettings.CustomTooltipDelay > 0f ) EditorApplication.update -= ShowTooltipDelayed; prevHoveredData = hoveredData; if( hoveredData != null ) { if( AssetUsageDetectorSettings.CustomTooltipDelay <= 0f ) SearchResultTooltip.Show( hoveredDataRect, hoveredData.tooltipText ); else { customTooltipShowTime = EditorApplication.timeSinceStartup + AssetUsageDetectorSettings.CustomTooltipDelay; EditorApplication.update += ShowTooltipDelayed; } } else SearchResultTooltip.Hide(); Repaint(); } GUI.enabled = guiEnabled; } protected override void RowGUI( RowGUIArgs args ) { #if !UNITY_2018_2_OR_NEWER // Do manual row culling on early Unity versions if( args.row < visibleRowTop || args.row > visibleRowBottom ) return; #endif if( isTreeViewEmpty ) { EditorGUI.LabelField( args.rowRect, "No matching results..." ); return; } Event ev = Event.current; ReferenceNodeData data = GetDataFromId( args.item.id ); Rect rect = args.rowRect; if( string.IsNullOrEmpty( args.item.displayName ) ) { Object unityObject = data.node.UnityObject; if( unityObject ) args.item.icon = AssetPreview.GetMiniThumbnail( unityObject ); StringBuilder sb = Utilities.stringBuilder; sb.Length = 0; if( data.isDuplicate ) sb.Append( "[D] " ); if( data.parent == null ) { if( treeType != TreeType.UnusedObjects ) sb.Append( "" ); else if( data.node.usedState == ReferenceNode.UsedState.MixedCollapsed ) sb.Append( "[!] " ); else if( data.node.usedState == ReferenceNode.UsedState.MixedExpanded ) sb.Append( "[!] " ); if( !isSearching || state.searchMode == SearchMode.ReferencesOnly ) sb.Append( data.node.Label ); else HighlightSearchTermInString( sb, data.node.Label ); if( treeType != TreeType.UnusedObjects ) sb.Append( "" ); } else { List linkDescriptions = data.parent.node[data.linkIndex].descriptions; if( linkDescriptions.Count > 0 ) { if( !isSearching || state.searchMode == SearchMode.SearchedObjectsOnly ) sb.Append( data.node.Label ).Append( " " ).Append( linkDescriptions[0] ); else { HighlightSearchTermInString( sb, data.node.Label ); sb.Append( " " ); HighlightSearchTermInString( sb, linkDescriptions[0] ); } if( linkDescriptions.Count > 1 ) { bool shouldHighlightRemainingLinkDescriptions = false; if( isSearching && state.searchMode != SearchMode.SearchedObjectsOnly ) { for( int i = linkDescriptions.Count - 1; i > 0; i-- ) { if( textComparer.IndexOf( linkDescriptions[i], state.searchTerm, textCompareOptions ) >= 0 ) { shouldHighlightRemainingLinkDescriptions = true; sb.Append( highlightedSearchTextColor ); break; } } } sb.Append( " and " ).Append( linkDescriptions.Count - 1 ).Append( " more" ); if( shouldHighlightRemainingLinkDescriptions ) sb.Append( "" ); } sb.Append( "" ); } else if( isSearching && state.searchMode != SearchMode.SearchedObjectsOnly ) HighlightSearchTermInString( sb, data.node.Label ); else sb.Append( data.node.Label ); } args.item.displayName = sb.ToString(); } sharedGUIContent.text = args.item.displayName; sharedGUIContent.tooltip = AssetUsageDetectorSettings.ShowUnityTooltip ? data.tooltipText : null; sharedGUIContent.image = args.item.icon; if( ev.type == EventType.Repaint ) { if( treeType != TreeType.UnusedObjects ) { if( args.item.depth == 0 ) { Color guiColor = GUI.color; // Draw background if( !args.selected ) { GUI.color = guiColor * ( ( AssetUsageDetectorSettings.ApplySelectedRowParentsTintToRootRows && selectedReferenceNodesHierarchyIds.Contains( args.item.id ) ) ? AssetUsageDetectorSettings.SelectedRowParentsTint : AssetUsageDetectorSettings.RootRowsBackgroundColor ); GUI.DrawTexture( rect, EditorGUIUtility.whiteTexture, ScaleMode.StretchToFill, true, 0f ); } // Draw border: https://github.com/Unity-Technologies/UnityCsReference/blob/33cbfe062d795667c39e16777230e790fcd4b28b/Editor/Mono/GUI/InternalEditorGUI.cs#L262-L275 if( AssetUsageDetectorSettings.RootRowsBorderColor.a > 0f ) { GUI.color = guiColor * AssetUsageDetectorSettings.RootRowsBorderColor; GUI.DrawTexture( new Rect( rect.x, rect.y, rect.width, SEARCHED_OBJECTS_BORDER_THICKNESS ), EditorGUIUtility.whiteTexture, ScaleMode.StretchToFill, true, 0f ); // Draw bottom border only if there isn't another searched object immediately below this one (otherwise, this bottom border and the following top border are drawn at the same space, resulting in darker shade for that edge) if( data.isLastLink || ( args.item.hasChildren && IsExpanded( args.item.id ) ) ) { GUI.DrawTexture( new Rect( rect.x, rect.yMax - SEARCHED_OBJECTS_BORDER_THICKNESS, rect.width, SEARCHED_OBJECTS_BORDER_THICKNESS ), EditorGUIUtility.whiteTexture, ScaleMode.StretchToFill, true, 0f ); GUI.DrawTexture( new Rect( rect.x, rect.y + 1, SEARCHED_OBJECTS_BORDER_THICKNESS, rect.height - 2f * SEARCHED_OBJECTS_BORDER_THICKNESS ), EditorGUIUtility.whiteTexture, ScaleMode.StretchToFill, true, 0f ); GUI.DrawTexture( new Rect( rect.xMax - SEARCHED_OBJECTS_BORDER_THICKNESS, rect.y + 1, SEARCHED_OBJECTS_BORDER_THICKNESS, rect.height - 2f * SEARCHED_OBJECTS_BORDER_THICKNESS ), EditorGUIUtility.whiteTexture, ScaleMode.StretchToFill, true, 0f ); } else { GUI.DrawTexture( new Rect( rect.x, rect.y + 1, SEARCHED_OBJECTS_BORDER_THICKNESS, rect.height ), EditorGUIUtility.whiteTexture, ScaleMode.StretchToFill, true, 0f ); GUI.DrawTexture( new Rect( rect.xMax - SEARCHED_OBJECTS_BORDER_THICKNESS, rect.y + 1, SEARCHED_OBJECTS_BORDER_THICKNESS, rect.height ), EditorGUIUtility.whiteTexture, ScaleMode.StretchToFill, true, 0f ); } } GUI.color = guiColor; } else { if( !args.selected ) { if( selectedReferenceNodesHierarchyIds.Contains( args.item.id ) ) EditorGUI.DrawRect( rect, AssetUsageDetectorSettings.SelectedRowParentsTint ); if( data.node.IsMainReference ) EditorGUI.DrawRect( new Rect( rect.x, rect.y, GetContentIndent( args.item ) - 1f, rect.height ), AssetUsageDetectorSettings.MainReferencesBackgroundColor ); } } } else { if( !args.selected && data.node.usedState == ReferenceNode.UsedState.Used ) EditorGUI.DrawRect( new Rect( rect.x, rect.y, GetContentIndent( args.item ) - 1f, rect.height ), AssetUsageDetectorSettings.MainReferencesBackgroundColor ); } if( !isLMBDown && treeType != TreeType.UnusedObjects && !args.selected && selectedReferenceNodes.Contains( data.node ) ) { if( !whiteGradientTexture ) { whiteGradientTexture = new Texture2D( 2, 1, TextureFormat.RGBA32, false ) { hideFlags = HideFlags.HideAndDontSave, alphaIsTransparency = true, filterMode = FilterMode.Bilinear, wrapMode = TextureWrapMode.Clamp }; whiteGradientTexture.SetPixels32( new Color32[2] { Color.white, new Color32( 255, 255, 255, 0 ) } ); whiteGradientTexture.Apply( false, true ); } Color guiColor = GUI.color; GUI.color = guiColor * AssetUsageDetectorSettings.SelectedRowOccurrencesColor; GUI.DrawTexture( new Rect( GetContentIndent( args.item ), rect.y, 125f, rect.height ), whiteGradientTexture, ScaleMode.StretchToFill, true, 0f ); GUI.color = guiColor; } if( hoveredData == data ) EditorGUI.DrawRect( rect, new Color( 0.5f, 0.5f, 0.5f, 0.25f ) ); if( AssetUsageDetectorSettings.ShowTreeLines && args.item.depth > 0 ) { // I was using EditorGUI.DrawRect here but looking at its source code, it's more performant to call GUI.DrawTexture directly: https://github.com/Unity-Technologies/UnityCsReference/blob/e740821767d2290238ea7954457333f06e952bad/Editor/Mono/GUI/InternalEditorGUI.cs#L246-L255 Color guiColor = GUI.color; bool shouldHighlightTreeLine; Rect verticalLineRect = new Rect( rect.x + GetContentIndent( args.item.parent ) - ( foldoutWidth + TREE_VIEW_LINES_THICKNESS ) * 0.5f - 2f, rect.y, TREE_VIEW_LINES_THICKNESS, rect.height ); Rect horizontalLineRect = new Rect( verticalLineRect.x, verticalLineRect.y + ( verticalLineRect.height - TREE_VIEW_LINES_THICKNESS ) * 0.5f, foldoutWidth + TREE_VIEW_LINES_THICKNESS - 4f, TREE_VIEW_LINES_THICKNESS ); for( ReferenceNodeData parentData = data.parent; parentData.parent != null; parentData = parentData.parent ) { if( !parentData.isLastLink ) { shouldHighlightTreeLine = selectedReferenceNodesHierarchyIndirectIds.Contains( parentData.item.id ); Rect _verticalLineRect = new Rect( verticalLineRect.x - depthIndentWidth * ( args.item.depth - parentData.item.depth ), verticalLineRect.y, verticalLineRect.width, verticalLineRect.height ); if( shouldHighlightTreeLine ) { _verticalLineRect.x -= ( HIGHLIGHTED_TREE_VIEW_LINES_THICKNESS - TREE_VIEW_LINES_THICKNESS ) * 0.5f; _verticalLineRect.width = HIGHLIGHTED_TREE_VIEW_LINES_THICKNESS; } GUI.color = guiColor * ( shouldHighlightTreeLine ? AssetUsageDetectorSettings.HighlightedTreeLinesColor : AssetUsageDetectorSettings.TreeLinesColor ); GUI.DrawTexture( _verticalLineRect, EditorGUIUtility.whiteTexture, ScaleMode.StretchToFill, true, 0f ); } } bool isInSelectedReferenceNodesHierarchy = selectedReferenceNodesHierarchyIds.Contains( args.item.id ); if( isInSelectedReferenceNodesHierarchy ) { horizontalLineRect.y -= ( HIGHLIGHTED_TREE_VIEW_LINES_THICKNESS - TREE_VIEW_LINES_THICKNESS ) * 0.5f; horizontalLineRect.height = HIGHLIGHTED_TREE_VIEW_LINES_THICKNESS; } GUI.color = guiColor * ( isInSelectedReferenceNodesHierarchy ? AssetUsageDetectorSettings.HighlightedTreeLinesColor : AssetUsageDetectorSettings.TreeLinesColor ); GUI.DrawTexture( horizontalLineRect, EditorGUIUtility.whiteTexture, ScaleMode.StretchToFill, true, 0f ); if( data.isLastLink ) verticalLineRect.height = ( verticalLineRect.height + TREE_VIEW_LINES_THICKNESS ) * 0.5f; GUI.color = guiColor * AssetUsageDetectorSettings.TreeLinesColor; GUI.DrawTexture( verticalLineRect, EditorGUIUtility.whiteTexture, ScaleMode.StretchToFill, true, 0f ); bool isInSelectedReferenceNodesIndirectHierarchy = selectedReferenceNodesHierarchyIndirectIds.Contains( args.item.id ); if( isInSelectedReferenceNodesHierarchy || isInSelectedReferenceNodesIndirectHierarchy ) { GUI.color = guiColor * AssetUsageDetectorSettings.HighlightedTreeLinesColor; if( isInSelectedReferenceNodesHierarchy && !isInSelectedReferenceNodesIndirectHierarchy ) { if( !data.isLastLink ) verticalLineRect.height = ( verticalLineRect.height + HIGHLIGHTED_TREE_VIEW_LINES_THICKNESS ) * 0.5f; else verticalLineRect.height += ( HIGHLIGHTED_TREE_VIEW_LINES_THICKNESS - TREE_VIEW_LINES_THICKNESS ) * 0.5f; } verticalLineRect.x -= ( HIGHLIGHTED_TREE_VIEW_LINES_THICKNESS - TREE_VIEW_LINES_THICKNESS ) * 0.5f; verticalLineRect.width = HIGHLIGHTED_TREE_VIEW_LINES_THICKNESS; GUI.DrawTexture( verticalLineRect, EditorGUIUtility.whiteTexture, ScaleMode.StretchToFill, true, 0f ); } GUI.color = guiColor; } rect.xMin += GetContentIndent( args.item ); rect.y += AssetUsageDetectorSettings.ExtraRowHeight * 0.5f; #if !UNITY_2019_3_OR_NEWER rect.y -= 2f; #endif rect.height += 4f; // Incrementing height fixes cropped icon issue on Unity 2019.2 or earlier if( foldoutLabelStyle == null ) foldoutLabelStyle = new GUIStyle( DefaultStyles.foldoutLabel ) { richText = true }; foldoutLabelStyle.Draw( rect, sharedGUIContent, false, false, args.selected && args.focused, args.selected ); // The only way to support Unity's tooltips seems to be by drawing an invisible GUI.Label over our own label if( sharedGUIContent.tooltip != null ) { sharedGUIContent.text = ""; sharedGUIContent.image = null; GUI.Label( rect, sharedGUIContent, foldoutLabelStyle ); } } else if( ev.type == EventType.MouseDown ) { if( ev.button == 2 && rect.Contains( ev.mousePosition ) ) { HideItems( new int[1] { args.item.id } ); GUIUtility.ExitGUI(); } } else if( ev.type == EventType.MouseMove ) { if( hoveredData != data && AssetUsageDetectorSettings.ShowCustomTooltip && rect.Contains( ev.mousePosition ) ) { hoveredData = data; hoveredDataRect = new Rect( GUIUtility.GUIToScreenPoint( rect.position ), new Vector2( EditorGUIUtility.currentViewWidth, 0f ) ); } } } protected override void SelectionChanged( IList selectedIds ) { if( isTreeViewEmpty ) return; RefreshSelectedNodes( selectedIds ); if( selectedIds.Count == 0 ) return; if( isSearching ) state.selectionChangedDuringSearch = true; Object selection, pingTarget = null; List selectedUnityObjects = new List( selectedIds.Count ); for( int i = 0; i < selectedIds.Count; i++ ) { Object obj = GetDataFromId( selectedIds[i] ).node.UnityObject; if( obj ) { obj.GetObjectsToSelectAndPing( out selection, out pingTarget ); if( selection && !selectedUnityObjects.Contains( selection ) ) selectedUnityObjects.Add( selection ); } } if( selectedUnityObjects.Count > 0 ) { if( AssetUsageDetectorSettings.PingClickedObjects && pingTarget ) EditorGUIUtility.PingObject( pingTarget ); if( AssetUsageDetectorSettings.SelectClickedObjects || ( AssetUsageDetectorSettings.SelectDoubleClickedObjects && selectedUnityObjects.Count > 1 ) ) Selection.objects = selectedUnityObjects.ToArray(); } } protected override void DoubleClickedItem( int id ) { if( isTreeViewEmpty ) return; isLMBDown = false; Object clickedObject = GetDataFromId( id ).node.UnityObject; #if UNITY_2018_3_OR_NEWER if( clickedObject && clickedObject.IsAsset() ) { GameObject clickedPrefabRoot = null; if( clickedObject is Component ) clickedPrefabRoot = ( (Component) clickedObject ).transform.root.gameObject; else if( clickedObject is GameObject ) clickedPrefabRoot = ( (GameObject) clickedObject ).transform.root.gameObject; if( clickedPrefabRoot ) { PrefabAssetType prefabAssetType = PrefabUtility.GetPrefabAssetType( clickedPrefabRoot ); if( prefabAssetType == PrefabAssetType.Regular || prefabAssetType == PrefabAssetType.Variant ) { // Try to open the prefab stage of this prefab string assetPath = AssetDatabase.GetAssetPath( clickedPrefabRoot ); PrefabStage openPrefabStage = PrefabStageUtility.GetCurrentPrefabStage(); #if UNITY_2020_1_OR_NEWER if( openPrefabStage == null || !openPrefabStage.stageHandle.IsValid() || assetPath != openPrefabStage.assetPath ) #else if( openPrefabStage == null || !openPrefabStage.stageHandle.IsValid() || assetPath != openPrefabStage.prefabAssetPath ) #endif AssetDatabase.OpenAsset( clickedPrefabRoot ); } } } #endif // Ping the clicked GameObject in the open prefab stage Object selection, pingTarget; clickedObject.GetObjectsToSelectAndPing( out selection, out pingTarget ); if( AssetUsageDetectorSettings.PingClickedObjects && pingTarget ) EditorGUIUtility.PingObject( pingTarget ); if( AssetUsageDetectorSettings.SelectDoubleClickedObjects ) Selection.activeObject = selection; } protected override void ContextClickedItem( int id ) { ContextClicked(); } protected override void ContextClicked() { if( !isTreeViewEmpty && HasSelection() && HasFocus() ) { IList selection = SortItemIDsInRowOrder( GetSelection() ); bool hasAnyDuplicateRows = false, hasAnyRowWithOutgoingLinks = false, hasAnyUnusedMixedCollapsedNode = false; for( int i = 0; i < selection.Count; i++ ) { ReferenceNodeData data = GetDataFromId( selection[i] ); if( !hasAnyDuplicateRows && data.isDuplicate ) hasAnyDuplicateRows = true; if( !hasAnyRowWithOutgoingLinks && data.node.NumberOfOutgoingLinks > 0 ) hasAnyRowWithOutgoingLinks = true; if( !hasAnyUnusedMixedCollapsedNode && data.node.usedState == ReferenceNode.UsedState.MixedCollapsed ) hasAnyUnusedMixedCollapsedNode = true; } GenericMenu contextMenu = new GenericMenu(); if( treeType != TreeType.IsolatedView ) contextMenu.AddItem( new GUIContent( "Hide" ), false, () => HideItems( selection ) ); if( treeType == TreeType.UnusedObjects ) { if( hasAnyUnusedMixedCollapsedNode ) { if( contextMenu.GetItemCount() > 0 ) contextMenu.AddSeparator( "" ); contextMenu.AddItem( new GUIContent( "Show Used Children" ), false, ShowChildrenOfSelectedUnusedObjects ); } } else { if( contextMenu.GetItemCount() > 0 ) contextMenu.AddSeparator( "" ); if( hasAnyDuplicateRows ) contextMenu.AddItem( new GUIContent( "Select First Occurrence" ), false, SelectFirstOccurrencesOfDuplicateSelection ); contextMenu.AddItem( new GUIContent( "Expand All Occurrences" ), false, ExpandAllSelectionOccurrences ); } if( hasAnyRowWithOutgoingLinks ) { if( contextMenu.GetItemCount() > 0 ) contextMenu.AddSeparator( "" ); contextMenu.AddItem( new GUIContent( "Show Children In New Window" ), false, ShowChildrenOfSelectionInNewWindow ); } contextMenu.ShowAsContext(); if( Event.current != null && Event.current.type == EventType.ContextClick ) Event.current.Use(); // It's safer to eat the event and if we don't, the context menu is sometimes displayed with a delay } } protected override void CommandEventHandling() { if( !isTreeViewEmpty && HasFocus() ) // There may be multiple SearchResultTreeViews. Execute the event only for the currently focused one { Event ev = Event.current; if( ev.type == EventType.ValidateCommand || ev.type == EventType.ExecuteCommand ) { if( ev.commandName == "Delete" || ev.commandName == "SoftDelete" ) { if( ev.type == EventType.ExecuteCommand ) HideItems( GetSelection() ); ev.Use(); return; } } } base.CommandEventHandling(); } protected override bool CanStartDrag( CanStartDragArgs args ) { return true; } protected override void SetupDragAndDrop( SetupDragAndDropArgs args ) { IList draggedItemIds = args.draggedItemIDs; if( draggedItemIds.Count == 0 ) return; List draggedUnityObjects = new List( draggedItemIds.Count ); for( int i = 0; i < draggedItemIds.Count; i++ ) { Object obj = GetDataFromId( draggedItemIds[i] ).node.UnityObject; if( obj ) draggedUnityObjects.Add( obj ); } if( draggedUnityObjects.Count > 0 ) { DragAndDrop.objectReferences = draggedUnityObjects.ToArray(); DragAndDrop.StartDrag( draggedUnityObjects.Count > 1 ? "" : draggedUnityObjects[0].name ); } } public void ExpandDirectReferences() { List expandedIds = new List( rootItem.children.Count ); for( int i = 0; i < rootItem.children.Count; i++ ) expandedIds.Add( rootItem.children[i].id ); SetExpanded( expandedIds ); } public void ExpandMainReferences() { List expandedIds = new List( references.Count * 12 ); for( int i = 0; i < rootItem.children.Count; i++ ) GetMainReferenceIdsRecursive( rootItem.children[i], expandedIds ); SetExpanded( expandedIds ); } public void ExpandMatchingSearchResults() { if( state.searchMode != SearchMode.ReferencesOnly ) return; List expandedIds = new List( references.Count * 12 ); for( int i = 0; i < rootItem.children.Count; i++ ) GetMatchingSearchResultIdsRecursive( rootItem.children[i], expandedIds ); SetExpanded( expandedIds ); } private void ExpandAllSelectionOccurrences() { IList selection = GetSelection(); if( selection.Count == 0 ) return; HashSet selectedNodes = new HashSet(); for( int i = selection.Count - 1; i >= 0; i-- ) selectedNodes.Add( GetDataFromId( selection[i] ).node ); List expandedIds = new List( GetExpanded() ); for( int i = 0; i < rootItem.children.Count; i++ ) GetReferenceNodeOccurrenceIdsRecursive( rootItem.children[i], selectedNodes, expandedIds ); SetExpanded( expandedIds ); } private bool GetMainReferenceIdsRecursive( TreeViewItem item, List ids ) { if( item.depth > 0 && GetDataFromId( item.id ).node.IsMainReference ) return true; bool shouldExpand = false; if( item.hasChildren ) { for( int i = 0; i < item.children.Count; i++ ) shouldExpand |= GetMainReferenceIdsRecursive( item.children[i], ids ); } else shouldExpand = true; // No main reference is encountered in this branch; expand the whole branch if( shouldExpand ) ids.Add( item.id ); return shouldExpand; } private bool GetMatchingSearchResultIdsRecursive( TreeViewItem item, List ids ) { bool shouldExpand = false; if( item.hasChildren ) { for( int i = 0; i < item.children.Count; i++ ) shouldExpand |= GetMatchingSearchResultIdsRecursive( item.children[i], ids ); } if( shouldExpand ) ids.Add( item.id ); else shouldExpand = GetDataFromId( item.id ).shouldExpandAfterSearch; return shouldExpand; } private bool GetReferenceNodeOccurrenceIdsRecursive( TreeViewItem item, HashSet referenceNodes, List ids ) { bool shouldExpand = false; if( item.hasChildren ) { for( int i = 0; i < item.children.Count; i++ ) shouldExpand |= GetReferenceNodeOccurrenceIdsRecursive( item.children[i], referenceNodes, ids ); } if( shouldExpand ) { if( !ids.Contains( item.id ) ) ids.Add( item.id ); return true; } else return referenceNodes.Contains( GetDataFromId( item.id ).node ); } private void HideItems( IList ids ) { if( ids.Count > 0 ) { List hiddenNodes = new List( ids.Count ); List hiddenLinks = new List( ids.Count ); List newExpandedItemIDs = new List( 32 ); List newSelectedItemIDs = new List( 16 ); for( int i = 0; i < ids.Count; i++ ) { ReferenceNodeData data = GetDataFromId( ids[i] ); if( data.item.depth > 0 ) hiddenLinks.Add( data.parent.node[data.linkIndex] ); else hiddenNodes.Add( data.node ); } int id = state.initialNodeId + 1; for( int i = 0; i < rootItem.children.Count; i++ ) CalculateNewItemIdsAfterHideRecursive( rootItem.children[i], hiddenNodes, hiddenLinks, newExpandedItemIDs, newSelectedItemIDs, ref id ); for( int i = 0; i < ids.Count; i++ ) { ReferenceNodeData data = GetDataFromId( ids[i] ); if( data.item.depth > 0 ) { // Can't remove by index here because if multiple sibling nodes are removed at once, the latter sibling nodes' linkIndex // will be different than their actual sibling indices until this TreeView is refreshed data.parent.node.RemoveLink( data.node ); } else references.Remove( data.node ); } SetSelection( newSelectedItemIDs ); SetExpanded( newExpandedItemIDs ); Reload(); } } private void CalculateNewItemIdsAfterHideRecursive( TreeViewItem item, List hiddenNodes, List hiddenLinks, List newExpandedItemIDs, List newSelectedItemIDs, ref int id ) { ReferenceNodeData data = GetDataFromId( item.id ); if( hiddenNodes.Contains( data.node ) || ( data.parent != null && hiddenLinks.Contains( data.parent.node[data.linkIndex] ) ) ) return; if( IsExpanded( item.id ) ) newExpandedItemIDs.Add( id ); if( IsSelected( item.id ) ) newSelectedItemIDs.Add( id ); id++; if( item.hasChildren ) { for( int i = 0; i < item.children.Count; i++ ) CalculateNewItemIdsAfterHideRecursive( item.children[i], hiddenNodes, hiddenLinks, newExpandedItemIDs, newSelectedItemIDs, ref id ); } } private void SelectFirstOccurrencesOfDuplicateSelection() { IList selection = GetSelection(); if( selection.Count == 0 ) return; HashSet selectedNodes = new HashSet(); for( int i = selection.Count - 1; i >= 0; i-- ) { ReferenceNodeData data = GetDataFromId( selection[i] ); if( data.isDuplicate ) selectedNodes.Add( data.node ); } List newSelection = new List( selection.Count ); for( int i = 0; i < rootItem.children.Count; i++ ) FindFirstOccurrencesOfSelectionRecursive( rootItem.children[i], selectedNodes, newSelection ); if( newSelection.Count > 0 ) { SetSelection( newSelection, TreeViewSelectionOptions.FireSelectionChanged | TreeViewSelectionOptions.RevealAndFrame ); if( treeType != TreeType.IsolatedView ) EditorWindow.focusedWindow.SendEvent( new Event() { type = EventType.KeyDown, keyCode = KeyCode.F } ); // To actually frame the row when external scroll view is used } } private void FindFirstOccurrencesOfSelectionRecursive( TreeViewItem item, HashSet selectedNodes, List result ) { ReferenceNodeData data = GetDataFromId( item.id ); if( !data.isDuplicate && selectedNodes.Remove( data.node ) ) result.Add( item.id ); if( item.hasChildren ) { for( int i = 0; i < item.children.Count; i++ ) FindFirstOccurrencesOfSelectionRecursive( item.children[i], selectedNodes, result ); } } private void ShowChildrenOfSelectedUnusedObjects() { IList selection = GetSelection(); if( selection.Count == 0 ) return; for( int i = selection.Count - 1; i >= 0; i-- ) { ReferenceNodeData data = GetDataFromId( selection[i] ); if( data.node.usedState != ReferenceNode.UsedState.MixedCollapsed ) continue; data.node.usedState = ReferenceNode.UsedState.MixedExpanded; Object unityObject = data.node.UnityObject; if( !unityObject ) continue; string assetPath = AssetDatabase.GetAssetPath( unityObject ); if( string.IsNullOrEmpty( assetPath ) ) { foreach( Object obj in usedObjectsSet ) { if( obj && obj is GameObject && obj != unityObject && ( (GameObject) obj ).transform.IsChildOf( ( (GameObject) unityObject ).transform ) ) { ReferenceNode childNode = new ReferenceNode() { nodeObject = obj }; childNode.InitializeRecursively(); data.node.AddLinkTo( childNode, "USED" ); } } } else { foreach( Object obj in usedObjectsSet ) { if( obj && AssetDatabase.GetAssetPath( obj ) == assetPath ) { ReferenceNode childNode = new ReferenceNode() { nodeObject = obj }; childNode.InitializeRecursively(); data.node.AddLinkTo( childNode, "USED" ); } } } } Reload(); } private void ShowChildrenOfSelectionInNewWindow() { IList selection = SortItemIDsInRowOrder( GetSelection() ); if( selection.Count == 0 ) return; List selectedNodes = new List( selection.Count ); for( int i = 0; i < selection.Count; i++ ) { ReferenceNodeData data = GetDataFromId( selection[i] ); if( data.node.NumberOfOutgoingLinks > 0 && !selectedNodes.Contains( data.node ) ) selectedNodes.Add( data.node ); } if( selectedNodes.Count > 0 ) { SearchResultTreeView isolatedTreeView = new SearchResultTreeView( new SearchResultTreeViewState(), selectedNodes, TreeType.IsolatedView, null, hideDuplicateRows, hideReduntantPrefabVariantLinks, false ); isolatedTreeView.ExpandMainReferences(); SearchResultTreeViewIsolatedView.Show( new Vector2( EditorWindow.focusedWindow.position.width, Mathf.Max( isolatedTreeView.totalHeight, EditorGUIUtility.singleLineHeight * 5f ) + 1f ), isolatedTreeView, new GUIContent( selectedNodes[0].Label + ( selectedNodes.Count <= 1 ? "" : ( " (and " + ( selectedNodes.Count - 1 ) + " more)" ) ) ) ); } } private void RefreshSelectedNodes( IList selectedIds ) { selectedReferenceNodes.Clear(); selectedReferenceNodesHierarchyIds.Clear(); selectedReferenceNodesHierarchyIndirectIds.Clear(); for( int i = 0; i < selectedIds.Count; i++ ) { ReferenceNodeData data = GetDataFromId( selectedIds[i] ); selectedReferenceNodes.Add( data.node ); selectedReferenceNodesHierarchyIds.Add( selectedIds[i] ); if( data.item.parent == null ) continue; TreeViewItem linkItem = data.item; for( TreeViewItem parentItem = linkItem.parent; parentItem.depth >= 0; parentItem = parentItem.parent ) { selectedReferenceNodesHierarchyIds.Add( parentItem.id ); List parentItemChildren = parentItem.children; for( int j = 0; parentItemChildren[j] != linkItem; j++ ) selectedReferenceNodesHierarchyIndirectIds.Add( parentItemChildren[j].id ); linkItem = parentItem; } } } private void ShowTooltipDelayed() { if( EditorApplication.timeSinceStartup >= customTooltipShowTime ) { EditorApplication.update -= ShowTooltipDelayed; if( GetRows().Contains( hoveredData.item ) ) // Make sure that the hovered item is still a part of the tree (e.g. it might have been removed with middle mouse button) SearchResultTooltip.Show( hoveredDataRect, hoveredData.tooltipText ); } } public void CancelDelayedTooltip() { EditorApplication.update -= ShowTooltipDelayed; } public void GetRowStateWithId( int id, out bool isFirstRow, out bool isLastRow, out bool isExpanded, out bool canExpand ) { if( isTreeViewEmpty ) { isFirstRow = isLastRow = true; isExpanded = canExpand = false; return; } IList rows = GetRows(); for( int i = 0; i < rows.Count; i++ ) { if( rows[i].id == id ) { isFirstRow = ( i <= 0 ); isLastRow = ( i >= rows.Count - 1 ); isExpanded = rows[i].hasChildren && IsExpanded( id ); canExpand = rows[i].hasChildren && !IsExpanded( id ); return; } } isFirstRow = isLastRow = isExpanded = canExpand = false; } public bool GetRowRectWithId( int id, out Rect rect ) { IList rows = GetRows(); for( int i = 0; i < rows.Count; i++ ) { if( rows[i].id == id ) { rect = GetRowRect( i ); return true; } } rect = new Rect(); return false; } public Rect SelectFirstRowAndReturnRect() { SetSelection( new int[1] { GetRows()[0].id }, TreeViewSelectionOptions.FireSelectionChanged ); return GetRowRect( 0 ); } public Rect SelectLastRowAndReturnRect() { IList rows = GetRows(); SetSelection( new int[1] { rows[rows.Count - 1].id }, TreeViewSelectionOptions.FireSelectionChanged ); return GetRowRect( rows.Count - 1 ); } private void HighlightSearchTermInString( StringBuilder sb, string str ) { int prevSearchOccurrenceIndex = 0, searchOccurrenceIndex = 0; while( ( searchOccurrenceIndex = textComparer.IndexOf( str, state.searchTerm, searchOccurrenceIndex, textCompareOptions ) ) >= 0 ) { sb.Append( str, prevSearchOccurrenceIndex, searchOccurrenceIndex - prevSearchOccurrenceIndex ); sb.Append( highlightedSearchTextColor ).Append( "" ); sb.Append( str, searchOccurrenceIndex, state.searchTerm.Length ); sb.Append( "" ).Append( "" ); searchOccurrenceIndex += state.searchTerm.Length; prevSearchOccurrenceIndex = searchOccurrenceIndex; } if( prevSearchOccurrenceIndex < str.Length ) sb.Append( str, prevSearchOccurrenceIndex, str.Length - prevSearchOccurrenceIndex ); } public void OnSettingsChanged( bool resetHighlightedSearchTextColor, bool resetTooltipDescriptionsTextColor ) { hoveredData = null; if( !resetHighlightedSearchTextColor && !resetTooltipDescriptionsTextColor ) return; if( resetHighlightedSearchTextColor ) highlightedSearchTextColor = ""; for( int i = idToNodeDataLookup.Count - 1; i >= 0; i-- ) { if( isSearching && resetHighlightedSearchTextColor ) idToNodeDataLookup[i].item.displayName = ""; if( resetTooltipDescriptionsTextColor ) idToNodeDataLookup[i].ResetTooltip(); } } } public class SearchResultTreeViewIsolatedView : EditorWindow { private SearchResultTreeView treeView; private bool shouldRepositionSelf = true; public static void Show( Vector2 preferredSize, SearchResultTreeView treeView, GUIContent title ) { SearchResultTreeViewIsolatedView window = CreateInstance(); window.treeView = treeView; window.titleContent = title; window.Show(); window.minSize = new Vector2( 150f, Mathf.Min( preferredSize.y, EditorGUIUtility.singleLineHeight * 2f ) ); window.position = new Rect( new Vector2( -9999f, -9999f ), preferredSize ); window.Repaint(); } private void OnEnable() { wantsMouseMove = wantsMouseEnterLeaveWindow = true; } private void OnGUI() { if( treeView == null ) // After domain reload Close(); else { treeView.OnGUI( GUILayoutUtility.GetRect( 0f, 100000f, 0f, 100000f ) ); if( shouldRepositionSelf ) { float preferredHeight = GUILayoutUtility.GetLastRect().height; if( preferredHeight > 10f ) { Vector2 size = position.size; position = Utilities.GetScreenFittedRect( new Rect( GUIUtility.GUIToScreenPoint( Event.current.mousePosition ) + new Vector2( size.x * -0.5f, 10f ), size ) ); shouldRepositionSelf = false; GUIUtility.ExitGUI(); } } } } } }