summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRémi Verschelde <rverschelde@gmail.com>2024-09-03 16:13:47 +0200
committerRémi Verschelde <rverschelde@gmail.com>2024-09-03 16:13:47 +0200
commit79da448d5fb01cd1b0461b3bded1e4b9049d69b6 (patch)
treecbb92afc6b9eb072683c4b0f20eec3b0cc218536
parent5a374548fa80bef1745392196c6755547012ba9b (diff)
parent6b2348adacb5bb7affb1d50a2c71ce2b3d3c5e70 (diff)
downloadredot-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.xml49
-rw-r--r--doc/classes/EditorPlugin.xml29
-rw-r--r--editor/editor_data.cpp135
-rw-r--r--editor/editor_data.h30
-rw-r--r--editor/filesystem_dock.cpp12
-rw-r--r--editor/plugins/editor_context_menu_plugin.cpp65
-rw-r--r--editor/plugins/editor_context_menu_plugin.h70
-rw-r--r--editor/plugins/editor_plugin.cpp18
-rw-r--r--editor/plugins/editor_plugin.h12
-rw-r--r--editor/plugins/script_editor_plugin.cpp27
-rw-r--r--editor/register_editor_types.cpp2
-rw-r--r--editor/scene_tree_dock.cpp21
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();