using System.Collections.Generic; using System.Collections.ObjectModel; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.SceneManagement; using UnityEngine.UI; #if ENABLE_INPUT_SYSTEM && !ENABLE_LEGACY_INPUT_MANAGER using UnityEngine.InputSystem; #endif namespace RuntimeInspectorNamespace { public class RuntimeHierarchy : SkinnedWindow, IListViewAdapter, ITooltipManager { [System.Flags] public enum SelectOptions { None = 0, Additive = 1, FocusOnSelection = 2, ForceRevealSelection = 4 }; public enum LongPressAction { None = 0, CreateDraggedReferenceItem = 1, ShowMultiSelectionToggles = 2, ShowMultiSelectionTogglesThenCreateDraggedReferenceItem = 3 }; public delegate void SelectionChangedDelegate( ReadOnlyCollection selection ); public delegate void DoubleClickDelegate( HierarchyData clickedItem ); public delegate bool GameObjectFilterDelegate( Transform transform ); #pragma warning disable 0649 [SerializeField] private float m_refreshInterval = 0f; public float RefreshInterval { get { return m_refreshInterval; } set { m_refreshInterval = value; } } [SerializeField] private float m_objectNamesRefreshInterval = 10f; public float ObjectNamesRefreshInterval { get { return m_objectNamesRefreshInterval; } set { m_objectNamesRefreshInterval = value; } } [SerializeField] private float m_searchRefreshInterval = 5f; public float SearchRefreshInterval { get { return m_searchRefreshInterval; } set { m_searchRefreshInterval = value; } } private float nextHierarchyRefreshTime = -1f; private float nextObjectNamesRefreshTime = -1f; private float nextSearchRefreshTime = -1f; [Space] [SerializeField] private bool m_allowMultiSelection = true; public bool AllowMultiSelection { get { return m_allowMultiSelection; } set { if( m_allowMultiSelection != value ) { m_allowMultiSelection = value; if( !value ) { MultiSelectionToggleSelectionMode = false; if( m_currentSelection.Count > 1 ) { for( int i = m_currentSelection.Count - 1; i >= 0; i-- ) { if( m_currentSelection[i] ) { singleTransformSelection[0] = m_currentSelection[i]; SelectInternal( singleTransformSelection ); return; } } DeselectInternal( null ); } } } } } private bool m_multiSelectionToggleSelectionMode = false; private bool justActivatedMultiSelectionToggleSelectionMode = false; public bool MultiSelectionToggleSelectionMode { get { return m_multiSelectionToggleSelectionMode; } set { if( !m_allowMultiSelection ) value = false; if( m_multiSelectionToggleSelectionMode != value ) { m_multiSelectionToggleSelectionMode = value; shouldRecalculateContentWidth = true; for( int i = drawers.Count - 1; i >= 0; i-- ) { if( drawers[i].gameObject.activeSelf ) drawers[i].MultiSelectionToggleVisible = value; } deselectAllButton.gameObject.SetActive( value ); if( !value ) EnsureScrollViewIsWithinBounds(); } } } [Space] [SerializeField] private bool m_exposeUnityScenes = true; public bool ExposeUnityScenes { get { return m_exposeUnityScenes; } set { if( m_exposeUnityScenes != value ) { m_exposeUnityScenes = value; for( int i = 0; i < SceneManager.sceneCount; i++ ) { if( value ) OnSceneLoaded( SceneManager.GetSceneAt( i ), LoadSceneMode.Single ); else OnSceneUnloaded( SceneManager.GetSceneAt( i ) ); } } } } [SerializeField, UnityEngine.Serialization.FormerlySerializedAs( "exposedScenes" )] private string[] exposedUnityScenesSubset; [SerializeField] private bool m_exposeDontDestroyOnLoadScene = true; public bool ExposeDontDestroyOnLoadScene { get { return m_exposeDontDestroyOnLoadScene; } set { if( m_exposeDontDestroyOnLoadScene != value ) { m_exposeDontDestroyOnLoadScene = value; if( value ) OnSceneLoaded( GetDontDestroyOnLoadScene(), LoadSceneMode.Single ); else OnSceneUnloaded( GetDontDestroyOnLoadScene() ); } } } [SerializeField] private string[] pseudoScenesOrder; [Space] [SerializeField] private LongPressAction m_pointerLongPressAction = LongPressAction.CreateDraggedReferenceItem; public LongPressAction PointerLongPressAction { get { return m_pointerLongPressAction; } set { m_pointerLongPressAction = value; } } [SerializeField, UnityEngine.Serialization.FormerlySerializedAs( "m_draggedReferenceHoldTime" )] private float m_pointerLongPressDuration = 0.4f; public float PointerLongPressDuration { get { return m_pointerLongPressDuration; } set { m_pointerLongPressDuration = value; } } [SerializeField] private float m_doubleClickThreshold = 0.5f; public float DoubleClickThreshold { get { return m_doubleClickThreshold; } set { m_doubleClickThreshold = value; } } [Space] [SerializeField] private bool m_canReorganizeItems = false; public bool CanReorganizeItems { get { return m_canReorganizeItems; } set { m_canReorganizeItems = value; } } [SerializeField] private bool m_canDropDraggedParentOnChild = false; public bool CanDropDraggedParentOnChild { get { return m_canDropDraggedParentOnChild; } set { m_canDropDraggedParentOnChild = value; } } [SerializeField] private bool m_canDropDraggedObjectsToPseudoScenes = false; public bool CanDropDraggedObjectsToPseudoScenes { get { return m_canDropDraggedObjectsToPseudoScenes; } set { m_canDropDraggedObjectsToPseudoScenes = value; } } [Space] [SerializeField] private bool m_showTooltips; public bool ShowTooltips { get { return m_showTooltips; } } [SerializeField] private float m_tooltipDelay = 0.5f; public float TooltipDelay { get { return m_tooltipDelay; } set { m_tooltipDelay = value; } } internal TooltipListener TooltipListener { get; private set; } [Space] [SerializeField] private bool m_showHorizontalScrollbar; public bool ShowHorizontalScrollbar { get { return m_showHorizontalScrollbar; } set { if( m_showHorizontalScrollbar != value ) { m_showHorizontalScrollbar = value; if( !value ) { scrollView.content.sizeDelta = new Vector2( 0f, scrollView.content.sizeDelta.y ); scrollView.horizontalNormalizedPosition = 0f; } else { for( int i = drawers.Count - 1; i >= 0; i-- ) { if( drawers[i].gameObject.activeSelf ) drawers[i].RefreshName(); } shouldRecalculateContentWidth = true; } scrollView.horizontal = value; } } } public string SearchTerm { get { return searchInputField.text; } set { searchInputField.text = value; } } private bool m_isInSearchMode = false; public bool IsInSearchMode { get { return m_isInSearchMode; } } #if UNITY_EDITOR [SerializeField] private bool syncSelectionWithEditorHierarchy = false; #endif [SerializeField] private RuntimeInspector m_connectedInspector; public RuntimeInspector ConnectedInspector { get { return m_connectedInspector; } set { if( m_connectedInspector != value ) { m_connectedInspector = value; for( int i = m_currentSelection.Count - 1; i >= 0; i-- ) { if( m_currentSelection[i] ) { m_connectedInspector.Inspect( m_currentSelection[i].gameObject ); break; } } } } } private bool m_isLocked = false; public bool IsLocked { get { return m_isLocked; } set { m_isLocked = value; } } [Header( "Internal Variables" )] [SerializeField] private ScrollRect scrollView; [SerializeField] private RectTransform drawArea; [SerializeField] private RecycledListView listView; [SerializeField] private Image background; [SerializeField] private Image verticalScrollbar; [SerializeField] private Image horizontalScrollbar; [SerializeField] private InputField searchInputField; [SerializeField] private Image searchIcon; [SerializeField] private Image searchInputFieldBackground; [SerializeField] private LayoutElement searchBarLayoutElement; [SerializeField] private Button deselectAllButton; [SerializeField] private LayoutElement deselectAllLayoutElement; [SerializeField] private Text deselectAllLabel; [SerializeField] private Image selectedPathBackground; [SerializeField] private Text selectedPathText; [SerializeField] private HierarchyDragDropListener dragDropListener; [SerializeField] private HierarchyField drawerPrefab; [SerializeField] private Sprite m_sceneDrawerBackground; internal Sprite SceneDrawerBackground { get { return m_sceneDrawerBackground; } } [SerializeField] private Sprite m_transformDrawerBackground; internal Sprite TransformDrawerBackground { get { return m_transformDrawerBackground; } } #pragma warning restore 0649 private static int aliveHierarchies = 0; private bool initialized = false; private readonly List drawers = new List( 32 ); private readonly List sceneData = new List( 8 ); private readonly List searchSceneData = new List( 8 ); private readonly Dictionary pseudoSceneDataLookup = new Dictionary(); private readonly List m_currentSelection = new List( 16 ); public ReadOnlyCollection CurrentSelection { get { return m_currentSelection.AsReadOnly(); } } // Stores the selected Transforms' instanceIDs. An object's instanceID can be retrieved via GetInstanceID() but it // makes an unnecessary "EnsureRunningOnMainThread()" call. Luckily, GetHashCode() also returns the instanceID and // it doesn't call "EnsureRunningOnMainThread()" private readonly HashSet currentSelectionSet = new HashSet(); private readonly HashSet newSelectionSet = new HashSet(); #pragma warning disable 0414 // Value is assigned but never used on Android & iOS private Transform multiSelectionPivotTransform; private HierarchyDataRoot multiSelectionPivotSceneData; private readonly List multiSelectionPivotSiblingIndexTraversalList = new List( 8 ); #pragma warning restore 0414 private readonly Transform[] singleTransformSelection = new Transform[1]; private int totalItemCount; internal int ItemCount { get { return totalItemCount; } } private bool selectLock = false; private bool isListViewDirty = true; private bool shouldRecalculateContentWidth; private float lastClickTime; private HierarchyField lastClickedDrawer; private HierarchyField currentlyPressedDrawer; private float pressedDrawerDraggedReferenceCreateTime; private PointerEventData pressedDrawerActivePointer; private Canvas m_canvas; public Canvas Canvas { get { return m_canvas; } } private float m_autoScrollSpeed; internal float AutoScrollSpeed { set { m_autoScrollSpeed = value; } } // Used to make sure that the scrolled content always remains within the scroll view's boundaries private PointerEventData nullPointerEventData; public SelectionChangedDelegate OnSelectionChanged; public DoubleClickDelegate OnItemDoubleClicked; private GameObjectFilterDelegate m_gameObjectDelegate; public GameObjectFilterDelegate GameObjectFilter { get { return m_gameObjectDelegate; } set { m_gameObjectDelegate = value; for( int i = 0; i < sceneData.Count; i++ ) { if( sceneData[i].IsExpanded ) { sceneData[i].IsExpanded = false; sceneData[i].IsExpanded = true; } } if( m_isInSearchMode ) { for( int i = 0; i < searchSceneData.Count; i++ ) { if( searchSceneData[i].IsExpanded ) { searchSceneData[i].IsExpanded = false; searchSceneData[i].IsExpanded = true; } } } } } int IListViewAdapter.Count { get { return totalItemCount; } } float IListViewAdapter.ItemHeight { get { return Skin.LineHeight; } } protected override void Awake() { base.Awake(); Initialize(); } private void Initialize() { if( initialized ) return; initialized = true; listView.SetAdapter( this ); aliveHierarchies++; m_canvas = GetComponentInParent(); nullPointerEventData = new PointerEventData( null ); searchInputField.onValueChanged.AddListener( OnSearchTermChanged ); deselectAllButton.onClick.AddListener( () => { DeselectInternal( null ); MultiSelectionToggleSelectionMode = false; } ); m_showHorizontalScrollbar = !m_showHorizontalScrollbar; ShowHorizontalScrollbar = !m_showHorizontalScrollbar; if( m_showTooltips ) { TooltipListener = gameObject.AddComponent(); TooltipListener.Initialize( this ); } RuntimeInspectorUtils.IgnoredTransformsInHierarchy.Add( drawArea ); #if ENABLE_INPUT_SYSTEM && !ENABLE_LEGACY_INPUT_MANAGER // On new Input System, scroll sensitivity is much higher than legacy Input system scrollView.scrollSensitivity *= 0.25f; #endif } private void Start() { SceneManager.sceneLoaded += OnSceneLoaded; SceneManager.sceneUnloaded += OnSceneUnloaded; if( ExposeUnityScenes ) { for( int i = 0; i < SceneManager.sceneCount; i++ ) OnSceneLoaded( SceneManager.GetSceneAt( i ), LoadSceneMode.Single ); } if( ExposeDontDestroyOnLoadScene ) OnSceneLoaded( GetDontDestroyOnLoadScene(), LoadSceneMode.Single ); } private void OnDestroy() { SceneManager.sceneLoaded -= OnSceneLoaded; SceneManager.sceneUnloaded -= OnSceneUnloaded; if( --aliveHierarchies == 0 ) HierarchyData.ClearPool(); RuntimeInspectorUtils.IgnoredTransformsInHierarchy.Remove( drawArea ); } private void OnRectTransformDimensionsChange() { shouldRecalculateContentWidth = true; } private void OnTransformParentChanged() { m_canvas = GetComponentInParent(); } #if UNITY_EDITOR private void OnEnable() { UnityEditor.Selection.selectionChanged -= OnEditorSelectionChanged; UnityEditor.Selection.selectionChanged += OnEditorSelectionChanged; } private void OnDisable() { UnityEditor.Selection.selectionChanged -= OnEditorSelectionChanged; } private void OnEditorSelectionChanged() { if( !syncSelectionWithEditorHierarchy ) return; Transform[] selection = UnityEditor.Selection.GetFiltered( UnityEditor.SelectionMode.ExcludePrefab ); if( selection.Length > 0 ) Select( selection ); } #endif protected override void Update() { base.Update(); float time = Time.realtimeSinceStartup; if( time > nextHierarchyRefreshTime ) Refresh(); if( m_isInSearchMode && time > nextSearchRefreshTime ) RefreshSearchResults(); if( isListViewDirty ) RefreshListView(); if( time > nextObjectNamesRefreshTime ) { nextObjectNamesRefreshTime = time + m_objectNamesRefreshInterval; for( int i = sceneData.Count - 1; i >= 0; i-- ) sceneData[i].ResetCachedNames(); for( int i = searchSceneData.Count - 1; i >= 0; i-- ) searchSceneData[i].ResetCachedNames(); for( int i = drawers.Count - 1; i >= 0; i-- ) { if( drawers[i].gameObject.activeSelf ) drawers[i].RefreshName(); } shouldRecalculateContentWidth = true; } if( m_showHorizontalScrollbar && shouldRecalculateContentWidth ) { shouldRecalculateContentWidth = false; float preferredWidth = 0f; for( int i = drawers.Count - 1; i >= 0; i-- ) { if( drawers[i].gameObject.activeSelf ) { float drawerWidth = drawers[i].PreferredWidth; if( drawerWidth > preferredWidth ) preferredWidth = drawerWidth; } } if( m_multiSelectionToggleSelectionMode && drawers.Count > 0 ) preferredWidth += Skin.LineHeight; float contentMinWidth = listView.ViewportWidth + scrollView.verticalScrollbarSpacing; if( preferredWidth > contentMinWidth ) scrollView.content.sizeDelta = new Vector2( preferredWidth - contentMinWidth, scrollView.content.sizeDelta.y ); else scrollView.content.sizeDelta = new Vector2( 0f, scrollView.content.sizeDelta.y ); EnsureScrollViewIsWithinBounds(); } if( m_pointerLongPressAction != LongPressAction.None && currentlyPressedDrawer && time > pressedDrawerDraggedReferenceCreateTime ) { if( currentlyPressedDrawer.gameObject.activeSelf && currentlyPressedDrawer.Data.BoundTransform ) { if( m_pointerLongPressAction == LongPressAction.CreateDraggedReferenceItem || ( m_pointerLongPressAction == LongPressAction.ShowMultiSelectionTogglesThenCreateDraggedReferenceItem && ( !m_allowMultiSelection || m_multiSelectionToggleSelectionMode ) ) ) { Transform[] draggedReferences = currentlyPressedDrawer.IsSelected ? m_currentSelection.ToArray() : new Transform[1] { currentlyPressedDrawer.Data.BoundTransform }; if( draggedReferences.Length > 1 ) { // The held drawer's Transform should be the first Transform in the array so that it'll be the Transform that will be received // by RuntimeInspector's object reference drawers int currentlyPressedDrawerIndex = System.Array.IndexOf( draggedReferences, currentlyPressedDrawer.Data.BoundTransform ); if( currentlyPressedDrawerIndex > 0 ) { for( int i = currentlyPressedDrawerIndex; i > 0; i-- ) draggedReferences[i] = draggedReferences[i - 1]; draggedReferences[0] = currentlyPressedDrawer.Data.BoundTransform; } } if( RuntimeInspectorUtils.CreateDraggedReferenceItem( draggedReferences, pressedDrawerActivePointer, Skin, m_canvas ) ) ( (IPointerEnterHandler) dragDropListener ).OnPointerEnter( pressedDrawerActivePointer ); } else if( m_allowMultiSelection && !m_multiSelectionToggleSelectionMode ) { if( currentSelectionSet.Add( currentlyPressedDrawer.Data.BoundTransform.GetHashCode() ) ) { m_currentSelection.Add( currentlyPressedDrawer.Data.BoundTransform ); currentlyPressedDrawer.IsSelected = true; OnCurrentSelectionChanged(); } MultiSelectionToggleSelectionMode = true; justActivatedMultiSelectionToggleSelectionMode = true; // Tooltip may accidentally appear while long pressing the drawer, hide the tooltip in that case if( TooltipListener ) TooltipListener.OnDrawerHovered( null, null, false ); } } currentlyPressedDrawer = null; pressedDrawerActivePointer = null; } if( m_autoScrollSpeed != 0f ) scrollView.verticalNormalizedPosition = Mathf.Clamp01( scrollView.verticalNormalizedPosition + m_autoScrollSpeed * Time.unscaledDeltaTime / totalItemCount ); } public void Refresh() { nextHierarchyRefreshTime = Time.realtimeSinceStartup + m_refreshInterval; bool hasChanged = false; for( int i = 0; i < sceneData.Count; i++ ) hasChanged |= sceneData[i].Refresh(); if( hasChanged ) isListViewDirty = true; else { for( int i = drawers.Count - 1; i >= 0; i-- ) { if( drawers[i].gameObject.activeSelf ) drawers[i].Refresh(); } } } private void RefreshListView() { isListViewDirty = false; totalItemCount = 0; if( !m_isInSearchMode ) { for( int i = sceneData.Count - 1; i >= 0; i-- ) totalItemCount += sceneData[i].Height; } else { for( int i = searchSceneData.Count - 1; i >= 0; i-- ) totalItemCount += searchSceneData[i].Height; } listView.UpdateList( false ); EnsureScrollViewIsWithinBounds(); } internal void SetListViewDirty() { isListViewDirty = true; } public void RefreshSearchResults() { if( !m_isInSearchMode ) return; nextSearchRefreshTime = Time.realtimeSinceStartup + m_searchRefreshInterval; for( int i = 0; i < searchSceneData.Count; i++ ) { HierarchyDataRootSearch data = (HierarchyDataRootSearch) searchSceneData[i]; bool wasExpandable = data.CanExpand; data.Refresh(); if( data.CanExpand && !wasExpandable ) data.IsExpanded = true; isListViewDirty = true; } } public void RefreshNameOf( Transform target ) { if( target ) { Scene targetScene = target.gameObject.scene; for( int i = sceneData.Count - 1; i >= 0; i-- ) { HierarchyDataRoot data = sceneData[i]; if( ( data is HierarchyDataRootPseudoScene ) || ( (HierarchyDataRootScene) data ).Scene == targetScene ) sceneData[i].RefreshNameOf( target ); } if( m_isInSearchMode ) { RefreshSearchResults(); for( int i = searchSceneData.Count - 1; i >= 0; i-- ) searchSceneData[i].RefreshNameOf( target ); } for( int i = drawers.Count - 1; i >= 0; i-- ) { if( drawers[i].gameObject.activeSelf && drawers[i].Data.BoundTransform == target ) drawers[i].RefreshName(); } shouldRecalculateContentWidth = true; } } protected override void RefreshSkin() { background.color = Skin.BackgroundColor; verticalScrollbar.color = Skin.ScrollbarColor; horizontalScrollbar.color = Skin.ScrollbarColor; searchInputField.textComponent.SetSkinInputFieldText( Skin ); searchInputFieldBackground.color = Skin.InputFieldNormalBackgroundColor.Tint( 0.08f ); searchIcon.color = Skin.ButtonTextColor; searchBarLayoutElement.SetHeight( Skin.LineHeight ); deselectAllLayoutElement.SetHeight( Skin.LineHeight ); deselectAllButton.targetGraphic.color = Skin.InputFieldInvalidBackgroundColor; deselectAllLabel.SetSkinInputFieldText( Skin ); selectedPathBackground.color = Skin.BackgroundColor.Tint( 0.1f ); selectedPathText.SetSkinButtonText( Skin ); Text placeholder = searchInputField.placeholder as Text; if( placeholder != null ) { float placeholderAlpha = placeholder.color.a; placeholder.SetSkinInputFieldText( Skin ); Color placeholderColor = placeholder.color; placeholderColor.a = placeholderAlpha; placeholder.color = placeholderColor; } LayoutRebuilder.ForceRebuildLayoutImmediate( drawArea ); listView.ResetList(); } // Makes sure that scroll view's contents are within scroll view's bounds private void EnsureScrollViewIsWithinBounds() { // When scrollbar is snapped to the very bottom of the scroll view, sometimes OnScroll alone doesn't work if( scrollView.verticalNormalizedPosition <= Mathf.Epsilon ) scrollView.verticalNormalizedPosition = 0.0001f; scrollView.OnScroll( nullPointerEventData ); } void IListViewAdapter.SetItemContent( RecycledListItem item ) { if( isListViewDirty ) RefreshListView(); HierarchyField drawer = (HierarchyField) item; HierarchyData data = GetDataAt( drawer.Position ); if( data != null ) { drawer.Skin = Skin; drawer.SetContent( data ); drawer.IsSelected = data.BoundTransform && currentSelectionSet.Contains( data.BoundTransform.GetHashCode() ); drawer.MultiSelectionToggleVisible = m_multiSelectionToggleSelectionMode; drawer.Refresh(); shouldRecalculateContentWidth = true; } } void IListViewAdapter.OnItemClicked( RecycledListItem item ) { HierarchyField drawer = (HierarchyField) item; if( OnItemDoubleClicked != null && drawer == lastClickedDrawer && Time.realtimeSinceStartup - lastClickTime <= m_doubleClickThreshold ) { lastClickTime = 0f; OnItemDoubleClicked( lastClickedDrawer.Data ); } else { lastClickTime = Time.realtimeSinceStartup; lastClickedDrawer = drawer; bool hasSelectionChanged = false; Transform clickedTransform = drawer.Data.BoundTransform; int clickedTransformInstanceID = clickedTransform ? clickedTransform.GetHashCode() : -1; #if UNITY_EDITOR || UNITY_STANDALONE || UNITY_WEBGL || UNITY_WSA || UNITY_WSA_10_0 // When Shift key is held, all items from the pivot item to the clicked item will be selected int multiSelectionPivotIndex; #if ENABLE_INPUT_SYSTEM && !ENABLE_LEGACY_INPUT_MANAGER if( m_allowMultiSelection && FindMultiSelectionPivotAbsoluteIndex( out multiSelectionPivotIndex ) && Keyboard.current != null && Keyboard.current.shiftKey.isPressed ) #else if( m_allowMultiSelection && FindMultiSelectionPivotAbsoluteIndex( out multiSelectionPivotIndex ) && ( Input.GetKey( KeyCode.LeftShift ) || Input.GetKey( KeyCode.RightShift ) ) ) #endif { newSelectionSet.Clear(); if( clickedTransform ) { newSelectionSet.Add( clickedTransformInstanceID ); if( currentSelectionSet.Add( clickedTransformInstanceID ) ) { m_currentSelection.Add( clickedTransform ); hasSelectionChanged = true; } } // Add new Transforms to selection for( int i = multiSelectionPivotIndex, endIndex = drawer.Position, increment = ( multiSelectionPivotIndex < endIndex ) ? 1 : -1; i != endIndex; i += increment ) { Transform newSelection = GetDataAt( i ).BoundTransform; if( !newSelection ) continue; int selectionInstanceID = newSelection.GetHashCode(); newSelectionSet.Add( selectionInstanceID ); if( currentSelectionSet.Add( selectionInstanceID ) ) { m_currentSelection.Add( newSelection ); hasSelectionChanged = true; } } // Remove old Transforms from selection for( int i = m_currentSelection.Count - 1; i >= 0; i-- ) { Transform oldSelection = m_currentSelection[i]; if( !oldSelection ) continue; int selectionInstanceID = oldSelection.GetHashCode(); if( !newSelectionSet.Contains( selectionInstanceID ) ) { m_currentSelection.RemoveAt( i ); currentSelectionSet.Remove( selectionInstanceID ); hasSelectionChanged = true; } } } else #endif { multiSelectionPivotTransform = clickedTransform; multiSelectionPivotSceneData = drawer.Data.Root; drawer.Data.GetSiblingIndexTraversalList( multiSelectionPivotSiblingIndexTraversalList ); // When in toggle selection mode or Control key is held, individual items can be multi-selected #if UNITY_EDITOR || UNITY_STANDALONE || UNITY_WEBGL || UNITY_WSA || UNITY_WSA_10_0 #if ENABLE_INPUT_SYSTEM && !ENABLE_LEGACY_INPUT_MANAGER if( m_allowMultiSelection && ( m_multiSelectionToggleSelectionMode || ( Keyboard.current != null && Keyboard.current.ctrlKey.isPressed ) ) ) #else if( m_allowMultiSelection && ( m_multiSelectionToggleSelectionMode || Input.GetKey( KeyCode.LeftControl ) || Input.GetKey( KeyCode.RightControl ) ) ) #endif #else if( m_allowMultiSelection && m_multiSelectionToggleSelectionMode ) #endif { if( clickedTransform ) { if( currentSelectionSet.Add( clickedTransformInstanceID ) ) m_currentSelection.Add( clickedTransform ); else { m_currentSelection.Remove( clickedTransform ); currentSelectionSet.Remove( clickedTransformInstanceID ); if( m_currentSelection.Count == 0 ) MultiSelectionToggleSelectionMode = false; } hasSelectionChanged = true; } } else { if( clickedTransform ) { if( m_currentSelection.Count != 1 || m_currentSelection[0] != clickedTransform ) { m_currentSelection.Clear(); currentSelectionSet.Clear(); m_currentSelection.Add( clickedTransform ); currentSelectionSet.Add( clickedTransformInstanceID ); hasSelectionChanged = true; } } else if( m_currentSelection.Count > 0 ) { m_currentSelection.Clear(); currentSelectionSet.Clear(); hasSelectionChanged = true; } } } if( hasSelectionChanged ) { for( int i = drawers.Count - 1; i >= 0; i-- ) { if( drawers[i].gameObject.activeSelf ) { Transform drawerTransform = drawers[i].Data.BoundTransform; if( drawerTransform ) { if( drawers[i].IsSelected != currentSelectionSet.Contains( drawerTransform.GetHashCode() ) ) drawers[i].IsSelected = !drawers[i].IsSelected; } else if( drawers[i].IsSelected ) drawers[i].IsSelected = false; } } OnCurrentSelectionChanged(); } if( m_isInSearchMode ) { bool shouldShowSearchPathText = false; for( int i = m_currentSelection.Count - 1; i >= 0; i-- ) { Transform selection = m_currentSelection[i]; if( selection ) { // Fetch the object's path and show it in Hierarchy System.Text.StringBuilder sb = RuntimeInspectorUtils.stringBuilder; sb.Length = 0; sb.AppendLine( "Path:" ); while( selection ) { sb.Append( " " ).AppendLine( selection.name ); selection = selection.parent; } selectedPathText.text = sb.Append( " " ).Append( drawer.Data.Root.Name ).ToString(); shouldShowSearchPathText = true; break; } } if( selectedPathBackground.gameObject.activeSelf != shouldShowSearchPathText ) selectedPathBackground.gameObject.SetActive( shouldShowSearchPathText ); } } } private bool FindMultiSelectionPivotAbsoluteIndex( out int pivotAbsoluteIndex ) { pivotAbsoluteIndex = 0; if( multiSelectionPivotSceneData == null ) return false; bool pivotSceneDataExists = false; List sceneData = m_isInSearchMode ? searchSceneData : this.sceneData; for( int i = 0; i < sceneData.Count; i++ ) { if( sceneData[i] != multiSelectionPivotSceneData ) pivotAbsoluteIndex += sceneData[i].Height; else { pivotSceneDataExists = true; break; } } if( !pivotSceneDataExists ) return false; if( multiSelectionPivotSiblingIndexTraversalList.Count == 0 ) return true; if( !multiSelectionPivotTransform ) return false; // To find the pivot Transform, first try traversing its recorded sibling indices in the HierarchyDataRoot that contains it HierarchyData multiSelectionPivotData = multiSelectionPivotSceneData.TraverseSiblingIndexList( multiSelectionPivotSiblingIndexTraversalList ); if( multiSelectionPivotData != null && multiSelectionPivotData.BoundTransform == multiSelectionPivotTransform ) { pivotAbsoluteIndex += multiSelectionPivotData.AbsoluteIndex; return true; } // Either HierarchyDataRoot no longer contains the pivot Transform, or the pivot Transform's sibling index have changed. Try finding // the pivot Transform among all visible children of the HierarchyDataRoot (if it's a pseudo-scene in which a Transform can appear // more than once, try finding the pivot Transform at the same depth as it was before) multiSelectionPivotData = multiSelectionPivotSceneData.FindTransformInVisibleChildren( multiSelectionPivotTransform, ( multiSelectionPivotSceneData is HierarchyDataRootPseudoScene ) ? multiSelectionPivotSiblingIndexTraversalList.Count : -1 ); if( multiSelectionPivotData != null ) { pivotAbsoluteIndex += multiSelectionPivotData.AbsoluteIndex; return true; } // Try finding the pivot Transform at all among all visible children of the pseudo-scene if( multiSelectionPivotSceneData is HierarchyDataRootPseudoScene ) { multiSelectionPivotData = multiSelectionPivotSceneData.FindTransformInVisibleChildren( multiSelectionPivotTransform, -1 ); if( multiSelectionPivotData != null ) { pivotAbsoluteIndex += multiSelectionPivotData.AbsoluteIndex; return true; } } return false; } internal HierarchyData GetDataAt( int index ) { List rootData = !m_isInSearchMode ? sceneData : searchSceneData; for( int i = 0; i < rootData.Count; i++ ) { if( rootData[i].Depth < 0 ) continue; if( index < rootData[i].Height ) return index > 0 ? rootData[i].FindDataAtIndex( index - 1 ) : rootData[i]; else index -= rootData[i].Height; } return null; } public void OnDrawerPointerEvent( HierarchyField drawer, PointerEventData eventData, bool isPointerDown ) { if( !isPointerDown ) { currentlyPressedDrawer = null; pressedDrawerActivePointer = null; // We have activated MultiSelectionToggleSelectionMode with this press; processing the click would result in // deselecting the Transform that we've just selected with this press because its selected state would be toggled // again inside OnItemClicked. Simply ignore the click to avoid this issue if( justActivatedMultiSelectionToggleSelectionMode ) { justActivatedMultiSelectionToggleSelectionMode = false; eventData.eligibleForClick = false; } #if ENABLE_INPUT_SYSTEM && !ENABLE_LEGACY_INPUT_MANAGER // On new Input System, DraggedReferenceItems aren't tracked by the PointerEventDatas that initiated them. However, when a DraggedReferenceItem is // created by holding a HierarchyField, the PointerEventData's dragged object will be set as the RuntimeHierarchy's ScrollRect. When it happens, // trying to scroll the RuntimeHierarchy by holding the DraggedReferenceItem at top/bottom edge of the ScrollRect doesn't work because scrollbar's // value is overwritten by the original PointerEventData. We can prevent this issue by stopping original PointerEventData's drag operation here if( eventData.dragging && eventData.pointerDrag == scrollView.gameObject && DraggedReferenceItem.InstanceItem ) { eventData.dragging = false; eventData.pointerDrag = null; } #endif } else if( m_pointerLongPressAction != LongPressAction.None ) { currentlyPressedDrawer = drawer; pressedDrawerActivePointer = eventData; pressedDrawerDraggedReferenceCreateTime = Time.realtimeSinceStartup + m_pointerLongPressDuration; } } public bool Select( Transform selection, SelectOptions selectOptions = SelectOptions.None ) { singleTransformSelection[0] = selection; return Select( singleTransformSelection, selectOptions ); } public bool Select( IList selection, SelectOptions selectOptions = SelectOptions.None ) { return !m_isLocked && SelectInternal( selection, selectOptions ); } internal bool SelectInternal( IList selection, SelectOptions selectOptions = SelectOptions.None ) { if( selectLock ) return false; if( selection.IsEmpty() ) { DeselectInternal( null ); return true; } Initialize(); bool additive = ( selectOptions & SelectOptions.Additive ) == SelectOptions.Additive; if( !m_allowMultiSelection ) { additive = false; if( selection.Count > 1 ) { // Assertion: At least one Transform isn't null because "selection.IsEmpty()" was false for( int i = selection.Count - 1; i >= 0; i-- ) { if( CanSelectTransform( selection[i] ) ) { singleTransformSelection[0] = selection[i]; selection = singleTransformSelection; break; } } } } bool hasSelectionChanged = false; if( additive ) { for( int i = 0; i < selection.Count; i++ ) { Transform _selection = selection[i]; if( CanSelectTransform( _selection ) && currentSelectionSet.Add( _selection.GetHashCode() ) ) { m_currentSelection.Add( _selection ); hasSelectionChanged = true; } } } else { newSelectionSet.Clear(); // Add new Transforms to selection for( int i = 0; i < selection.Count; i++ ) { Transform newSelection = selection[i]; if( !CanSelectTransform( newSelection ) ) continue; int selectionInstanceID = newSelection.GetHashCode(); newSelectionSet.Add( selectionInstanceID ); if( currentSelectionSet.Add( selectionInstanceID ) ) { m_currentSelection.Add( newSelection ); hasSelectionChanged = true; } } // Remove old Transforms from selection for( int i = m_currentSelection.Count - 1; i >= 0; i-- ) { Transform oldSelection = m_currentSelection[i]; if( !oldSelection ) continue; int selectionInstanceID = oldSelection.GetHashCode(); if( !newSelectionSet.Contains( selectionInstanceID ) ) { m_currentSelection.RemoveAt( i ); currentSelectionSet.Remove( selectionInstanceID ); hasSelectionChanged = true; } } } if( !hasSelectionChanged && ( ( selectOptions & SelectOptions.ForceRevealSelection ) != SelectOptions.ForceRevealSelection ) ) return true; if( hasSelectionChanged ) OnCurrentSelectionChanged(); // Make sure that the contents of the hierarchy are up-to-date Refresh(); RefreshSearchResults(); HierarchyDataTransform itemToFocus = null; int itemToFocusSceneDataIndex = 0; // Expand the selected Transforms' parents if they are currently collapsed List sceneData = m_isInSearchMode ? searchSceneData : this.sceneData; for( int i = 0; i < m_currentSelection.Count; i++ ) { Transform _selection = m_currentSelection[i]; if( !_selection ) continue; Scene selectionScene = _selection.gameObject.scene; for( int j = 0; j < sceneData.Count; j++ ) { HierarchyDataRoot data = sceneData[j]; if( m_isInSearchMode || ( data is HierarchyDataRootPseudoScene ) || ( (HierarchyDataRootScene) data ).Scene == selectionScene ) { HierarchyDataTransform selectionItem = data.FindTransform( _selection ); if( selectionItem != null ) { itemToFocus = selectionItem; itemToFocusSceneDataIndex = j; // Transform may exist in multiple places (zero or one Unity scene and zero or more pseudo-scene(s)). After expanding the Transform's parent in // one of these scenes, we can call it a day. This way, we'd use less CPU-cycles but the Transform's parent in other scene(s) may not be expanded. // If performance is important and expanding the Transform's parent in a single scene is OK, then you can uncomment the line below //break; } } } } RefreshListView(); if( itemToFocus != null ) { // Focus on the latest selected HierarchyItem if( ( selectOptions & SelectOptions.FocusOnSelection ) == SelectOptions.FocusOnSelection ) { int itemIndex = itemToFocus.AbsoluteIndex; for( int i = 0; i < itemToFocusSceneDataIndex; i++ ) itemIndex += sceneData[i].Height; // This isn't just the drawArea but rather the RuntimeHierarchy itself because in search mode, when SelectedSearchItemPath is visible, // it will decrease the scroll view's height which may result in an incorrect viewportHeight value. This is especially obvious // when calling Select immediately after exiting the search mode LayoutRebuilder.ForceRebuildLayoutImmediate( (RectTransform) transform ); // Credit: https://gist.github.com/yasirkula/75ca350fb83ddcc1558d33a8ecf1483f float drawAreaHeight = drawArea.rect.height; float viewportHeight = ( (RectTransform) drawArea.parent ).rect.height; if( drawAreaHeight > viewportHeight ) { float focusPoint = ( (float) itemIndex / totalItemCount ) * drawAreaHeight + Skin.LineHeight * 0.5f; scrollView.verticalNormalizedPosition = 1f - Mathf.Clamp01( ( focusPoint - viewportHeight * 0.5f ) / ( drawAreaHeight - viewportHeight ) ); } } // When itemToFocus isn't null, it also means that we were successfully able to expand the selection's parents in the hierarchy return true; } return false; } public void Deselect() { Deselect( (IList) null ); } public void Deselect( Transform deselection ) { singleTransformSelection[0] = deselection; Deselect( singleTransformSelection ); } public void Deselect( IList deselection ) { if( !m_isLocked ) DeselectInternal( deselection ); } internal void DeselectInternal( IList deselection ) { if( selectLock || m_currentSelection.Count == 0 ) return; Initialize(); bool hasSelectionChanged = false; if( deselection == null ) { m_currentSelection.Clear(); currentSelectionSet.Clear(); hasSelectionChanged = true; } else { for( int i = deselection.Count - 1; i >= 0; i-- ) { Transform deselect = deselection[i]; if( deselect && currentSelectionSet.Remove( deselect.GetHashCode() ) ) { m_currentSelection.Remove( deselect ); hasSelectionChanged = true; } } } if( hasSelectionChanged ) { for( int i = drawers.Count - 1; i >= 0; i-- ) { if( drawers[i].gameObject.activeSelf && drawers[i].IsSelected ) drawers[i].IsSelected = false; } OnCurrentSelectionChanged(); } } public bool IsSelected( Transform transform ) { return transform && currentSelectionSet.Contains( transform.GetHashCode() ); } // Check if Transform is hidden from this RuntimeHierarchy private bool CanSelectTransform( Transform transform ) { if( !transform ) return false; if( RuntimeInspectorUtils.IgnoredTransformsInHierarchy.Contains( transform ) || ( m_gameObjectDelegate != null && !m_gameObjectDelegate( transform ) ) ) return false; // When Transform passes the above checks, it doesn't necessarily mean that it is visible since one of its parents might also be hidden from this RuntimeHierarchy. // We aren't just checking if all parents of the Transform are visible; imagine the scenario where there exist an A/B/C Transform hierarchy in which A is hidden from // this RuntimeHierarchy and we're checking if C is visible. Logically, when A is hidden, its grandchild C would also be hidden implicitly. However, if there is a // pseudo-scene with either B or C as a root object, then C would be visible in that pseudo-scene because neither B nor C are explicitly hidden from this RuntimeHierarchy Transform nearestRoot = null; for( int i = 0; i < sceneData.Count; i++ ) { Transform parent = sceneData[i].GetNearestRootOf( transform ); if( parent && ( !nearestRoot || parent.IsChildOf( nearestRoot ) ) ) nearestRoot = parent; } if( !nearestRoot ) return false; if( nearestRoot != transform ) { // Check if B is explicitly hidden from this RuntimeHierarchy in the A/B/C example above for( Transform _transform = transform.parent; _transform && _transform != nearestRoot; _transform = _transform.parent ) { if( RuntimeInspectorUtils.IgnoredTransformsInHierarchy.Contains( _transform ) || ( m_gameObjectDelegate != null && !m_gameObjectDelegate( _transform ) ) ) return false; } } return true; } private void OnCurrentSelectionChanged() { selectLock = true; try { #if UNITY_EDITOR if( syncSelectionWithEditorHierarchy ) { List selection = new List( m_currentSelection.Count ); for( int i = 0; i < m_currentSelection.Count; i++ ) { if( m_currentSelection[i] ) selection.Add( m_currentSelection[i].gameObject ); } UnityEditor.Selection.objects = selection.ToArray(); } #endif if( m_connectedInspector ) { for( int i = m_currentSelection.Count - 1; i >= 0; i-- ) { if( m_currentSelection[i] ) { m_connectedInspector.Inspect( m_currentSelection[i].gameObject ); break; } } } if( OnSelectionChanged != null ) OnSelectionChanged( m_currentSelection.AsReadOnly() ); } catch( System.Exception e ) { Debug.LogException( e ); } finally { selectLock = false; } } private void OnSearchTermChanged( string search ) { if( search != null ) search = search.Trim(); if( string.IsNullOrEmpty( search ) ) { if( m_isInSearchMode ) { for( int i = 0; i < searchSceneData.Count; i++ ) searchSceneData[i].IsExpanded = false; scrollView.verticalNormalizedPosition = 1f; selectedPathBackground.gameObject.SetActive( false ); isListViewDirty = true; m_isInSearchMode = false; // Focus on currently selected object(s) after exiting search mode if( m_currentSelection.Count > 0 ) SelectInternal( m_currentSelection, SelectOptions.FocusOnSelection | SelectOptions.ForceRevealSelection ); } } else { if( !m_isInSearchMode ) { scrollView.verticalNormalizedPosition = 1f; nextSearchRefreshTime = Time.realtimeSinceStartup + m_searchRefreshInterval; isListViewDirty = true; m_isInSearchMode = true; RefreshSearchResults(); for( int i = 0; i < searchSceneData.Count; i++ ) searchSceneData[i].IsExpanded = true; } else RefreshSearchResults(); } } private void OnSceneLoaded( Scene arg0, LoadSceneMode arg1 ) { if( !ExposeUnityScenes || ( arg0.buildIndex >= 0 && exposedUnityScenesSubset != null && exposedUnityScenesSubset.Length > 0 && System.Array.IndexOf( exposedUnityScenesSubset, arg0.name ) == -1 ) ) return; if( !arg0.IsValid() ) return; for( int i = 0; i < sceneData.Count; i++ ) { if( sceneData[i] is HierarchyDataRootScene && ( (HierarchyDataRootScene) sceneData[i] ).Scene == arg0 ) return; } HierarchyDataRootScene data = new HierarchyDataRootScene( this, arg0 ); data.Refresh(); // Unity scenes should come before pseudo-scenes int index = sceneData.Count - pseudoSceneDataLookup.Count; sceneData.Insert( index, data ); searchSceneData.Insert( index, new HierarchyDataRootSearch( this, data ) ); isListViewDirty = true; } private void OnSceneUnloaded( Scene arg0 ) { for( int i = 0; i < sceneData.Count; i++ ) { if( sceneData[i] is HierarchyDataRootScene && ( (HierarchyDataRootScene) sceneData[i] ).Scene == arg0 ) { sceneData[i].IsExpanded = false; sceneData.RemoveAt( i ); searchSceneData[i].IsExpanded = false; searchSceneData.RemoveAt( i ); isListViewDirty = true; return; } } } private Scene GetDontDestroyOnLoadScene() { GameObject temp = null; try { temp = new GameObject(); DontDestroyOnLoad( temp ); Scene dontDestroyOnLoad = temp.scene; DestroyImmediate( temp ); temp = null; return dontDestroyOnLoad; } catch( System.Exception e ) { Debug.LogException( e ); return new Scene(); } finally { if( temp != null ) DestroyImmediate( temp ); } } public void AddToPseudoScene( string scene, Transform transform ) { GetPseudoScene( scene, true ).AddChild( transform ); } public void AddToPseudoScene( string scene, IEnumerable transforms ) { HierarchyDataRootPseudoScene pseudoScene = GetPseudoScene( scene, true ); foreach( Transform transform in transforms ) pseudoScene.AddChild( transform ); } public void RemoveFromPseudoScene( string scene, Transform transform, bool deleteSceneIfEmpty ) { HierarchyDataRootPseudoScene pseudoScene = GetPseudoScene( scene, false ); if( pseudoScene == null ) return; pseudoScene.RemoveChild( transform ); if( deleteSceneIfEmpty && pseudoScene.ChildCount == 0 ) DeletePseudoScene( scene ); } public void RemoveFromPseudoScene( string scene, IEnumerable transforms, bool deleteSceneIfEmpty ) { HierarchyDataRootPseudoScene pseudoScene = GetPseudoScene( scene, false ); if( pseudoScene == null ) return; foreach( Transform transform in transforms ) pseudoScene.RemoveChild( transform ); if( deleteSceneIfEmpty && pseudoScene.ChildCount == 0 ) DeletePseudoScene( scene ); } private HierarchyDataRootPseudoScene GetPseudoScene( string scene, bool createIfNotExists ) { HierarchyDataRootPseudoScene data; if( pseudoSceneDataLookup.TryGetValue( scene, out data ) ) return data; if( createIfNotExists ) return CreatePseudoSceneInternal( scene ); return null; } public void CreatePseudoScene( string scene ) { if( pseudoSceneDataLookup.ContainsKey( scene ) ) return; CreatePseudoSceneInternal( scene ); } private HierarchyDataRootPseudoScene CreatePseudoSceneInternal( string scene ) { int index = 0; if( pseudoScenesOrder.Length > 0 ) { for( int i = 0; i < pseudoScenesOrder.Length; i++ ) { if( pseudoScenesOrder[i] == scene ) break; if( pseudoSceneDataLookup.ContainsKey( pseudoScenesOrder[i] ) ) index++; } } else index = pseudoSceneDataLookup.Count; HierarchyDataRootPseudoScene data = new HierarchyDataRootPseudoScene( this, scene ); // Pseudo-scenes should come after Unity scenes index += sceneData.Count - pseudoSceneDataLookup.Count; sceneData.Insert( index, data ); searchSceneData.Insert( index, new HierarchyDataRootSearch( this, data ) ); pseudoSceneDataLookup[scene] = data; isListViewDirty = true; return data; } public void DeleteAllPseudoScenes() { for( int i = sceneData.Count - 1; i >= 0; i-- ) { if( sceneData[i] is HierarchyDataRootPseudoScene ) { sceneData[i].IsExpanded = false; sceneData.RemoveAt( i ); searchSceneData[i].IsExpanded = false; searchSceneData.RemoveAt( i ); } } pseudoSceneDataLookup.Clear(); isListViewDirty = true; } public void DeletePseudoScene( string scene ) { for( int i = 0; i < sceneData.Count; i++ ) { HierarchyDataRootPseudoScene pseudoScene = sceneData[i] as HierarchyDataRootPseudoScene; if( pseudoScene != null && pseudoScene.Name == scene ) { pseudoSceneDataLookup.Remove( pseudoScene.Name ); sceneData[i].IsExpanded = false; sceneData.RemoveAt( i ); searchSceneData[i].IsExpanded = false; searchSceneData.RemoveAt( i ); isListViewDirty = true; return; } } } RecycledListItem IListViewAdapter.CreateItem( Transform parent ) { HierarchyField result = (HierarchyField) Instantiate( drawerPrefab, parent, false ); result.Initialize( this ); result.Skin = Skin; drawers.Add( result ); return result; } } }