Skip to content

Instantly share code, notes, and snippets.

@Qubus0
Last active September 8, 2023 12:40
Show Gist options
  • Save Qubus0/87179587d2cfc0ceec780c75d87f891d to your computer and use it in GitHub Desktop.
Save Qubus0/87179587d2cfc0ceec780c75d87f891d to your computer and use it in GitHub Desktop.
Godot Plugins: adding context actions to the file system dock (3.5)

Adding context actions to the file system dock in a plugin

Note: This mini tutorial is for Godot 3.5. There are some changes that need to be made for Godot 4, check them out in this other gist

Since there is no built-in method to add context actions, we have to make our own.

To start, we need to get the file system dock. This can be done in the EditorPlugin. If we need it in any other script, we can just pass it to them.

tool
extends EditorPlugin

func _enter_tree() -> void:
    var file_system := get_editor_interface().get_file_system_dock()
    connect_file_system_context_actions(file_system)

There are two panels in the file system dock: a file Tree and an ItemList of files. Both of these have their own context menu popup. If you can't see the ItemList, you have to toggle the small icon button with two parallel bars at the top of the FileSystem dock.

The context menus are dynamic, so they are cleared every time they pop up. It won't be enough to get them once and add our menu items, we have to do it every time.

This method is used to get the two context menus and connect their about_to_show methods.

func connect_file_system_context_actions(file_system_dock: FileSystemDock) -> void:
    # There are two file systems: one is a Tree, the second is an ItemList.
    # Both need to be handled. Toggle the button with two bars at the top to see both
    var file_tree: Tree
    var file_list: ItemList
    for node in file_system_dock.get_children():
        # Only the parent of the file tree and file list is a VSplit
        if node is VSplitContainer:
            file_tree = node.get_child(0)
            file_list = node.get_child(1).get_child(1)
            break

    for node in file_system_dock.get_children():
        var context_menu: PopupMenu = node as PopupMenu
        if not context_menu: continue

        # The order of the menus isn't always the same, but we can use their
        # signal connections to find out if they belong to the list or the tree
        var signals: Array = context_menu.get_signal_connection_list("id_pressed")
        if not signals.empty():
            match signals[0]["method"]:
                "_tree_rmb_option":
                    context_menu.connect("about_to_show", self, "_on_file_tree_context_actions_about_to_show", [context_menu, file_tree])
                "_file_list_rmb_option":
                    context_menu.connect("about_to_show", self, "_on_file_list_context_actions_about_to_show", [context_menu, file_list])

These two functions are what we connect them to. Since we want the same context menu for bot the list and the tree, but have to get the file path information in a different way for each, we use these to unify the way that the data is passed on.

We take the file path of the item that is selected and pass it on to the next method.

func _on_file_tree_context_actions_about_to_show(context_menu: PopupMenu, tree: Tree) -> void:
    var selected := tree.get_next_selected(null)
    if not selected:		# Empty space was clicked
        add_custom_context_actions(context_menu, "")
        return

    # The file system tree only has one column
    var file_path := selected.get_metadata(0) as String
    printt(selected, file_path)
    add_custom_context_actions(context_menu, file_path)


    # If you need multiselection you can modify add_custom_context_actions to take an array
    var file_paths := []
    while selected: 		
        file_paths.append(selected.get_metadata(0))
        selected = tree.get_next_selected(selected)


func _on_file_list_context_actions_about_to_show(context_menu: PopupMenu, list: ItemList) -> void:
    if not list.get_selected_items().size() > 0:		# Empty space was clicked
        add_custom_context_actions(context_menu, "")
        return

    var item_index = list.get_selected_items()[0]

    var selected := list.items[item_index] as Control
    var file_path := list.get_item_metadata(item_index) as String
    printt(selected, file_path)
    add_custom_context_actions(context_menu, file_path)

This is the unified method that will build our context menu. We can get all the context information we need from the file path, as shown below.

Note that an item can be focused at the same time that the empty space was clicked - if you want to avoid this, just return after adding your menu items there.

# Called every time the file system context actions pop up
# Since they are dynamic, they are cleared every time and need to be refilled
func add_custom_context_actions(context_menu: PopupMenu, file_path: String) -> void:
    var dir := Directory.new()
    context_menu.add_separator()

    if file_path == "":
        context_menu.add_item("Clicked empty space")
        # return # ignore the focused item if empty space was clicked

    if dir.dir_exists(file_path):
        print(file_path)
        context_menu.add_item("A directory is selected")

    if dir.file_exists(file_path):
        context_menu.add_item("A file is selected")

    if "res://addons" in file_path:
        context_menu.add_item("Something in the addons directory is selected")

And that's it! If you have more questions, feel free to ask.

If you want to support me, you can

Buy Me A Coffee
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment