From 6b2348adacb5bb7affb1d50a2c71ce2b3d3c5e70 Mon Sep 17 00:00:00 2001 From: citizenl Date: Sun, 21 Jul 2024 16:43:53 +0800 Subject: [PATCH] Add support for custom items to editor right-click context menus --- doc/classes/EditorContextMenuPlugin.xml | 49 +++++++ doc/classes/EditorPlugin.xml | 29 ++++ editor/editor_data.cpp | 135 ++++++++++++++++++ editor/editor_data.h | 30 ++++ editor/filesystem_dock.cpp | 12 ++ editor/plugins/editor_context_menu_plugin.cpp | 65 +++++++++ editor/plugins/editor_context_menu_plugin.h | 70 +++++++++ editor/plugins/editor_plugin.cpp | 18 +++ editor/plugins/editor_plugin.h | 12 ++ editor/plugins/script_editor_plugin.cpp | 27 ++++ editor/register_editor_types.cpp | 2 + editor/scene_tree_dock.cpp | 21 +++ 12 files changed, 470 insertions(+) create mode 100644 doc/classes/EditorContextMenuPlugin.xml create mode 100644 editor/plugins/editor_context_menu_plugin.cpp create mode 100644 editor/plugins/editor_context_menu_plugin.h 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 c3ea440aea9..e52883832c6 100644 --- a/doc/classes/EditorPlugin.xml +++ b/doc/classes/EditorPlugin.xml @@ -405,6 +405,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. + + @@ -621,6 +630,14 @@ Removes an Autoload [param name] from the list. + + + + + + Removes a context menu plugin from the specified slot. + + @@ -864,5 +881,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 11fea8b7285..8d6ebebda04 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() { @@ -509,6 +512,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