Why are new GameObjects added as children from context menu but not from main menu?

I am looking at the C# source code since hours and I simply do not understand how it works.

Line 1370 in “SceneHierarchy.cs” in “CreateGameObjectContextClick()” says:

// Set the context of each MenuItem to the current selection, so the created gameobjects will be added as children
// Sets includeCreateEmptyChild to false, since that item is superfluous here (the normal "Create Empty" is added as a child anyway)

While Line 951 in “GameObjectCreateDropdownButton()” says:

// The context should be null, just like it is in the main menu. Case 1185434.

The only real difference between the below execution of “AddCreateGameObjectItemsToMenu()” is the context parameter. But it goes nowhere. It’s not used. The parent object is never set, the MenuEntries are not given the info of the selected gameobject (target parent).

I really just want to understand where the magic happens.

Ok actually I’d like to execute a menu item and parent the created GameObject to the current Selection, just from a custom Editor Tool and not with the convoluted right click menu.

In case you didn’t know: At the current state (without custom code) it’s not possible to create a TextMeshProUI-Object (or really anything except an Empty) as child of the current Selection with a shortcut.

1 Like

I would also like to understand this so I can do the same.

The magic is in the (internal) EditorApplication.ExecuteMenuItemWithTemporaryContext, which gets passed the initial context containing the selected game objects. The flow is: CreateGameObjectContextClick calls AddCreateGameObjectItemsToMenu with the selected game objects as context, which passes the context to MenuUtils.ExtractOnlyEnabledMenuItem. The context gets set on MenuCallbackObject and then passed to MenuUtils.MenuCallback when the menu item is executed, where finally ExecuteMenuItemWithTemporaryContext is called.

But most of the magic is happening in Unity’s native side. The menu items are created there, retrieved with the native call Menu.GetMenuItems and finally executed again on the native side with EditorApplication.ExecuteMenuItemWithTemporaryContext. There’s little you can do besides triggering the stock menu items with the proper context.

And this is all Unity internals, which might change without warning. Since this is pretty basic behaviour, you’re better of replicating it using public APIs.

Actually, I realized that the MenuItem documentation does provide the way to do this, with GameObjectUtility.SetParentAndAlign and Undo.RegisterCreatedObjectUndo.

If I would write my own menu items, this would work. But I just want to trigger the existing ones - and when doing so, I don’t get a reference to the created object.

Maybe I could call FindObjectsOfType before and after the execution to find the object that was created and then parent it accordingly. Feels kinda hacky tho.

The created object is being selected. You should be able to get it from Selection.activeGameObject right after executing the menu item and before anything else is able to change the selection again?

This might work, I still have to change if the object was created using the MenuItem Command. The user could have a GameObject selected and then using a menu item like “Select Children” or “Invert Selection”, my Code would then assume that a new Object was created using the command and reparent the new selection to the old selection.

I just really hope to see the Searchable Context Menu (that’s listed on their roadmap), as they will have to solve the exact same case, just with access to internal C# and C++ code.

Also I found out that even with reflection over all loaded assemblys, not all MenuItems will show up. “Assets/Create/Material” never appears in this list, no matter if I use C# Reflection or Unitys TypeCache. I wonder where the corresponding MenuItem is hiding.

The only thing that really lists all Menu Items is EditorGUIUtility.SerializeMainMenuToString(); which isn’t documented. Also it lists a few strange entries and doesn’t contain shortcut symbols.

Damn, I spent a lot of time to use Reflection to trigger Shortcuts from Code - now that everything is working, I’m realizing that when using shortcuts, the selection in the hierarchy is ignored as it is the same as executing the MenuItem in the MainMenu.

But if anyone is interrested in a blender like search over all available shortcuts, well there you go:

Tested in 2022.2.16, just put the script in an Editor-Folder, wait for the recompile, press spacebar and search for anything you want, done.

9005458--1241059--upload_2023-5-10_21-29-9.png

using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using UnityEditor;
using UnityEditor.Experimental.GraphView;
using UnityEditor.ShortcutManagement;
using UnityEngine;
using Object = UnityEngine.Object;

namespace JL
{
    public class QuickHotbar
    {
        [MenuItem("Tools/Misc/QuickHotbar")]
        [Shortcut("QuickHotbar", KeyCode.Space)]
        static void OpenSearch()
        {
            Provider providerInstance = ScriptableObject.CreateInstance<Provider>();
            providerInstance.focusedWindow = EditorWindow.focusedWindow;
            Rect mainWindowRect = EditorGUIUtility.GetMainWindowPosition();
            float wantedWidth = 500;
            float wantedHeight = 300;
            Vector2 initPos = Vector2.zero;
            initPos.x = mainWindowRect.x + mainWindowRect.width / 2;
            initPos.y = mainWindowRect.y + mainWindowRect.height / 2 - wantedHeight / 2;
            SearchWindowContext context = new SearchWindowContext(
                   initPos, wantedWidth, wantedHeight);
            SearchWindow.Open<Provider>(context, providerInstance);
            Object.DestroyImmediate(providerInstance);
        }
        class Provider : ScriptableObject, ISearchWindowProvider
        {
            public EditorWindow focusedWindow;
            public List<SearchTreeEntry> CreateSearchTree(SearchWindowContext context)
            {
                List<SearchTreeEntry> list = new()
                {
                    new SearchTreeGroupEntry(new GUIContent("Commands List"), 0)
                };
                string lastCategory = "";
                List<Entry> entries = GetAll();
                for (int i = 0; i < entries.Count; i++)
                {
                    if (!entries[i].name.Contains('/'))
                    {
                        Entry entry = entries[i];
                        entry.name = "Unsorted/" + entries[i].name;
                        entries[i] = entry;
                    }
                }
                entries.Sort((x, y) => x.name.CompareTo(y.name));
                foreach (Entry entry in entries)
                {
                    string category = entry.name.Split('/')[0];
                    if (lastCategory != category)
                    {
                        lastCategory = category;
                        list.Add(new SearchTreeGroupEntry(new GUIContent(category), 1));
                    }
                    string fullName = entry.name + " " + entry.keyCombinationString;
                    list.Add(new SearchTreeEntry(new GUIContent(fullName))
                    {
                        level = 2,
                        userData = entry.action,
                    });
                }
                return list;
            }
            public bool OnSelectEntry(SearchTreeEntry SearchTreeEntry, SearchWindowContext context)
            {
                (SearchTreeEntry.userData as Action).Invoke();
                return true;
            }
            struct Entry
            {
                public string name;
                public Action action;
                public string keyCombinationString;
            }
            List<Entry> GetAll()
            {
                List<Entry> entries = new();
                BindingFlags flags = Toolbox.AllBindingFlags();
                IShortcutManager manager = ShortcutManager.instance;
                FieldInfo prop = manager.GetType().GetField("m_ShortcutProfileManager", flags);
                object shortcutProfileManager = prop.GetValue(manager);
                MethodInfo getAllShortcutsMethod = shortcutProfileManager.GetType().GetMethod("GetAllShortcuts");
                object shortcutEntries = getAllShortcutsMethod.Invoke(shortcutProfileManager, null);
                foreach (object shortcutEntry in shortcutEntries as IEnumerable)
                {
                    ShortcutEntryRef shortcutEntryRef = new(shortcutEntry);
                    Action wrappedAction = () =>
                    {
                        shortcutEntryRef.action.Invoke(new ShortcutArguments()
                        {
                            context = focusedWindow,
                            stage = ShortcutStage.End,
                        });
                    };
                    string keyCombinationString = "";
                    if (shortcutEntryRef.keyCombinations.Count > 0)
                    {
                        keyCombinationString = "(";
                        foreach (KeyCombination keyCombination in shortcutEntryRef.keyCombinations)
                        {
                            keyCombinationString += keyCombination.ToString();
                        }
                        keyCombinationString += ")";
                    }
                    Entry entry = new Entry()
                    {
                        name = shortcutEntryRef.displayName,
                        action = wrappedAction,
                        keyCombinationString = keyCombinationString,
                    };
                    entries.Add(entry);
                }
                return entries;
            }
            struct ShortcutEntryRef
            {
                object _shortcutEntry;
                PropertyInfo _displayNameProp;
                public string displayName
                {
                    get
                    {
                        return _displayNameProp.GetValue(_shortcutEntry) as string;
                    }
                }
                PropertyInfo _actionProp;
                public Action<ShortcutArguments> action
                {
                    get
                    {
                        return _actionProp.GetValue(_shortcutEntry) as Action<ShortcutArguments>;
                    }
                }
                PropertyInfo _keyCombinationsProp;
                public IList<KeyCombination> keyCombinations
                {
                    get
                    {
                        return _keyCombinationsProp.GetValue(_shortcutEntry) as IList<KeyCombination>;
                    }
                }
                public ShortcutEntryRef(object shortcutEntry)
                {
                    Type entryType = shortcutEntry.GetType();
                    _shortcutEntry = shortcutEntry;
                    _displayNameProp = entryType.GetProperty("displayName");
                    _actionProp = entryType.GetProperty("action");
                    _keyCombinationsProp = entryType.GetProperty("combinations");
                }
            }
        }
    }
}