How to add submenu items to MenuButton before calling _ready()?

Godot Version

v4.3

Question

For context, I’m making a simple windows style Notepad app and would like to add a dropdown menu that contains a submenu with its own selectable options. When hovering over a submenu item in the main dropdown menu, a connected panel should appear that contains its own list of menu items. For example:

I’ve settled on a MenuButton for the main menu panel because it seems to require much less manual setup than a PopupMenu to behave like the above screenshot. However, from the limited research I have done it seems impossible to add a submenu to a MenuButton through the inspector. Simply adding a PopupMenu child to the main MenuButton does not create a submenu item. You must add it through code, like this:

extends MenuButton

func _ready() -> void:
	var mainMenu = get_popup()
	var submenu = PopupMenu.new()
	submenu.add_radio_check_item("System")
	submenu.add_radio_check_item("User data")
	submenu.add_radio_check_item("Notepad")
	submenu.set_item_checked(-1, true)
	mainMenu.add_child(submenu)
	mainMenu.add_submenu_node_item("Directory", submenu)

Unfortunately, it also seems like you cannot insert a submenu item between existing MenuButton items defined in the inspector. The submenu items are always appended. This means that after I add my first submenu to the Menubutton, I must define every single item after it manually in code if I want to get the order right. Do this for all of the MenuButtons I want, and I end up with a very bloated _ready() function.

So, with all of that out of the way, is there a way that I can set up my MenuButtons and submenus before calling the _ready() function? I’d like to just preset everything through the inspector, but that seems impossible. It seems silly to me that my program needs to rebuild my menus at runtime over and over instead of just referencing a hardcoded data set. Is there any way I can reduce the clutter in my _ready() function and remove those unnecessary calculations?

You can change the item index with PopupMenu.set_item_index() to re-order the menu items as you need. You can also change its id with PopupMenu.set_item_id() if needed.

But, you can still create the submenus in the editor and use PopupMenu.set_item_submenu_node() in code to attach them where you need them. Example:

extends MenuButton


const EXPORT_ID = 2

@onready var export_menu: PopupMenu = %ExportMenu


func _ready() -> void:
	var menu = get_popup()
	var idx = menu.get_item_index(EXPORT_ID)
	export_menu.reparent(menu)
	menu.set_item_submenu_node(idx, export_menu)

Awesome, thanks for the tips! I feel very silly for missing the set_item_submenu_node() function in the docs. Your code works great, although I found a similar code segment that seems to do the same thing (names changed for my readability):

extends MenuButton

@onready var submenu: PopupMenu = $SubmenuNode


func _ready() -> void:
	var mainMenu = get_popup()
	submenu.reparent(mainMenu)
	mainMenu.set_item_submenu_node(2, submenu)

If you don’t mind me asking, what was the purpose of the original var idx = menu.get_item_index(EXPORT_ID) line, and why did you use a const for the submenu item index? Does that improve runtime performance?

Also, I found that your export_menu.reparent(menu) line was needed to create the submenu item even if I had set the PopupMenu as a child of the MenuButton in the scene tree. Why is that?

Finally, I would like to connect the index_pressed signal from the MenuButton to my main script. It looks like I have to do this in the _ready() function with MenuButton.id_pressed.connect(receiverMethod) even though I can use the inspector for my PopupMenus. Is there a way around this? Thank you for all your help.

It was just an example in case you didn’t know which index the item you want to set the submenu to has. If you know the index then you can use it directly.

It’s needed because the PopupMenu needs another PopupMenu as a parent and the MenuButton is not a PopupMenu. The MenuButton creates a PopupMenu internally so we need to re-parent it to that internal PopupMenu node.

Same as above, MenuButton isn’t a PopupMenu so you can’t access the specific PopupMenu signals from the editor.