diff --git a/doc/classes/EditorContextMenuPlugin.xml b/doc/classes/EditorContextMenuPlugin.xml
new file mode 100644
index 00000000000..7eeee3d7fd7
--- /dev/null
+++ b/doc/classes/EditorContextMenuPlugin.xml
@@ -0,0 +1,49 @@
+
+
+
+ Plugin for adding custom context menus in the editor.
+
+
+ [EditorContextMenuPlugin] allows for the addition of custom options in the editor's context menu.
+ Currently, context menus are supported for three commonly used areas: the file system, scene tree, and editor script list panel.
+
+
+
+
+
+
+
+
+ Called when creating a context menu, custom options can be added by using the [method add_context_menu_item] function.
+
+
+
+
+
+
+
+
+
+ Add custom options to the context menu of the currently specified slot.
+ To trigger a [param shortcut] before the context menu is created, please additionally call the [method add_menu_shortcut] function.
+ [codeblock]
+ func _popup_menu(paths):
+ add_context_menu_item("File Custom options", handle, ICON)
+ [/codeblock]
+
+
+
+
+
+
+
+ To register the shortcut for the context menu, call this function within the [method Object._init] function, even if the context menu has not been created yet.
+ Note that this method should only be invoked from [method Object._init]; otherwise, the shortcut will not be registered correctly.
+ [codeblock]
+ func _init():
+ add_menu_shortcut(SHORTCUT, handle);
+ [/codeblock]
+
+
+
+
diff --git a/doc/classes/EditorPlugin.xml b/doc/classes/EditorPlugin.xml
index 37f8b2213be..b3191e53786 100644
--- a/doc/classes/EditorPlugin.xml
+++ b/doc/classes/EditorPlugin.xml
@@ -401,6 +401,15 @@
Adds a script at [param path] to the Autoload list as [param name].
+
+
+
+
+
+ Adds a plugin to the context menu. [param slot] is the position in the context menu where the plugin will be added.
+ Context menus are supported for three commonly used areas: the file system, scene tree, and editor script list panel.
+
+
@@ -624,6 +633,14 @@
Removes an Autoload [param name] from the list.
+
+
+
+
+
+ Removes a context menu plugin from the specified slot.
+
+
@@ -874,5 +891,17 @@
Pass the [InputEvent] to other editor plugins except the main [Node3D] one. This can be used to prevent node selection changes and work with sub-gizmos instead.
+
+ Context menu slot for the SceneTree.
+
+
+ Context menu slot for the FileSystem.
+
+
+ Context menu slot for the ScriptEditor file list.
+
+
+ Context menu slot for the FileSystem create submenu.
+
diff --git a/editor/editor_data.cpp b/editor/editor_data.cpp
index 80c4c49c87f..e5caa6a3522 100644
--- a/editor/editor_data.cpp
+++ b/editor/editor_data.cpp
@@ -36,11 +36,14 @@
#include "core/io/image_loader.h"
#include "core/io/resource_loader.h"
#include "editor/editor_node.h"
+#include "editor/editor_string_names.h"
#include "editor/editor_undo_redo_manager.h"
#include "editor/multi_node_edit.h"
+#include "editor/plugins/editor_context_menu_plugin.h"
#include "editor/plugins/editor_plugin.h"
#include "editor/plugins/script_editor_plugin.h"
#include "editor/themes/editor_scale.h"
+#include "scene/gui/popup_menu.h"
#include "scene/resources/packed_scene.h"
void EditorSelectionHistory::cleanup_history() {
@@ -519,6 +522,138 @@ EditorPlugin *EditorData::get_extension_editor_plugin(const StringName &p_class_
return plugin == nullptr ? nullptr : *plugin;
}
+void EditorData::add_context_menu_plugin(ContextMenuSlot p_slot, const Ref &p_plugin) {
+ ContextMenu cm;
+ cm.p_slot = p_slot;
+ cm.plugin = p_plugin;
+ p_plugin->start_idx = context_menu_plugins.size() * EditorContextMenuPlugin::MAX_ITEMS;
+ context_menu_plugins.push_back(cm);
+}
+
+void EditorData::remove_context_menu_plugin(ContextMenuSlot p_slot, const Ref &p_plugin) {
+ for (int i = context_menu_plugins.size() - 1; i > -1; i--) {
+ if (context_menu_plugins[i].p_slot == p_slot && context_menu_plugins[i].plugin == p_plugin) {
+ context_menu_plugins.remove_at(i);
+ }
+ }
+}
+
+int EditorData::match_context_menu_shortcut(ContextMenuSlot p_slot, const Ref &p_event) {
+ for (ContextMenu &cm : context_menu_plugins) {
+ if (cm.p_slot != p_slot) {
+ continue;
+ }
+ HashMap[, Callable> &cms = cm.plugin->context_menu_shortcuts;
+ int shortcut_idx = 0;
+ for (KeyValue][, Callable> &E : cms) {
+ const Ref &p_shortcut = E.key;
+ if (p_shortcut->matches_event(p_event)) {
+ return EditorData::CONTEXT_MENU_ITEM_ID_BASE + cm.plugin->start_idx + shortcut_idx;
+ }
+ shortcut_idx++;
+ }
+ }
+ return 0;
+}
+
+void EditorData::add_options_from_plugins(PopupMenu *p_popup, ContextMenuSlot p_slot, const Vector &p_paths) {
+ bool add_separator = false;
+
+ for (ContextMenu &cm : context_menu_plugins) {
+ if (cm.p_slot != p_slot) {
+ continue;
+ }
+ cm.plugin->clear_context_menu_items();
+ cm.plugin->add_options(p_paths);
+ HashMap &items = cm.plugin->context_menu_items;
+ if (items.size() > 0 && !add_separator) {
+ add_separator = true;
+ p_popup->add_separator();
+ }
+ for (KeyValue &E : items) {
+ EditorContextMenuPlugin::ContextMenuItem &item = E.value;
+
+ if (item.icon.is_valid()) {
+ p_popup->add_icon_item(item.icon, item.item_name, item.idx);
+ const int icon_size = p_popup->get_theme_constant(SNAME("class_icon_size"), EditorStringName(Editor));
+ p_popup->set_item_icon_max_width(-1, icon_size);
+ } else {
+ p_popup->add_item(item.item_name, item.idx);
+ }
+ if (item.shortcut.is_valid()) {
+ p_popup->set_item_shortcut(-1, item.shortcut, true);
+ }
+ }
+ }
+}
+
+template
+void EditorData::invoke_plugin_callback(ContextMenuSlot p_slot, int p_option, const T &p_arg) {
+ Variant arg = p_arg;
+ Variant *argptr = &arg;
+
+ for (int i = 0; i < context_menu_plugins.size(); i++) {
+ if (context_menu_plugins[i].p_slot != p_slot || context_menu_plugins[i].plugin.is_null()) {
+ continue;
+ }
+ Ref plugin = context_menu_plugins[i].plugin;
+
+ // Shortcut callback.
+ int shortcut_idx = 0;
+ int shortcut_base_idx = EditorData::CONTEXT_MENU_ITEM_ID_BASE + plugin->start_idx;
+ for (KeyValue][, Callable> &E : plugin->context_menu_shortcuts) {
+ if (shortcut_base_idx + shortcut_idx == p_option) {
+ const Callable &callable = E.value;
+ Callable::CallError ce;
+ Variant result;
+ callable.callp((const Variant **)&argptr, 1, result, ce);
+ }
+ shortcut_idx++;
+ }
+ if (p_option < shortcut_base_idx + shortcut_idx) {
+ return;
+ }
+
+ HashMap &items = plugin->context_menu_items;
+ for (KeyValue &E : items) {
+ EditorContextMenuPlugin::ContextMenuItem &item = E.value;
+
+ if (p_option != item.idx || !item.callable.is_valid()) {
+ continue;
+ }
+
+ Callable::CallError ce;
+ Variant result;
+ item.callable.callp((const Variant **)&argptr, 1, result, ce);
+
+ if (ce.error != Callable::CallError::CALL_OK) {
+ String err = Variant::get_callable_error_text(item.callable, nullptr, 0, ce);
+ ERR_PRINT("Error calling function from context menu: " + err);
+ }
+ }
+ }
+ // Invoke submenu items.
+ if (p_slot == CONTEXT_SLOT_FILESYSTEM) {
+ invoke_plugin_callback(CONTEXT_SUBMENU_SLOT_FILESYSTEM_CREATE, p_option, p_arg);
+ }
+}
+
+void EditorData::filesystem_options_pressed(ContextMenuSlot p_slot, int p_option, const Vector &p_selected) {
+ invoke_plugin_callback(p_slot, p_option, p_selected);
+}
+
+void EditorData::scene_tree_options_pressed(ContextMenuSlot p_slot, int p_option, const List &p_selected) {
+ TypedArray nodes;
+ for (Node *selected : p_selected) {
+ nodes.append(selected);
+ }
+ invoke_plugin_callback(p_slot, p_option, nodes);
+}
+
+void EditorData::script_editor_options_pressed(ContextMenuSlot p_slot, int p_option, const Ref &p_script) {
+ invoke_plugin_callback(p_slot, p_option, p_script);
+}
+
void EditorData::add_custom_type(const String &p_type, const String &p_inherits, const Ref]