-
-
Save yasirkula/06edc780beaa4d8705b3564d60886fa6 to your computer and use it in GitHub Desktop.
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 static MethodInfo screenFittedRectGetter; | |
private static FieldInfo editorWindowHostViewGetter; | |
private static PropertyInfo hostViewContainerWindowGetter; | |
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 ) ); | |
Rect rect = new Rect(GUIUtility.GUIToScreenPoint(Event.current.mousePosition) - new Vector2(0f, position.height), position.size); // Show dropdown above the cursor instead of below the cursor | |
position = GetScreenFittedRect(rect, this); | |
} | |
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; | |
wantsLessLayoutEvents = false; | |
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() | |
{ | |
SceneView.duringSceneGui += ( SceneView sceneView ) => | |
{ | |
/// 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 ); | |
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 | |
{ | |
#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( SceneVisibilityManager.instance.IsHidden( rectTransform.gameObject, false ) ) | |
return false; | |
if( SceneVisibilityManager.instance.IsPickingDisabled( rectTransform.gameObject, false ) ) | |
return false; | |
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; | |
} | |
/// <summary> | |
/// Restricts the given Rect within the screen's bounds. | |
/// </summary> | |
private static Rect GetScreenFittedRect(Rect originalRect, EditorWindow editorWindow) | |
{ | |
screenFittedRectGetter ??= typeof(EditorWindow).Assembly.GetType("UnityEditor.ContainerWindow").GetMethod("FitRectToScreen", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static); | |
if (screenFittedRectGetter.GetParameters().Length == 3) | |
return (Rect)screenFittedRectGetter.Invoke(null, new object[] { originalRect, true, true }); | |
else | |
{ | |
// New version introduced in Unity 2022.3.62f1, Unity 6.0.49f1 and Unity 6.1.0f1. | |
// Usage example: https://github.com/Unity-Technologies/UnityCsReference/blob/10f8718268a7e34844ba7d59792117c28d75a99b/Editor/Mono/EditorWindow.cs#L1264 | |
editorWindowHostViewGetter ??= typeof(EditorWindow).GetField("m_Parent", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); | |
hostViewContainerWindowGetter ??= typeof(EditorWindow).Assembly.GetType("UnityEditor.HostView").GetProperty("window", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); | |
return (Rect)screenFittedRectGetter.Invoke(null, new object[] { originalRect, originalRect.center, true, hostViewContainerWindowGetter.GetValue(editorWindowHostViewGetter.GetValue(editorWindow), null) }); | |
} | |
} | |
} |
@yasirkula Thank you for the tool, it is awesome. But it throws exception in Unity 6.1. It seems they changed the signature for UnityEditor.ContainerWindow.FitRectToScreen
. Can you please update the code?
I think the method call should look something like this:
position = (Rect) screenFittedRectGetter.Invoke(null,
new object[4]
{
new Rect(
GUIUtility.GUIToScreenPoint(Event.current.mousePosition)
- new Vector2(0f, position.height), position.size),
Event.current.mousePosition,
true,
mouseOverWindow
});
At least that has fixed error for me.
@MarkZaytsev Thank you for bringing this to my attention. It affects some of my other plugins as well. I've tried resolving it by calling FitRectToScreen the same way EditorWindow does: https://github.com/Unity-Technologies/UnityCsReference/blob/10f8718268a7e34844ba7d59792117c28d75a99b/Editor/Mono/EditorWindow.cs#L1264
@MarkZaytsev Thank you for bringing this to my attention. It affects some of my other plugins as well. I've tried resolving it by calling FitRectToScreen the same way EditorWindow does: https://github.com/Unity-Technologies/UnityCsReference/blob/10f8718268a7e34844ba7d59792117c28d75a99b/Editor/Mono/EditorWindow.cs#L1264
Great, thanks for the quick update)
To select only visible you could add
if(rectTransform.GetComponents<Graphic>().Any(e => e.enabled))
{
Entry entry = new(rectTransform);
result.Add(entry);
result = entry.Children;
}
In this fuction
private static void CheckRectTransformRecursive
(
RectTransform rectTransform,
Vector2 pointerPos,
Camera camera,
bool culledByCanvasGroup,
List<Entry> result
)
{
if(RectTransformUtility.RectangleContainsScreenPoint
(rectTransform, pointerPos, camera)
&& ShouldCheckRectTransform
(
rectTransform,
pointerPos,
camera,
ref culledByCanvasGroup
))
{
// Show Only Visible
if(rectTransform.GetComponents<Graphic>().Any(e => e.enabled))
{
Entry entry = new(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
);
}
}
@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.