Search Unity

OnHierarchyGUI: Possible to draw default Hierarchy?

Discussion in 'Immediate Mode GUI (IMGUI)' started by FuguFirecracker, Dec 13, 2017.

  1. FuguFirecracker

    FuguFirecracker

    Joined:
    Sep 20, 2011
    Posts:
    419
    Hi Hi
    Is it possible to draw the base Hierarchy Context Menu without having to recreate the entirety of it in a custom GenericMenu ? Undocumented DrawDefault command? Some Reflection wizardry, perhaps?


    Code (CSharp):
    1.  
    2. public class CustomContext
    3. {
    4. public static GenericMenu MyBitchinMenu = new GenericMenu();
    5.  
    6.  [InitializeOnLoadMethod]
    7.     static void StartInitializeOnLoadMethod()
    8.     {
    9.         EditorApplication.hierarchyWindowItemOnGUI += OnHierarchyGUI;
    10.     }
    11.  
    12.     static void OnHierarchyGUI(int instanceID, Rect selectionRect)
    13.     {
    14.         var e = Event.current;
    15.  
    16.         if (e.type == EventType.ContextClick)
    17.         {
    18.  
    19. // Want to draw default Hierarchy Context Menu Here. ###############
    20.  
    21.             MyBitchinMenu.AddItem(new GUIContent("Menu Item 1"), false,
    22.             () => { Debug.Log("Bitchery-Do"); });
    23.             MyBitchinMenu.ShowAsContext();
    24.             e.Use();
    25.         }
    26. }
    27.  
    Can do, or must I recreate it using EditorApplication.ExecuteMenuItem ?
    Any ideas?
     
    sandolkakos likes this.
  2. Xarbrough

    Xarbrough

    Joined:
    Dec 11, 2014
    Posts:
    1,188
    I don't think there's an undocumented DrawDefault command and I don't know about the ExecuteMenuItem because if it exists, you probably can't add your own items.

    You do know about the MenuItem class and that you can add custom items to the right-click context menu already?

    If you have some special reason to handle more logic yourself, you can indeed use reflection. Maybe something like this to create the default context menu from the hierarchy:

    Code (CSharp):
    1. if (e.type == EventType.ContextClick)
    2. {
    3.     // Populate my custom menu with the default hierarchy context menu by finding this internal method:
    4.     // private void CreateGameObjectContextClick(GenericMenu menu, int contextClickedItemID)
    5.  
    6.     System.Type hierarchyWindowType = Assembly.GetAssembly(typeof(Editor)).GetType("UnityEditor.SceneHierarchyWindow");
    7.     MethodInfo createContextMenuMethod = hierarchyWindowType.GetMethod("CreateGameObjectContextClick", BindingFlags.NonPublic | BindingFlags.Instance);
    8.     Object hierarchyWindow = Resources.FindObjectsOfTypeAll(hierarchyWindowType).FirstOrDefault();
    9.     createContextMenuMethod.Invoke(hierarchyWindow, new object[] { MyMenu, 0 });
    10.  
    11.     // Add items after create to add to the end or move in front of the create call.
    12.     MyMenu.AddItem(new GUIContent("My Menu Item 1"), false,
    13.         () => { Debug.Log("My menu item was clicked"); });
    14.  
    15.     MyMenu.ShowAsContext();
    16.     e.Use();
    17. }
    You probably should create the custom menu on right click, not cache it (Unity does so with all menus I've seen so far). Also, I would add some null checks and error messages, if sometime in the future the internal behavior changes and your code breaks. Next, it looks like the hierarchy window handles right-clicking on an existing item differently from clicking the empty space area. Maybe you need to handle this as well, but then you should start looking at the source code.

    To inspect the Unity editor code yourself, you can just google for it (there's at least one GitHub repo which hosts it) or you can decompile the UnityEditor.dll with a tool like ILSpy or even look at it with MonoBehaviour.

    Post more information about what you are doing exactly if you need more help with reflection or if there is an easier solution to your concrete problem. ;)
     
    CanisLupus and FuguFirecracker like this.
  3. FuguFirecracker

    FuguFirecracker

    Joined:
    Sep 20, 2011
    Posts:
    419
    Hey! Thanks for taking the time for a thorough reply.

    What I was looking to accomplish was to inject my own set of commands into the right-click Hierarchy Context Menu without overwriting the existing 'out-of-the-box' context menu.

    The only way I could discover to add items to the Hierarchy Context Menu [lets call it HCM from now on --- it's a chore to write out;...] was to pony on the back of the GameObjects menu a la : "GameObject/MyStuff/HeresACommand"

    That works well until one realizes that that the custom menu command will fire for every GameObject Selected in the Hierarchy... but only for Top level prefabs and not for the child objects selected. That MIGHT be desired and acceptable behaviour... or not....

    So I was figuring that I'd have to rewrite the whole thing so that it LOOKS like the out-of -the-box HCM, but with a custom section just for me.

    Until I realized... There's an 'ALT' button that is not in use.

    So I just put my custom menu in ALT-Right-Click.

    And here is the template I've come up with :

    Code (CSharp):
    1. using UnityEditor;
    2. using UnityEngine;
    3.  
    4. namespace FuguFirecracker
    5. {
    6.     [InitializeOnLoad]
    7.     public class AltContext
    8.     {
    9.         public static GenericMenu MyMenu = new GenericMenu();
    10.  
    11.         private static readonly GUIContent Item1 = new GUIContent("This is Item 1");
    12.         private static readonly GUIContent Item2 = new GUIContent("This is Item 2");
    13.         private static readonly GUIContent Item3 = new GUIContent("This is Item 3");
    14.         private static readonly GUIContent Item4 = new GUIContent("This is Item 4");
    15.    
    16.  
    17.         static AltContext()
    18.         {
    19.             EditorApplication.hierarchyWindowItemOnGUI += OnHierarchyGUI;
    20.  
    21.             MyMenu.AddItem(Item1, false, () => DoThis());
    22.             MyMenu.AddItem(Item2, false, () => DoThat());
    23.             MyMenu.AddItem(Item3, false, () => DoTheOtherThing());
    24.             MyMenu.AddItem(Item4, false, () => DoSomethingElseEntirely());
    25.         }
    26.  
    27.         private static void OnHierarchyGUI(int instanceid, Rect selectionrect)
    28.         {
    29.             var e = Event.current;
    30.             if (e.type == EventType.ContextClick && e.alt && Selection.activeTransform)
    31.             {
    32.                  MyMenu.ShowAsContext();
    33.                   e.Use();
    34.              }
    35.  
    36.         }
    37.     }
    38.  
    39. }
    Really? I've been building my menu in a static constructor. It seemed odd to me to recreate it in every OnHierarchyGUI call. I know... IMMEDIATE mode... But I just let the UI call handle the events.

    Can handle that with a Selection.activeTransform null check

    If you have any further insights, I'm keen to hear :)
     
  4. FuguFirecracker

    FuguFirecracker

    Joined:
    Sep 20, 2011
    Posts:
    419
    Oh! I forgot to say... The reflection code does work well, Thank you very much :)
    Gonna spend some more time with this and see which way I'll go...
     
  5. Xarbrough

    Xarbrough

    Joined:
    Dec 11, 2014
    Posts:
    1,188
    Yea you can create the menu once and recycle it if you want, but with my posted reflection code I noticed, that we need to make sure we only add our custom code once or clear the menu everytime we use. Then I saw that Unity creates the GenericMenu when the ContextClick event occurs, which seems like a clean solution because we don't have to think about any left-over state.

    No further insight so far, but maybe you can find something helpful in the editor source code. :)
     
  6. FuguFirecracker

    FuguFirecracker

    Joined:
    Sep 20, 2011
    Posts:
    419
    Yessir, building it all in the ContextClick event is the way to go.
    I too was experiencing odd remnants from caching it.
     
  7. CanisLupus

    CanisLupus

    Joined:
    Jul 29, 2013
    Posts:
    427
    Big thanks to @Xarbrough for the reflection code!

    In more recent Unity versions @Xarbrough's code does not work anymore because CreateGameObjectContextClick was moved to the SceneHierarchy class inside of SceneHierarchyWindow (I looked at the Unity code reference to find it again).

    Since I needed this to work on recent and older Unity versions I made code that successively tries each location I know of (the last 3), starting with the most recent. Might be useful to someone.

    NOTE: This code WILL break whenever the location changes again!

    Code (CSharp):
    1. public static bool ShowHierarchyContextMenu()
    2. {
    3.     // We need to use reflection to be able to call the context menu used for GameObjects in the hierarchy.
    4.     // Working as of Unity 2018.3. Please assume that this process WILL break in any future Unity version.
    5.  
    6.     // In newer Unity versions, we want to call this method:
    7.     //        void CreateGameObjectContextClick(GenericMenu menu, int contextClickedItemID)
    8.     // It's an instance method of the class SceneHierarchy. The SceneHierarchy object we want lives in SceneHierarchyWindow.
    9.  
    10.     System.Type hierarchyWindowType;
    11.     Object[] hierarchyWindows;
    12.  
    13.     try {
    14.         hierarchyWindowType = Assembly.GetAssembly(typeof(Editor)).GetType("UnityEditor.SceneHierarchyWindow");
    15.         hierarchyWindows = Resources.FindObjectsOfTypeAll(hierarchyWindowType);
    16.  
    17.         if (hierarchyWindows == null || hierarchyWindows.Length == 0) {
    18.             return false;
    19.         }
    20.     } catch {
    21.         return false;
    22.     }
    23.  
    24.     try {
    25.         PropertyInfo sceneHierarchyProperty = hierarchyWindowType.GetProperty("sceneHierarchy", BindingFlags.Public | BindingFlags.Instance);
    26.         object sceneHierarchy = sceneHierarchyProperty.GetValue(hierarchyWindows[0], new object[] { });
    27.  
    28.         System.Type sceneHierarchyType = Assembly.GetAssembly(typeof(Editor)).GetType("UnityEditor.SceneHierarchy");
    29.         MethodInfo createContextMenuMethod = sceneHierarchyType.GetMethod("CreateGameObjectContextClick", BindingFlags.NonPublic | BindingFlags.Instance);
    30.  
    31.         var menu = new GenericMenu();
    32.         createContextMenuMethod.Invoke(sceneHierarchy, new object[] { menu, 0 });
    33.         menu.ShowAsContext();
    34.  
    35.         return true;
    36.     } catch {
    37.         /* */
    38.     }
    39.  
    40.     // In some older-but-not-too-old Unity versions, the method lived directly inside the SceneHierarchyWindow object.
    41.  
    42.     try {
    43.         MethodInfo createContextMenuMethod = hierarchyWindowType.GetMethod("CreateGameObjectContextClick", BindingFlags.NonPublic | BindingFlags.Instance);
    44.  
    45.         var menu = new GenericMenu();
    46.         createContextMenuMethod.Invoke(hierarchyWindows[0], new object[] { menu, 0 });
    47.         menu.ShowAsContext();
    48.  
    49.         return true;
    50.     } catch {
    51.         /* */
    52.     }
    53.  
    54.     // For even older Unity versions, we want this method:
    55.     //        private void HandleContextClick()
    56.     // It lives inside the SceneHierarchyWindow object.
    57.  
    58.     try {
    59.         MethodInfo createContextMenuMethod = hierarchyWindowType.GetMethod("HandleContextClick", BindingFlags.NonPublic | BindingFlags.Instance);
    60.  
    61.         // We're already doing reflection, so how much dirtier can this get...?
    62.         // HACK: HandleContextClick does nothing if the current event is not ContextClick, so we force it before the call.
    63.         EventType backupEventType = Event.current.type;
    64.         Event.current.type = EventType.ContextClick;
    65.         createContextMenuMethod.Invoke(hierarchyWindows[0], new object[] {});
    66.         Event.current.type = backupEventType;
    67.  
    68.         return true;
    69.     } catch {
    70.         /* */
    71.     }
    72.  
    73.     return false;
    74. }
     
    rboerdijk and FuguFirecracker like this.