Skip to content

Instantly share code, notes, and snippets.

@yasirkula
Last active April 2, 2024 23:43
Show Gist options
  • Star 26 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save yasirkula/06edc780beaa4d8705b3564d60886fa6 to your computer and use it in GitHub Desktop.
Save yasirkula/06edc780beaa4d8705b3564d60886fa6 to your computer and use it in GitHub Desktop.
Select the UI object under the cursor via right click in Unity's Scene window
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text;
using UnityEditor;
using UnityEngine;
using UnityEngine.UI;
#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
public class SceneViewUIObjectPickerContextWindow : EditorWindow
{
private struct Entry
{
public readonly RectTransform RectTransform;
public readonly List<Entry> Children;
public Entry( RectTransform rectTransform )
{
RectTransform = rectTransform;
Children = new List<Entry>( 2 );
}
}
private readonly List<RectTransform> uiObjects = new List<RectTransform>( 16 );
private readonly List<string> uiObjectLabels = new List<string>( 16 );
private static RectTransform hoveredUIObject;
private static readonly Vector3[] hoveredUIObjectCorners = new Vector3[4];
private static readonly List<ICanvasRaycastFilter> raycastFilters = new List<ICanvasRaycastFilter>( 4 );
private static double lastRightClickTime;
private static Vector2 lastRightPos;
private static bool blockSceneViewInput;
private readonly MethodInfo screenFittedRectGetter = typeof( EditorWindow ).Assembly.GetType( "UnityEditor.ContainerWindow" ).GetMethod( "FitRectToScreen", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static );
private const float Padding = 1f;
private float RowHeight { get { return EditorGUIUtility.singleLineHeight; } }
private GUIStyle RowGUIStyle { get { return "MenuItem"; } }
private void ShowContextWindow( List<Entry> results )
{
StringBuilder sb = new StringBuilder( 100 );
InitializeUIObjectsRecursive( results, 0, sb );
GUIStyle rowGUIStyle = RowGUIStyle;
float preferredWidth = 0f;
foreach( string label in uiObjectLabels )
preferredWidth = Mathf.Max( preferredWidth, rowGUIStyle.CalcSize( new GUIContent( label ) ).x );
ShowAsDropDown( new Rect(), new Vector2( preferredWidth + Padding * 2f, uiObjects.Count * RowHeight + Padding * 2f ) );
// Show dropdown above the cursor instead of below the cursor
position = (Rect) screenFittedRectGetter.Invoke( null, new object[3] { new Rect( GUIUtility.GUIToScreenPoint( Event.current.mousePosition ) - new Vector2( 0f, position.height ), position.size ), true, true } );
}
private void InitializeUIObjectsRecursive( List<Entry> results, int depth, StringBuilder sb )
{
foreach( Entry entry in results )
{
sb.Length = 0;
uiObjects.Add( entry.RectTransform );
uiObjectLabels.Add( sb.Append( ' ', depth * 4 ).Append( entry.RectTransform.name ).ToString() );
if( entry.Children.Count > 0 )
InitializeUIObjectsRecursive( entry.Children, depth + 1, sb );
}
}
protected void OnEnable()
{
wantsMouseMove = wantsMouseEnterLeaveWindow = true;
#if UNITY_2020_1_OR_NEWER
wantsLessLayoutEvents = false;
#endif
blockSceneViewInput = true;
}
protected void OnDisable()
{
hoveredUIObject = null;
SceneView.RepaintAll();
}
protected void OnGUI()
{
Event ev = Event.current;
float rowWidth = position.width - Padding * 2f, rowHeight = RowHeight;
GUIStyle rowGUIStyle = RowGUIStyle;
int hoveredRowIndex = -1;
for( int i = 0; i < uiObjects.Count; i++ )
{
Rect rect = new Rect( Padding, Padding + i * rowHeight, rowWidth, rowHeight );
if( GUI.Button( rect, uiObjectLabels[i], rowGUIStyle ) )
{
if( uiObjects[i] != null )
Selection.activeTransform = uiObjects[i];
blockSceneViewInput = false;
ev.Use();
Close();
GUIUtility.ExitGUI();
}
if( hoveredRowIndex < 0 && ev.type == EventType.MouseMove && rect.Contains( ev.mousePosition ) )
hoveredRowIndex = i;
}
if( ev.type == EventType.MouseMove || ev.type == EventType.MouseLeaveWindow )
{
RectTransform hoveredUIObject = ( hoveredRowIndex >= 0 ) ? uiObjects[hoveredRowIndex] : null;
if( hoveredUIObject != SceneViewUIObjectPickerContextWindow.hoveredUIObject )
{
SceneViewUIObjectPickerContextWindow.hoveredUIObject = hoveredUIObject;
Repaint();
SceneView.RepaintAll();
}
}
}
[InitializeOnLoadMethod]
private static void OnSceneViewGUI()
{
#if UNITY_2019_1_OR_NEWER
SceneView.duringSceneGui += ( SceneView sceneView ) =>
#else
SceneView.onSceneGUIDelegate += ( SceneView sceneView ) =>
#endif
{
/// Couldn't get <see cref="EventType.ContextClick"/> to work here in Unity 5.6 so implemented context click detection manually
Event ev = Event.current;
switch( ev.type )
{
case EventType.MouseDown:
{
if( ev.button == 1 )
{
lastRightClickTime = EditorApplication.timeSinceStartup;
lastRightPos = ev.mousePosition;
}
else if( blockSceneViewInput )
{
// User has clicked outside the context window to close it. Ignore this click in Scene view if it's left click
blockSceneViewInput = false;
if( ev.button == 0 )
{
GUIUtility.hotControl = 0;
ev.Use();
}
}
break;
}
case EventType.MouseUp:
{
if( ev.button == 1 && EditorApplication.timeSinceStartup - lastRightClickTime < 0.2 && ( ev.mousePosition - lastRightPos ).magnitude < 2f )
OnSceneViewRightClicked( sceneView );
break;
}
}
if( hoveredUIObject != null )
{
hoveredUIObject.GetWorldCorners( hoveredUIObjectCorners );
Handles.DrawSolidRectangleWithOutline( hoveredUIObjectCorners, new Color( 1f, 1f, 0f, 0.25f ), Color.black );
}
};
}
private static void OnSceneViewRightClicked( SceneView sceneView )
{
// Find all UI objects under the cursor
Vector2 pointerPos = HandleUtility.GUIPointToScreenPixelCoordinate( Event.current.mousePosition );
Entry rootEntry = new Entry( null );
#if UNITY_2018_3_OR_NEWER
PrefabStage prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
if( prefabStage != null && prefabStage.stageHandle.IsValid() && prefabStage.prefabContentsRoot.transform is RectTransform prefabStageRoot )
CheckRectTransformRecursive( prefabStageRoot, pointerPos, sceneView.camera, false, rootEntry.Children );
else
#endif
{
#if UNITY_2022_3_OR_NEWER
Canvas[] canvases = FindObjectsByType<Canvas>( FindObjectsSortMode.None );
#else
Canvas[] canvases = FindObjectsOfType<Canvas>();
#endif
Array.Sort( canvases, ( c1, c2 ) => c1.sortingOrder.CompareTo( c2.sortingOrder ) );
foreach( Canvas canvas in canvases )
{
if( canvas != null && canvas.gameObject.activeInHierarchy && canvas.isRootCanvas )
CheckRectTransformRecursive( (RectTransform) canvas.transform, pointerPos, sceneView.camera, false, rootEntry.Children );
}
}
// Remove non-Graphic root entries with no children from the results
rootEntry.Children.RemoveAll( ( canvasEntry ) => canvasEntry.Children.Count == 0 && !canvasEntry.RectTransform.GetComponent<Graphic>() );
// If any results found, show the context window
if( rootEntry.Children.Count > 0 )
CreateInstance<SceneViewUIObjectPickerContextWindow>().ShowContextWindow( rootEntry.Children );
}
private static void CheckRectTransformRecursive( RectTransform rectTransform, Vector2 pointerPos, Camera camera, bool culledByCanvasGroup, List<Entry> result )
{
Canvas canvas = rectTransform.GetComponent<Canvas>();
if( canvas != null && !canvas.enabled )
return;
if ( RectTransformUtility.RectangleContainsScreenPoint( rectTransform, pointerPos, camera ) && ShouldCheckRectTransform( rectTransform, pointerPos, camera, ref culledByCanvasGroup ) )
{
Entry entry = new Entry( rectTransform );
result.Add( entry );
result = entry.Children;
}
for( int i = 0, childCount = rectTransform.childCount; i < childCount; i++ )
{
RectTransform childRectTransform = rectTransform.GetChild( i ) as RectTransform;
if( childRectTransform != null && childRectTransform.gameObject.activeSelf )
CheckRectTransformRecursive( childRectTransform, pointerPos, camera, culledByCanvasGroup, result );
}
}
private static bool ShouldCheckRectTransform( RectTransform rectTransform, Vector2 pointerPos, Camera camera, ref bool culledByCanvasGroup )
{
#if UNITY_2019_3_OR_NEWER
if( SceneVisibilityManager.instance.IsHidden( rectTransform.gameObject, false ) )
return false;
if( SceneVisibilityManager.instance.IsPickingDisabled( rectTransform.gameObject, false ) )
return false;
#endif
CanvasRenderer canvasRenderer = rectTransform.GetComponent<CanvasRenderer>();
if( canvasRenderer != null && canvasRenderer.cull )
return false;
CanvasGroup canvasGroup = rectTransform.GetComponent<CanvasGroup>();
if( canvasGroup != null )
{
if( canvasGroup.ignoreParentGroups )
culledByCanvasGroup = canvasGroup.alpha == 0f;
else if( canvasGroup.alpha == 0f )
culledByCanvasGroup = true;
}
if( !culledByCanvasGroup )
{
// If the target is a MaskableGraphic that ignores masks (i.e. visible outside masks) and isn't fully transparent, accept it
MaskableGraphic maskableGraphic = rectTransform.GetComponent<MaskableGraphic>();
if( maskableGraphic != null && !maskableGraphic.maskable && maskableGraphic.color.a > 0f )
return true;
raycastFilters.Clear();
rectTransform.GetComponentsInParent( false, raycastFilters );
foreach( var raycastFilter in raycastFilters )
{
if( !raycastFilter.IsRaycastLocationValid( pointerPos, camera ) )
return false;
}
}
return !culledByCanvasGroup;
}
}
@yasirkula
Copy link
Author

How To

Simply create a folder called Editor inside your Project window and add this script inside it. Then, right click anywhere in Scene view to show a context menu displaying the UI objects under the cursor.

Demo

@MadMacMad
Copy link

thank you!!
what a nice xmas gift from you...! ❤️
works Wonderful on my Mac :-)
keep it up my friend...
your tools are one reason why I'll still use Unity...
if there is a better alternative I'll would switch...
because Unity doesn't care much about us Mac user anymore
I'm user since 2006 ... when it was Mac only -> good times for us... ;-)
and now you see what all those PC Nerd's had done to the company...

@yasirkula
Copy link
Author

@MadMacMad Thank you for your kind words ^_^ Actually most developers in our company also use Mac and I haven't heard major Unity issues from them. The errors you're encountering may be universal and I hope that they get resolved by Unity 🍀

@shastr
Copy link

shastr commented Dec 5, 2023

seems interesting.. thanks for your work!

@redflowerleadprogrammer
Copy link

It works great! There is a great lack of exactly the same functionality, only for highlighting ordinary objects on the scene!!
How can you do the same thing, just by highlighting the usual objects of the scene? We have a lot of objects in our project and are already tired of the inaccuracy of selecting Unity objects! When I click on an object, the desired object is highlighted only starting from 3-5 times.

@yasirkula
Copy link
Author

@HollyRivay I think, for a Collider-free solution, I'd iterate over all Renderers in the scene and raycast against their localBounds (you can get a Ray via HandleUtility.GUIPointToWorldRay(Event.current.position)). Note that you'd have to convert the ray from world space to local space since localBounds is also in local space. To highlight an object, I'd either draw a transparent cube via Handles class or figure out a way to draw a transparent unlit mesh via other functions Unity provide.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment