Skip to content

Instantly share code, notes, and snippets.

@lfod1997
Last active December 8, 2025 09:25
Show Gist options
  • Select an option

  • Save lfod1997/6e4ad613637d7e8411c1a603999b41d8 to your computer and use it in GitHub Desktop.

Select an option

Save lfod1997/6e4ad613637d7e8411c1a603999b41d8 to your computer and use it in GitHub Desktop.
Godot: Create & Use `PopupMenu`s
## Builds menu from given [param items].
## [br][url=https://gist.github.com/lfod1997/6e4ad613637d7e8411c1a603999b41d8]Source and usage[/url].
static func build_menu(
menu_id: StringName, items: Array, handler: Callable,
out_menus: Dictionary[StringName, PopupMenu], out_item_locator: Dictionary[StringName, Dictionary],
separator_id: int = 10000
) -> PopupMenu:
var menu := PopupMenu.new()
var locator: Dictionary[int, int] # Converts ID to index in the built `PopupMenu`
for it in items:
match it.size():
0: # []
menu.add_separator("", separator_id)
1: # [Separator Label]
menu.add_separator(it[0], separator_id)
3: # [ID, Label, Enabled by default]
menu.add_item(it[1], it[0])
menu.set_item_disabled(menu.item_count - 1, not it[2])
locator[it[0]] = menu.item_count - 1
5: # [ID, Label, Enabled by default, Submenu Name, [Submenu Items]]
menu.add_submenu_node_item(it[1], _build_menu(it[3], it[4], handler, out_menus, out_item_locator, separator_id), it[0])
menu.set_item_disabled(menu.item_count - 1, not it[2])
locator[it[0]] = menu.item_count - 1
_: # Invalid
printerr("Could not parse menu item: %s" % str(it))
menu.id_pressed.connect(handler)
out_menus[menu_id] = menu
out_item_locator[menu_id] = locator
return menu
# In your class that manages popup menus:
# These hold references to built menus
var _menus: Dictionary[StringName, PopupMenu]
var _menu_item_locator: Dictionary[StringName, Dictionary]
func _enter_tree() -> void:
# Build the main context menu
add_child(build_menu(
&"main_menu", # You later access the built `PopupMenu` through: `_menus[&"main_menu"]`
[
[1, "Edit", false], # To add a normal menu item, use format: [ID, Label, Enabled by default]
[2, "Copy Name", false],
[], # To add a separator without text, use empty array: []
[101, "Group Selected", false],
# To add a submenu, use format: [ID, Label, Enabled by default, Submenu Name, [Submenu Items]]
[100, "Create", true,
&"main_menu_add", # You later access the built sub-`PopupMenu` through: `_menus[&"main_menu_add"]`
[
[111, "New Dependency Group...", false],
[112, "New Path Pattern...", false],
[113, "New Proxy...", false],
]
],
],
_execute_routine, # "Primary handler" of all menu items, see below
_menus, # All built `PopupMenu` are inserted into this
_menu_item_locator # Converts an item's ID (that you define in the above array) to its index in the built `PopupMenu` that contains it
))
# Build the menu to show on file drop
add_child(build_menu(
&"drag_menu",
[
["Import"], # To add a separator with text, use format: [Label]
[1001, "As File Group", false],
[1002, "As Path Pattern", false],
[1003, "Dependency Group from this", false],
],
_execute_routine, _menus, _menu_item_locator # Same as in the previous call
))
# This function handles signal `PopupMenu.id_pressed`, which emits when an item in the popup menu is selected (and the menu hides)
func _execute_routine(id: int) -> void:
print("MENU SELECT: %d" % id)
match id: # `id` equals what you've defined in the menu items array: [ This number!! -> ID, Label, Enabled by default ]
1: # [1, "Edit", false]
print("The user wants to edit the selected file")
2: # [2, "Copy Name", false]
print("The user wants to copy name of the selected file")
101: # [101, "Group Selected", false]
print("The user wants to group selected files")
#TODO: ^^^ Make cases & call your code above ^^^
_:
print("Routine %d is invalid or not yet implemented" % id)
# This function (that YOU should somehow call!) shows the main context menu
func _show_main_menu(pos: Vector2) -> void:
# Before showing, update related items' availability
# Let's say the menu item "Edit" should be DISABLED if more than one file is selected
_menus[&"main_menu"].set_item_disabled( # Godot API that sets: if item at the given index (not ID!) is disabled (true) or not (false)
_menu_item_locator[&"main_menu"][1], # ID 1 is "Edit", this expr means: find index of item "Edit" in the `&"main_menu"` menu!
_selected_files.size() != 1 # ykwim
)
# ...and "Group Selected" should only be ENABLED when more than one file is selected
_menus[&"main_menu"].set_item_disabled(
_menu_item_locator[&"main_menu"][101], # ID 101 is "Group Selected"
_selected_files.size() == 1
)
_menus[&"main_menu"].position = pos + Vector2(get_window().position) # Should popup at a screen space position (in pixels)
_menus[&"main_menu"].popup() # Shows the popup
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment