diff options
author | Rémi Verschelde <rverschelde@gmail.com> | 2024-09-03 16:13:47 +0200 |
---|---|---|
committer | Rémi Verschelde <rverschelde@gmail.com> | 2024-09-03 16:13:47 +0200 |
commit | 79da448d5fb01cd1b0461b3bded1e4b9049d69b6 (patch) | |
tree | cbb92afc6b9eb072683c4b0f20eec3b0cc218536 | |
parent | 5a374548fa80bef1745392196c6755547012ba9b (diff) | |
parent | 6b2348adacb5bb7affb1d50a2c71ce2b3d3c5e70 (diff) | |
download | redot-engine-79da448d5fb01cd1b0461b3bded1e4b9049d69b6.tar.gz |
Merge pull request #94582 from citizenll/feat_context_menu_plugin4.x
Add support for custom items to editor right-click context menus
-rw-r--r-- | doc/classes/EditorContextMenuPlugin.xml | 49 | ||||
-rw-r--r-- | doc/classes/EditorPlugin.xml | 29 | ||||
-rw-r--r-- | editor/editor_data.cpp | 135 | ||||
-rw-r--r-- | editor/editor_data.h | 30 | ||||
-rw-r--r-- | editor/filesystem_dock.cpp | 12 | ||||
-rw-r--r-- | editor/plugins/editor_context_menu_plugin.cpp | 65 | ||||
-rw-r--r-- | editor/plugins/editor_context_menu_plugin.h | 70 | ||||
-rw-r--r-- | editor/plugins/editor_plugin.cpp | 18 | ||||
-rw-r--r-- | editor/plugins/editor_plugin.h | 12 | ||||
-rw-r--r-- | editor/plugins/script_editor_plugin.cpp | 27 | ||||
-rw-r--r-- | editor/register_editor_types.cpp | 2 | ||||
-rw-r--r-- | editor/scene_tree_dock.cpp | 21 |
12 files changed, 470 insertions, 0 deletions
diff --git a/doc/classes/EditorContextMenuPlugin.xml b/doc/classes/EditorContextMenuPlugin.xml new file mode 100644 index 0000000000..7eeee3d7fd --- /dev/null +++ b/doc/classes/EditorContextMenuPlugin.xml @@ -0,0 +1,49 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<class name="EditorContextMenuPlugin" inherits="RefCounted" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../class.xsd"> + <brief_description> + Plugin for adding custom context menus in the editor. + </brief_description> + <description> + [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. + </description> + <tutorials> + </tutorials> + <methods> + <method name="_popup_menu" qualifiers="virtual"> + <return type="void" /> + <param index="0" name="paths" type="PackedStringArray" /> + <description> + Called when creating a context menu, custom options can be added by using the [method add_context_menu_item] function. + </description> + </method> + <method name="add_context_menu_item"> + <return type="void" /> + <param index="0" name="name" type="String" /> + <param index="1" name="callback" type="Callable" /> + <param index="2" name="icon" type="Texture2D" default="null" /> + <param index="3" name="shortcut" type="Shortcut" default="null" /> + <description> + 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] + </description> + </method> + <method name="add_menu_shortcut"> + <return type="void" /> + <param index="0" name="shortcut" type="Shortcut" /> + <param index="1" name="callback" type="Callable" /> + <description> + 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] + </description> + </method> + </methods> +</class> diff --git a/doc/classes/EditorPlugin.xml b/doc/classes/EditorPlugin.xml index 37f8b2213b..b3191e5378 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]. </description> </method> + <method name="add_context_menu_plugin"> + <return type="void" /> + <param index="0" name="slot" type="int" enum="EditorPlugin.ContextMenuSlot" /> + <param index="1" name="plugin" type="EditorContextMenuPlugin" /> + <description> + 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. + </description> + </method> <method name="add_control_to_bottom_panel"> <return type="Button" /> <param index="0" name="control" type="Control" /> @@ -624,6 +633,14 @@ Removes an Autoload [param name] from the list. </description> </method> + <method name="remove_context_menu_plugin"> + <return type="void" /> + <param index="0" name="slot" type="int" enum="EditorPlugin.ContextMenuSlot" /> + <param index="1" name="plugin" type="EditorContextMenuPlugin" /> + <description> + Removes a context menu plugin from the specified slot. + </description> + </method> <method name="remove_control_from_bottom_panel"> <return type="void" /> <param index="0" name="control" type="Control" /> @@ -874,5 +891,17 @@ <constant name="AFTER_GUI_INPUT_CUSTOM" value="2" enum="AfterGUIInput"> 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. </constant> + <constant name="CONTEXT_SLOT_SCENE_TREE" value="0" enum="ContextMenuSlot"> + Context menu slot for the SceneTree. + </constant> + <constant name="CONTEXT_SLOT_FILESYSTEM" value="1" enum="ContextMenuSlot"> + Context menu slot for the FileSystem. + </constant> + <constant name="CONTEXT_SLOT_SCRIPT_EDITOR" value="2" enum="ContextMenuSlot"> + Context menu slot for the ScriptEditor file list. + </constant> + <constant name="CONTEXT_SUBMENU_SLOT_FILESYSTEM_CREATE" value="3" enum="ContextMenuSlot"> + Context menu slot for the FileSystem create submenu. + </constant> </constants> </class> diff --git a/editor/editor_data.cpp b/editor/editor_data.cpp index 80c4c49c87..e5caa6a352 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<EditorContextMenuPlugin> &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<EditorContextMenuPlugin> &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<InputEvent> &p_event) { + for (ContextMenu &cm : context_menu_plugins) { + if (cm.p_slot != p_slot) { + continue; + } + HashMap<Ref<Shortcut>, Callable> &cms = cm.plugin->context_menu_shortcuts; + int shortcut_idx = 0; + for (KeyValue<Ref<Shortcut>, Callable> &E : cms) { + const Ref<Shortcut> &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<String> &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<String, EditorContextMenuPlugin::ContextMenuItem> &items = cm.plugin->context_menu_items; + if (items.size() > 0 && !add_separator) { + add_separator = true; + p_popup->add_separator(); + } + for (KeyValue<String, EditorContextMenuPlugin::ContextMenuItem> &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 <typename T> +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<EditorContextMenuPlugin> 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<Ref<Shortcut>, 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<String, EditorContextMenuPlugin::ContextMenuItem> &items = plugin->context_menu_items; + for (KeyValue<String, EditorContextMenuPlugin::ContextMenuItem> &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<String> &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<Node *> &p_selected) { + TypedArray<Node> 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<Resource> &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<Script> &p_script, const Ref<Texture2D> &p_icon) { ERR_FAIL_COND_MSG(p_script.is_null(), "It's not a reference to a valid Script object."); CustomType ct; diff --git a/editor/editor_data.h b/editor/editor_data.h index 524c93807b..754d44c479 100644 --- a/editor/editor_data.h +++ b/editor/editor_data.h @@ -37,6 +37,8 @@ class ConfigFile; class EditorPlugin; class EditorUndoRedoManager; +class EditorContextMenuPlugin; +class PopupMenu; /** * Stores the history of objects which have been selected for editing in the Editor & the Inspector. @@ -123,6 +125,22 @@ public: uint64_t last_checked_version = 0; }; + enum ContextMenuSlot { + CONTEXT_SLOT_SCENE_TREE, + CONTEXT_SLOT_FILESYSTEM, + CONTEXT_SLOT_SCRIPT_EDITOR, + CONTEXT_SUBMENU_SLOT_FILESYSTEM_CREATE, + }; + + inline static constexpr int CONTEXT_MENU_ITEM_ID_BASE = 1000; + + struct ContextMenu { + int p_slot; + Ref<EditorContextMenuPlugin> plugin; + }; + + Vector<ContextMenu> context_menu_plugins; + private: Vector<EditorPlugin *> editor_plugins; HashMap<StringName, EditorPlugin *> extension_editor_plugins; @@ -177,6 +195,18 @@ public: bool has_extension_editor_plugin(const StringName &p_class_name); EditorPlugin *get_extension_editor_plugin(const StringName &p_class_name); + // Context menu plugin. + void add_context_menu_plugin(ContextMenuSlot p_slot, const Ref<EditorContextMenuPlugin> &p_plugin); + void remove_context_menu_plugin(ContextMenuSlot p_slot, const Ref<EditorContextMenuPlugin> &p_plugin); + int match_context_menu_shortcut(ContextMenuSlot p_slot, const Ref<InputEvent> &p_event); + + void add_options_from_plugins(PopupMenu *p_popup, ContextMenuSlot p_slot, const Vector<String> &p_paths); + void filesystem_options_pressed(ContextMenuSlot p_slot, int p_option, const Vector<String> &p_selected); + void scene_tree_options_pressed(ContextMenuSlot p_slot, int p_option, const List<Node *> &p_selected); + void script_editor_options_pressed(ContextMenuSlot p_slot, int p_option, const Ref<Resource> &p_script); + template <typename T> + void invoke_plugin_callback(ContextMenuSlot p_slot, int p_option, const T &p_arg); + void add_undo_redo_inspector_hook_callback(Callable p_callable); // Callbacks should have this signature: void (Object* undo_redo, Object *modified_object, String property, Variant new_value) void remove_undo_redo_inspector_hook_callback(Callable p_callable); const Vector<Callable> get_undo_redo_inspector_hook_callback(); diff --git a/editor/filesystem_dock.cpp b/editor/filesystem_dock.cpp index bfb7123925..99cd2ad526 100644 --- a/editor/filesystem_dock.cpp +++ b/editor/filesystem_dock.cpp @@ -49,6 +49,7 @@ #include "editor/gui/editor_scene_tabs.h" #include "editor/import/3d/scene_import_settings.h" #include "editor/import_dock.h" +#include "editor/plugins/editor_context_menu_plugin.h" #include "editor/plugins/editor_resource_tooltip_plugins.h" #include "editor/scene_create_dialog.h" #include "editor/scene_tree_dock.h" @@ -2556,6 +2557,9 @@ void FileSystemDock::_file_option(int p_option, const Vector<String> &p_selected String dir = ProjectSettings::get_singleton()->globalize_path(fpath); ScriptEditor::get_singleton()->open_text_file_create_dialog(dir); } break; + default: + EditorNode::get_editor_data().filesystem_options_pressed(EditorData::CONTEXT_SLOT_FILESYSTEM, p_option, p_selected); + break; } } @@ -3178,6 +3182,8 @@ void FileSystemDock::_file_and_folders_fill_popup(PopupMenu *p_popup, const Vect new_menu->add_icon_item(get_editor_theme_icon(SNAME("Script")), TTR("Script..."), FILE_NEW_SCRIPT); new_menu->add_icon_item(get_editor_theme_icon(SNAME("Object")), TTR("Resource..."), FILE_NEW_RESOURCE); new_menu->add_icon_item(get_editor_theme_icon(SNAME("TextFile")), TTR("TextFile..."), FILE_NEW_TEXTFILE); + + EditorNode::get_editor_data().add_options_from_plugins(new_menu, EditorData::CONTEXT_SUBMENU_SLOT_FILESYSTEM_CREATE, p_paths); p_popup->add_separator(); } @@ -3317,6 +3323,8 @@ void FileSystemDock::_file_and_folders_fill_popup(PopupMenu *p_popup, const Vect current_path = fpath; } + + EditorNode::get_editor_data().add_options_from_plugins(p_popup, EditorData::CONTEXT_SLOT_FILESYSTEM, p_paths); } void FileSystemDock::_tree_rmb_select(const Vector2 &p_pos, MouseButton p_button) { @@ -3546,6 +3554,10 @@ void FileSystemDock::_tree_gui_input(Ref<InputEvent> p_event) { } else if (ED_IS_SHORTCUT("editor/open_search", p_event)) { focus_on_filter(); } else { + int match_option = EditorNode::get_editor_data().match_context_menu_shortcut(EditorData::CONTEXT_SLOT_FILESYSTEM, p_event); + if (match_option) { + _tree_rmb_option(match_option); + } return; } diff --git a/editor/plugins/editor_context_menu_plugin.cpp b/editor/plugins/editor_context_menu_plugin.cpp new file mode 100644 index 0000000000..86d3c0a896 --- /dev/null +++ b/editor/plugins/editor_context_menu_plugin.cpp @@ -0,0 +1,65 @@ +/**************************************************************************/ +/* editor_context_menu_plugin.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#include "editor_context_menu_plugin.h" + +#include "core/input/shortcut.h" +#include "editor/editor_node.h" +#include "scene/resources/texture.h" + +void EditorContextMenuPlugin::add_options(const Vector<String> &p_paths) { + GDVIRTUAL_CALL(_popup_menu, p_paths); +} + +void EditorContextMenuPlugin::add_menu_shortcut(const Ref<Shortcut> &p_shortcut, const Callable &p_callable) { + context_menu_shortcuts.insert(p_shortcut, p_callable); +} + +void EditorContextMenuPlugin::add_context_menu_item(const String &p_name, const Callable &p_callable, const Ref<Texture2D> &p_texture, const Ref<Shortcut> &p_shortcut) { + ERR_FAIL_COND_MSG(context_menu_items.has(p_name), "Context menu item already registered."); + ERR_FAIL_COND_MSG(context_menu_items.size() == MAX_ITEMS, "Maximum number of context menu items reached."); + ContextMenuItem item; + item.item_name = p_name; + item.callable = p_callable; + item.icon = p_texture; + item.shortcut = p_shortcut; + item.idx = EditorData::CONTEXT_MENU_ITEM_ID_BASE + start_idx + context_menu_shortcuts.size() + context_menu_items.size(); + context_menu_items.insert(p_name, item); +} + +void EditorContextMenuPlugin::clear_context_menu_items() { + context_menu_items.clear(); +} + +void EditorContextMenuPlugin::_bind_methods() { + ClassDB::bind_method(D_METHOD("add_menu_shortcut", "shortcut", "callback"), &EditorContextMenuPlugin::add_menu_shortcut); + ClassDB::bind_method(D_METHOD("add_context_menu_item", "name", "callback", "icon", "shortcut"), &EditorContextMenuPlugin::add_context_menu_item, DEFVAL(Ref<Texture2D>()), DEFVAL(Ref<Shortcut>())); + GDVIRTUAL_BIND(_popup_menu, "paths"); +} diff --git a/editor/plugins/editor_context_menu_plugin.h b/editor/plugins/editor_context_menu_plugin.h new file mode 100644 index 0000000000..db05e6c6c7 --- /dev/null +++ b/editor/plugins/editor_context_menu_plugin.h @@ -0,0 +1,70 @@ +/**************************************************************************/ +/* editor_context_menu_plugin.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef EDITOR_CONTEXT_MENU_PLUGIN_H +#define EDITOR_CONTEXT_MENU_PLUGIN_H + +#include "core/object/gdvirtual.gen.inc" +#include "core/object/ref_counted.h" + +class Texture2D; +class Shortcut; + +class EditorContextMenuPlugin : public RefCounted { + GDCLASS(EditorContextMenuPlugin, RefCounted); + +public: + int start_idx; + + inline static constexpr int MAX_ITEMS = 100; + + struct ContextMenuItem { + int idx = 0; + String item_name; + Callable callable; + Ref<Texture2D> icon; + Ref<Shortcut> shortcut; + }; + HashMap<String, ContextMenuItem> context_menu_items; + HashMap<Ref<Shortcut>, Callable> context_menu_shortcuts; + +protected: + static void _bind_methods(); + + GDVIRTUAL1(_popup_menu, Vector<String>); + +public: + virtual void add_options(const Vector<String> &p_paths); + void add_menu_shortcut(const Ref<Shortcut> &p_shortcut, const Callable &p_callable); + void add_context_menu_item(const String &p_name, const Callable &p_callable, const Ref<Texture2D> &p_texture, const Ref<Shortcut> &p_shortcut); + void clear_context_menu_items(); +}; + +#endif // EDITOR_CONTEXT_MENU_PLUGIN_H diff --git a/editor/plugins/editor_plugin.cpp b/editor/plugins/editor_plugin.cpp index 8ce667568f..0ee67e08ba 100644 --- a/editor/plugins/editor_plugin.cpp +++ b/editor/plugins/editor_plugin.cpp @@ -47,6 +47,7 @@ #include "editor/import/editor_import_plugin.h" #include "editor/inspector_dock.h" #include "editor/plugins/canvas_item_editor_plugin.h" +#include "editor/plugins/editor_context_menu_plugin.h" #include "editor/plugins/editor_debugger_plugin.h" #include "editor/plugins/editor_resource_conversion_plugin.h" #include "editor/plugins/node_3d_editor_plugin.h" @@ -490,6 +491,16 @@ void EditorPlugin::remove_scene_post_import_plugin(const Ref<EditorScenePostImpo ResourceImporterScene::remove_post_importer_plugin(p_plugin); } +void EditorPlugin::add_context_menu_plugin(ContextMenuSlot p_slot, const Ref<EditorContextMenuPlugin> &p_plugin) { + ERR_FAIL_COND(p_plugin.is_null()); + EditorNode::get_editor_data().add_context_menu_plugin(EditorData::ContextMenuSlot(p_slot), p_plugin); +} + +void EditorPlugin::remove_context_menu_plugin(ContextMenuSlot p_slot, const Ref<EditorContextMenuPlugin> &p_plugin) { + ERR_FAIL_COND(p_plugin.is_null()); + EditorNode::get_editor_data().remove_context_menu_plugin(EditorData::ContextMenuSlot(p_slot), p_plugin); +} + int find(const PackedStringArray &a, const String &v) { const String *r = a.ptr(); for (int j = 0; j < a.size(); ++j) { @@ -629,6 +640,8 @@ void EditorPlugin::_bind_methods() { ClassDB::bind_method(D_METHOD("remove_resource_conversion_plugin", "plugin"), &EditorPlugin::remove_resource_conversion_plugin); ClassDB::bind_method(D_METHOD("set_input_event_forwarding_always_enabled"), &EditorPlugin::set_input_event_forwarding_always_enabled); ClassDB::bind_method(D_METHOD("set_force_draw_over_forwarding_enabled"), &EditorPlugin::set_force_draw_over_forwarding_enabled); + ClassDB::bind_method(D_METHOD("add_context_menu_plugin", "slot", "plugin"), &EditorPlugin::add_context_menu_plugin); + ClassDB::bind_method(D_METHOD("remove_context_menu_plugin", "slot", "plugin"), &EditorPlugin::remove_context_menu_plugin); ClassDB::bind_method(D_METHOD("get_editor_interface"), &EditorPlugin::get_editor_interface); ClassDB::bind_method(D_METHOD("get_script_create_dialog"), &EditorPlugin::get_script_create_dialog); @@ -694,6 +707,11 @@ void EditorPlugin::_bind_methods() { BIND_ENUM_CONSTANT(AFTER_GUI_INPUT_PASS); BIND_ENUM_CONSTANT(AFTER_GUI_INPUT_STOP); BIND_ENUM_CONSTANT(AFTER_GUI_INPUT_CUSTOM); + + BIND_ENUM_CONSTANT(CONTEXT_SLOT_SCENE_TREE); + BIND_ENUM_CONSTANT(CONTEXT_SLOT_FILESYSTEM); + BIND_ENUM_CONSTANT(CONTEXT_SLOT_SCRIPT_EDITOR); + BIND_ENUM_CONSTANT(CONTEXT_SUBMENU_SLOT_FILESYSTEM_CREATE); } EditorUndoRedoManager *EditorPlugin::get_undo_redo() { diff --git a/editor/plugins/editor_plugin.h b/editor/plugins/editor_plugin.h index 2e0771f906..35f65f03cd 100644 --- a/editor/plugins/editor_plugin.h +++ b/editor/plugins/editor_plugin.h @@ -53,6 +53,7 @@ class EditorToolAddons; class EditorTranslationParserPlugin; class EditorUndoRedoManager; class ScriptCreateDialog; +class EditorContextMenuPlugin; class EditorPlugin : public Node { GDCLASS(EditorPlugin, Node); @@ -102,6 +103,13 @@ public: AFTER_GUI_INPUT_CUSTOM, }; + enum ContextMenuSlot { + CONTEXT_SLOT_SCENE_TREE, + CONTEXT_SLOT_FILESYSTEM, + CONTEXT_SLOT_SCRIPT_EDITOR, + CONTEXT_SUBMENU_SLOT_FILESYSTEM_CREATE, + }; + protected: void _notification(int p_what); @@ -249,6 +257,9 @@ public: void add_resource_conversion_plugin(const Ref<EditorResourceConversionPlugin> &p_plugin); void remove_resource_conversion_plugin(const Ref<EditorResourceConversionPlugin> &p_plugin); + void add_context_menu_plugin(ContextMenuSlot p_slot, const Ref<EditorContextMenuPlugin> &p_plugin); + void remove_context_menu_plugin(ContextMenuSlot p_slot, const Ref<EditorContextMenuPlugin> &p_plugin); + void enable_plugin(); void disable_plugin(); @@ -259,6 +270,7 @@ public: VARIANT_ENUM_CAST(EditorPlugin::CustomControlContainer); VARIANT_ENUM_CAST(EditorPlugin::DockSlot); VARIANT_ENUM_CAST(EditorPlugin::AfterGUIInput); +VARIANT_ENUM_CAST(EditorPlugin::ContextMenuSlot); typedef EditorPlugin *(*EditorPluginCreateFunc)(); diff --git a/editor/plugins/script_editor_plugin.cpp b/editor/plugins/script_editor_plugin.cpp index d7de5a7223..5d3f1b5bf0 100644 --- a/editor/plugins/script_editor_plugin.cpp +++ b/editor/plugins/script_editor_plugin.cpp @@ -57,6 +57,7 @@ #include "editor/gui/editor_toaster.h" #include "editor/inspector_dock.h" #include "editor/node_dock.h" +#include "editor/plugins/editor_context_menu_plugin.h" #include "editor/plugins/shader_editor_plugin.h" #include "editor/plugins/text_shader_editor.h" #include "editor/themes/editor_scale.h" @@ -1398,6 +1399,16 @@ void ScriptEditor::_menu_option(int p_option) { } } + // Context menu options. + if (p_option >= EditorData::CONTEXT_MENU_ITEM_ID_BASE) { + Ref<Resource> resource; + if (current) { + resource = current->get_edited_resource(); + } + EditorNode::get_editor_data().script_editor_options_pressed(EditorData::CONTEXT_SLOT_SCRIPT_EDITOR, p_option, resource); + return; + } + if (current) { switch (p_option) { case FILE_SAVE: { @@ -3304,6 +3315,11 @@ void ScriptEditor::shortcut_input(const Ref<InputEvent> &p_event) { _menu_option(WINDOW_MOVE_DOWN); accept_event(); } + // Context menu shortcuts. + int match_option = EditorNode::get_editor_data().match_context_menu_shortcut(EditorData::CONTEXT_SLOT_SCRIPT_EDITOR, p_event); + if (match_option) { + _menu_option(match_option); + } } void ScriptEditor::_script_list_clicked(int p_item, Vector2 p_local_mouse_pos, MouseButton p_mouse_button_index) { @@ -3362,6 +3378,17 @@ void ScriptEditor::_make_script_list_context_menu() { context_menu->set_item_disabled(context_menu->get_item_index(WINDOW_MOVE_DOWN), tab_container->get_current_tab() >= tab_container->get_tab_count() - 1); context_menu->set_item_disabled(context_menu->get_item_index(WINDOW_SORT), tab_container->get_tab_count() <= 1); + // Context menu plugin. + Vector<String> selected_paths; + if (se) { + Ref<Resource> scr = se->get_edited_resource(); + if (scr.is_valid()) { + String path = scr->get_path(); + selected_paths.push_back(path); + } + } + EditorNode::get_editor_data().add_options_from_plugins(context_menu, EditorData::CONTEXT_SLOT_SCRIPT_EDITOR, selected_paths); + context_menu->set_position(get_screen_position() + get_local_mouse_position()); context_menu->reset_size(); context_menu->popup(); diff --git a/editor/register_editor_types.cpp b/editor/register_editor_types.cpp index 8e135e7eae..00377a0dd2 100644 --- a/editor/register_editor_types.cpp +++ b/editor/register_editor_types.cpp @@ -79,6 +79,7 @@ #include "editor/plugins/cpu_particles_2d_editor_plugin.h" #include "editor/plugins/cpu_particles_3d_editor_plugin.h" #include "editor/plugins/curve_editor_plugin.h" +#include "editor/plugins/editor_context_menu_plugin.h" #include "editor/plugins/editor_debugger_plugin.h" #include "editor/plugins/editor_resource_tooltip_plugins.h" #include "editor/plugins/font_config_plugin.h" @@ -179,6 +180,7 @@ void register_editor_types() { GDREGISTER_CLASS(EditorResourcePicker); GDREGISTER_CLASS(EditorScriptPicker); GDREGISTER_ABSTRACT_CLASS(EditorUndoRedoManager); + GDREGISTER_CLASS(EditorContextMenuPlugin); GDREGISTER_ABSTRACT_CLASS(FileSystemDock); GDREGISTER_VIRTUAL_CLASS(EditorFileSystemImportFormatSupportQuery); diff --git a/editor/scene_tree_dock.cpp b/editor/scene_tree_dock.cpp index 3110ecb926..87b0ce6c8f 100644 --- a/editor/scene_tree_dock.cpp +++ b/editor/scene_tree_dock.cpp @@ -50,6 +50,7 @@ #include "editor/node_dock.h" #include "editor/plugins/animation_player_editor_plugin.h" #include "editor/plugins/canvas_item_editor_plugin.h" +#include "editor/plugins/editor_context_menu_plugin.h" #include "editor/plugins/node_3d_editor_plugin.h" #include "editor/plugins/script_editor_plugin.h" #include "editor/reparent_dialog.h" @@ -213,6 +214,10 @@ void SceneTreeDock::shortcut_input(const Ref<InputEvent> &p_event) { } else if (ED_IS_SHORTCUT("scene_tree/delete", p_event)) { _tool_selected(TOOL_ERASE); } else { + int match_option = EditorNode::get_editor_data().match_context_menu_shortcut(EditorData::CONTEXT_SLOT_SCENE_TREE, p_event); + if (match_option) { + _tool_selected(match_option); + } return; } @@ -1482,6 +1487,13 @@ void SceneTreeDock::_tool_selected(int p_tool, bool p_confirm_override) { } break; default: { + // Editor context plugin. + if (p_tool >= EditorData::CONTEXT_MENU_ITEM_ID_BASE) { + List<Node *> selection = editor_selection->get_selected_node_list(); + EditorNode::get_editor_data().scene_tree_options_pressed(EditorData::CONTEXT_SLOT_SCENE_TREE, p_tool, selection); + break; + } + _filter_option_selected(p_tool); if (p_tool >= EDIT_SUBRESOURCE_BASE) { @@ -3729,6 +3741,15 @@ void SceneTreeDock::_tree_rmb(const Vector2 &p_menu_pos) { menu->add_separator(); menu->add_icon_shortcut(get_editor_theme_icon(SNAME("Remove")), ED_GET_SHORTCUT("scene_tree/delete"), TOOL_ERASE); } + + Vector<String> p_paths; + Node *root = EditorNode::get_singleton()->get_edited_scene(); + for (List<Node *>::Element *E = selection.front(); E; E = E->next()) { + String node_path = root->get_path().rel_path_to(E->get()->get_path()); + p_paths.push_back(node_path); + } + EditorNode::get_editor_data().add_options_from_plugins(menu, EditorData::CONTEXT_SLOT_SCENE_TREE, p_paths); + menu->reset_size(); menu->set_position(p_menu_pos); menu->popup(); |