diff --git a/bt/tasks/bt_action.cpp b/bt/tasks/bt_action.cpp index af5e6a2..9719a4e 100644 --- a/bt/tasks/bt_action.cpp +++ b/bt/tasks/bt_action.cpp @@ -13,7 +13,7 @@ PackedStringArray BTAction::get_configuration_warnings() const { PackedStringArray warnings = BTTask::get_configuration_warnings(); - if (get_child_count() != 0) { + if (get_child_count_excluding_comments() != 0) { warnings.append("Action can't have child tasks."); } return warnings; diff --git a/bt/tasks/bt_comment.cpp b/bt/tasks/bt_comment.cpp new file mode 100644 index 0000000..c57a8d3 --- /dev/null +++ b/bt/tasks/bt_comment.cpp @@ -0,0 +1,32 @@ +/** + * bt_comment.cpp + * ============================================================================= + * Copyright 2021-2023 Serhii Snitsaruk + * + * Use of this source code is governed by an MIT-style + * license that can be found in the LICENSE file or at + * https://opensource.org/licenses/MIT. + * ============================================================================= + */ + +#include "bt_comment.h" + +#include "bt_task.h" + +Ref BTComment::clone() const { + if (Engine::get_singleton()->is_editor_hint()) { + return BTTask::clone(); + } + return nullptr; +} + +PackedStringArray BTComment::get_configuration_warnings() const { + PackedStringArray warnings = BTTask::get_configuration_warnings(); + if (get_child_count_excluding_comments() > 0) { + warnings.append("Can only have other comment tasks as children."); + } + if (get_parent() == nullptr) { + warnings.append("Can't be the root task."); + } + return warnings; +} diff --git a/bt/tasks/bt_comment.h b/bt/tasks/bt_comment.h new file mode 100644 index 0000000..442db08 --- /dev/null +++ b/bt/tasks/bt_comment.h @@ -0,0 +1,27 @@ +/** + * bt_comment.h + * ============================================================================= + * Copyright 2021-2023 Serhii Snitsaruk + * + * Use of this source code is governed by an MIT-style + * license that can be found in the LICENSE file or at + * https://opensource.org/licenses/MIT. + * ============================================================================= + */ +/* bt_comment.h */ + +#ifndef BT_COMMENT_H +#define BT_COMMENT_H + +#include "bt_task.h" + +class BTComment : public BTTask { + GDCLASS(BTComment, BTTask); + +private: +public: + virtual Ref clone() const override; + virtual PackedStringArray get_configuration_warnings() const override; +}; + +#endif // BT_COMMENT \ No newline at end of file diff --git a/bt/tasks/bt_composite.cpp b/bt/tasks/bt_composite.cpp index 2fb35c9..364e483 100644 --- a/bt/tasks/bt_composite.cpp +++ b/bt/tasks/bt_composite.cpp @@ -13,7 +13,7 @@ PackedStringArray BTComposite::get_configuration_warnings() const { PackedStringArray warnings = BTTask::get_configuration_warnings(); - if (get_child_count() < 1) { + if (get_child_count_excluding_comments() < 1) { warnings.append("Composite should have at least one child task."); } return warnings; diff --git a/bt/tasks/bt_condition.cpp b/bt/tasks/bt_condition.cpp index 904f8ee..73bf254 100644 --- a/bt/tasks/bt_condition.cpp +++ b/bt/tasks/bt_condition.cpp @@ -13,7 +13,7 @@ PackedStringArray BTCondition::get_configuration_warnings() const { PackedStringArray warnings = BTTask::get_configuration_warnings(); - if (get_child_count() != 0) { + if (get_child_count_excluding_comments() != 0) { warnings.append("Condition task can't have child tasks."); } return warnings; diff --git a/bt/tasks/bt_decorator.cpp b/bt/tasks/bt_decorator.cpp index b887b6d..8ac2de1 100644 --- a/bt/tasks/bt_decorator.cpp +++ b/bt/tasks/bt_decorator.cpp @@ -13,7 +13,7 @@ PackedStringArray BTDecorator::get_configuration_warnings() const { PackedStringArray warnings = BTTask::get_configuration_warnings(); - if (get_child_count() != 1) { + if (get_child_count_excluding_comments() != 1) { warnings.append("Decorator should have a single child task."); } return warnings; diff --git a/bt/tasks/bt_task.cpp b/bt/tasks/bt_task.cpp index f3cc2fe..31f6ecc 100644 --- a/bt/tasks/bt_task.cpp +++ b/bt/tasks/bt_task.cpp @@ -11,6 +11,7 @@ #include "bt_task.h" +#include "bt_comment.h" #include "modules/limboai/blackboard/blackboard.h" #include "modules/limboai/util/limbo_string_names.h" #include "modules/limboai/util/limbo_utility.h" @@ -104,10 +105,19 @@ Ref BTTask::clone() const { inst->data.parent = nullptr; inst->data.agent = nullptr; inst->data.blackboard.unref(); + int num_null = 0; for (int i = 0; i < data.children.size(); i++) { Ref c = get_child(i)->clone(); - c->data.parent = inst.ptr(); - inst->data.children.set(i, c); + if (c.is_valid()) { + c->data.parent = inst.ptr(); + inst->data.children.set(i - num_null, c); + } else { + num_null += 1; + } + } + if (num_null > 0) { + // * BTComment tasks return nullptr at runtime - we remove those. + inst->data.children.resize(data.children.size() - num_null); } // Make BBParam properties unique. @@ -189,6 +199,16 @@ int BTTask::get_child_count() const { return data.children.size(); } +int BTTask::get_child_count_excluding_comments() const { + int count = 0; + for (int i = 0; i < data.children.size(); i++) { + if (!data.children[i]->is_class_ptr(BTComment::get_class_ptr_static())) { + count += 1; + } + } + return count; +} + void BTTask::add_child(Ref p_child) { ERR_FAIL_COND_MSG(p_child->get_parent().is_valid(), "p_child already has a parent!"); p_child->data.parent = this; @@ -282,6 +302,7 @@ void BTTask::_bind_methods() { ClassDB::bind_method(D_METHOD("execute", "p_delta"), &BTTask::execute); ClassDB::bind_method(D_METHOD("get_child", "p_idx"), &BTTask::get_child); ClassDB::bind_method(D_METHOD("get_child_count"), &BTTask::get_child_count); + ClassDB::bind_method(D_METHOD("get_child_count_excluding_comments"), &BTTask::get_child_count_excluding_comments); ClassDB::bind_method(D_METHOD("add_child", "p_child"), &BTTask::add_child); ClassDB::bind_method(D_METHOD("add_child_at_index", "p_child", "p_idx"), &BTTask::add_child_at_index); ClassDB::bind_method(D_METHOD("remove_child", "p_child"), &BTTask::remove_child); diff --git a/bt/tasks/bt_task.h b/bt/tasks/bt_task.h index d21ae79..80e28c1 100644 --- a/bt/tasks/bt_task.h +++ b/bt/tasks/bt_task.h @@ -93,6 +93,7 @@ public: Ref get_child(int p_idx) const; int get_child_count() const; + int get_child_count_excluding_comments() const; void add_child(Ref p_child); void add_child_at_index(Ref p_child, int p_idx); void remove_child(Ref p_child); diff --git a/config.py b/config.py index ef9e5dd..3c1b166 100644 --- a/config.py +++ b/config.py @@ -65,6 +65,7 @@ def get_doc_classes(): "BTCheckAgentProperty", "BTCheckTrigger", "BTCheckVar", + "BTComment", "BTComposite", "BTCondition", "BTConsolePrint", diff --git a/doc_classes/BTParallel.xml b/doc_classes/BTParallel.xml index 6253f9a..3b74ff6 100644 --- a/doc_classes/BTParallel.xml +++ b/doc_classes/BTParallel.xml @@ -1,27 +1,27 @@ - BT composite that executes tasks simultaneously. + BT composite that executes child tasks until one of the criteria is met. - BT composite that executes tasks simultaneously until one of the criterea is met. BTParallel will execute each task from first to last at least once before returning a result. - If set to [member repeat], the tasks will be executed again, even if they returned [code]SUCCESS[/code] or [code]FAILURE[/code] on the previous tick. - Returns [code]FAILURE[/code] when a required number of tasks return [code]FAILURE[/code]. When [member repeat] is set to [code]false[/code], if none of the criteria were met, and all child tasks returned [code]SUCCESS[/code] or [code]FAILURE[/code], [BTParallel] will return [code]FAILURE[/code]. - Returns [code]SUCCESS[/code] when a required number of tasks return [code]SUCCESS[/code]. - Returns [code]RUNNING[/code] after executing all tasks from first to last, and for as long as the above criterea are not met. + BTParallel executes its child tasks until one of the criterea is met. It will execute each task at least once, from the first to the last, before returning a result. + If set to [member repeat], the child tasks will be re-executed, regardless of whether they previously resulted in a [code]SUCCESS[/code] or [code]FAILURE[/code]. + Returns [code]FAILURE[/code] when the required number of tasks return [code]FAILURE[/code]. When [member repeat] is set to [code]false[/code], if none of the criteria were met, and all child tasks resulted in a [code]SUCCESS[/code] or [code]FAILURE[/code], BTParallel will return [code]FAILURE[/code]. + Returns [code]SUCCESS[/code] when the required number of tasks return [code]SUCCESS[/code]. + Returns [code]RUNNING[/code] if none of the criterea were fulfilled, and either [member repeat] is set to [code]true[/code] or a child task resulted in [code]RUNNING[/code]. - When the specified number of child tasks return [code]SUCCESS[/code], [BTParallel] will also return [code]SUCCESS[/code]. + When the specified number of child tasks return [code]SUCCESS[/code], BTParallel will also return [code]SUCCESS[/code]. - When the specified number of child tasks return [code]FAILURE[/code], [BTParallel] will also return [code]FAILURE[/code]. + When the specified number of child tasks return [code]FAILURE[/code], BTParallel will also return [code]FAILURE[/code]. - When [code]true[/code], the tasks will be executed again, even if they returned [code]SUCCESS[/code] or [code]FAILURE[/code] on the previous tick. - When [code]false[/code], if none of the criteria were met, and all child tasks returned [code]SUCCESS[/code] or [code]FAILURE[/code], [BTParallel] will return [code]FAILURE[/code]. + When [code]true[/code], the child tasks will be executed again, regardless of whether they previously resulted in a [code]SUCCESS[/code] or [code]FAILURE[/code]. + When [code]false[/code], if none of the criteria were met, and all child tasks resulted in a [code]SUCCESS[/code] or [code]FAILURE[/code], BTParallel will return [code]FAILURE[/code]. diff --git a/doc_classes/BTSelector.xml b/doc_classes/BTSelector.xml index be57ecc..d18601a 100644 --- a/doc_classes/BTSelector.xml +++ b/doc_classes/BTSelector.xml @@ -1,13 +1,13 @@ - BT composite that executes tasks in turn until first [code]SUCCESS[/code]. + BT composite that sequentially executes tasks until first [code]SUCCESS[/code]. - BT composite that executes child tasks from first to last until any child returns [code]SUCCESS[/code]. - Returns [code]RUNNING[/code] if a task returns [code]RUNNING[/code], and remembers last [code]RUNNING[/code] child. BTSelector will continue where it left off on the next execution tick. - Returns [code]FAILURE[/code] if all tasks return [code]FAILURE[/code]. - Returns [code]SUCCESS[/code] if any task returns [code]SUCCESS[/code]. + BTSelector executes its child tasks sequentially, from first to last, until any child returns [code]SUCCESS[/code]. + Returns [code]RUNNING[/code] if any child task returns [code]RUNNING[/code]. The composite will also remember the last child task that returned [code]RUNNING[/code], ensuring it resumes from that point in the next execution tick. + Returns [code]FAILURE[/code] if all child tasks return [code]FAILURE[/code]. + Returns [code]SUCCESS[/code] if a child task returns [code]SUCCESS[/code]. diff --git a/doc_classes/BTSequence.xml b/doc_classes/BTSequence.xml index d92e33b..e10a786 100644 --- a/doc_classes/BTSequence.xml +++ b/doc_classes/BTSequence.xml @@ -1,13 +1,13 @@ - BT composite that executes tasks in turn for as long as they return [code]SUCCESS[/code]. + BT composite that sequentially executes tasks as long as they return [code]SUCCESS[/code]. - BT composite that executes child tasks from first to last for as long as they return [code]SUCCESS[/code]. - Returns [code]RUNNING[/code] if a task returns [code]RUNNING[/code], and remembers last [code]RUNNING[/code] child. BTSequence will continue where it left off on the next execution tick. - Returns [code]FAILURE[/code] if any task returns [code]FAILURE[/code]. - Returns [code]SUCCESS[/code] if all tasks return [code]SUCCESS[/code]. + BTSequence executes its child tasks sequentially, from first to last, as long as they return [code]SUCCESS[/code]. + Returns [code]RUNNING[/code] if any child task returns [code]RUNNING[/code]. The composite will also remember the last child task that returned [code]RUNNING[/code], ensuring it resumes from that point in the next execution tick. + Returns [code]FAILURE[/code] if a child task returns [code]FAILURE[/code]. + Returns [code]SUCCESS[/code] if all child tasks return [code]SUCCESS[/code]. diff --git a/editor/action_banner.cpp b/editor/action_banner.cpp new file mode 100644 index 0000000..312139b --- /dev/null +++ b/editor/action_banner.cpp @@ -0,0 +1,79 @@ +/** + * action_banner.cpp + * ============================================================================= + * Copyright 2021-2023 Serhii Snitsaruk + * + * Use of this source code is governed by an MIT-style + * license that can be found in the LICENSE file or at + * https://opensource.org/licenses/MIT. + * ============================================================================= + */ + +#include "action_banner.h" + +#include "scene/gui/button.h" + +void ActionBanner::set_text(const String &p_text) { + message->set_text(p_text); +} + +String ActionBanner::get_text() const { + return message->get_text(); +} + +void ActionBanner::close() { + queue_free(); +} + +void ActionBanner::add_action(const String &p_name, const Callable &p_action, bool p_auto_close) { + Button *action_btn = memnew(Button); + action_btn->set_text(p_name); + action_btn->connect(SNAME("pressed"), callable_mp(this, &ActionBanner::_execute_action).bind(p_action, p_auto_close)); + hbox->add_child(action_btn); +} + +void ActionBanner::_execute_action(const Callable &p_action, bool p_auto_close) { + Callable::CallError ce; + Variant ret; + p_action.callp(nullptr, 0, ret, ce); + + if (p_auto_close) { + queue_free(); + } +} + +void ActionBanner::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_THEME_CHANGED: { + icon->set_texture(get_theme_icon(SNAME("StatusWarning"), SNAME("EditorIcons"))); + } break; + } +} + +void ActionBanner::_bind_methods() { +} + +ActionBanner::ActionBanner() { + add_theme_constant_override("margin_bottom", 4); + add_theme_constant_override("margin_top", 4); + add_theme_constant_override("margin_left", 10); + add_theme_constant_override("margin_right", 10); + + hbox = memnew(HBoxContainer); + hbox->add_theme_constant_override("hseparation", 8); + add_child(hbox); + + icon = memnew(TextureRect); + icon->set_expand_mode(TextureRect::ExpandMode::EXPAND_KEEP_SIZE); + icon->set_stretch_mode(TextureRect::StretchMode::STRETCH_KEEP_CENTERED); + hbox->add_child(icon); + + message = memnew(Label); + message->set_text(vformat(TTR("User task folder doesn't exist"))); + message->set_vertical_alignment(VERTICAL_ALIGNMENT_CENTER); + hbox->add_child(message); + + Control *spacer = memnew(Control); + spacer->set_custom_minimum_size(Size2(0, 16)); + hbox->add_child(spacer); +} diff --git a/editor/action_banner.h b/editor/action_banner.h new file mode 100644 index 0000000..a4a7ad0 --- /dev/null +++ b/editor/action_banner.h @@ -0,0 +1,48 @@ +/** + * action_banner.h + * ============================================================================= + * Copyright 2021-2023 Serhii Snitsaruk + * + * Use of this source code is governed by an MIT-style + * license that can be found in the LICENSE file or at + * https://opensource.org/licenses/MIT. + * ============================================================================= + */ +/* action_banner.h */ + +#ifndef ACTION_BANNER_H +#define ACTION_BANNER_H + +#include "scene/gui/margin_container.h" + +#include "scene/gui/box_container.h" +#include "scene/gui/label.h" +#include "scene/gui/texture_rect.h" + +class ActionBanner : public MarginContainer { + GDCLASS(ActionBanner, MarginContainer); + +private: + TextureRect *icon; + Label *message; + HBoxContainer *hbox; + + void _execute_action(const Callable &p_action, bool p_auto_close); + +protected: + static void _bind_methods(); + + void _notification(int p_what); + +public: + void set_text(const String &p_text); + String get_text() const; + + void add_action(const String &p_name, const Callable &p_action, bool p_auto_close = false); + + void close(); + + ActionBanner(); +}; + +#endif // ACTION_BANNER \ No newline at end of file diff --git a/editor/debugger/limbo_debugger_plugin.cpp b/editor/debugger/limbo_debugger_plugin.cpp index 9e33e08..efb7fa5 100644 --- a/editor/debugger/limbo_debugger_plugin.cpp +++ b/editor/debugger/limbo_debugger_plugin.cpp @@ -32,9 +32,10 @@ #include "scene/gui/label.h" #include "scene/gui/line_edit.h" #include "scene/gui/split_container.h" +#include "scene/gui/tab_container.h" #include "scene/gui/texture_rect.h" -/////////////////////// LimboDebuggerTab +//**** LimboDebuggerTab void LimboDebuggerTab::start_session() { bt_player_list->clear(); @@ -209,7 +210,9 @@ LimboDebuggerTab::LimboDebuggerTab(Ref p_session, WindowW stop_session(); } -//////////////////////// LimboDebuggerPlugin +//**** LimboDebuggerPlugin + +LimboDebuggerPlugin *LimboDebuggerPlugin::singleton = nullptr; void LimboDebuggerPlugin::_window_visibility_changed(bool p_visible) { } @@ -259,8 +262,23 @@ bool LimboDebuggerPlugin::has_capture(const String &p_capture) const { return p_capture == "limboai"; } +WindowWrapper *LimboDebuggerPlugin::get_session_tab() const { + return window_wrapper; +} + +int LimboDebuggerPlugin::get_session_tab_index() const { + TabContainer *c = Object::cast_to(window_wrapper->get_parent()); + ERR_FAIL_COND_V(c == nullptr, -1); + return c->get_tab_idx_from_control(window_wrapper); +} + LimboDebuggerPlugin::LimboDebuggerPlugin() { tab = nullptr; + singleton = this; +} + +LimboDebuggerPlugin::~LimboDebuggerPlugin() { + singleton = nullptr; } #endif // TOOLS_ENABLED diff --git a/editor/debugger/limbo_debugger_plugin.h b/editor/debugger/limbo_debugger_plugin.h index ea5895e..fcfdfa6 100644 --- a/editor/debugger/limbo_debugger_plugin.h +++ b/editor/debugger/limbo_debugger_plugin.h @@ -70,17 +70,25 @@ class LimboDebuggerPlugin : public EditorDebuggerPlugin { GDCLASS(LimboDebuggerPlugin, EditorDebuggerPlugin); private: + static LimboDebuggerPlugin *singleton; + LimboDebuggerTab *tab = nullptr; WindowWrapper *window_wrapper = nullptr; void _window_visibility_changed(bool p_visible); public: + static _FORCE_INLINE_ LimboDebuggerPlugin *get_singleton() { return singleton; } + void setup_session(int p_idx) override; bool has_capture(const String &p_capture) const override; bool capture(const String &p_message, const Array &p_data, int p_session) override; + WindowWrapper *get_session_tab() const; + int get_session_tab_index() const; + LimboDebuggerPlugin(); + ~LimboDebuggerPlugin(); }; #endif // LIMBO_DEBUGGER_PLUGIN diff --git a/editor/limbo_ai_editor_plugin.cpp b/editor/limbo_ai_editor_plugin.cpp index aa67d4c..39f9275 100644 --- a/editor/limbo_ai_editor_plugin.cpp +++ b/editor/limbo_ai_editor_plugin.cpp @@ -13,8 +13,10 @@ #include "limbo_ai_editor_plugin.h" +#include "action_banner.h" #include "modules/limboai/bt/behavior_tree.h" #include "modules/limboai/bt/tasks/bt_action.h" +#include "modules/limboai/bt/tasks/bt_comment.h" #include "modules/limboai/bt/tasks/bt_task.h" #include "modules/limboai/bt/tasks/composites/bt_parallel.h" #include "modules/limboai/bt/tasks/composites/bt_selector.h" @@ -49,7 +51,10 @@ #include "core/variant/callable.h" #include "core/variant/dictionary.h" #include "core/variant/variant.h" +#include "editor/debugger/editor_debugger_node.h" +#include "editor/debugger/script_editor_debugger.h" #include "editor/editor_file_system.h" +#include "editor/editor_help.h" #include "editor/editor_inspector.h" #include "editor/editor_node.h" #include "editor/editor_paths.h" @@ -59,6 +64,7 @@ #include "editor/editor_undo_redo_manager.h" #include "editor/inspector_dock.h" #include "editor/plugins/script_editor_plugin.h" +#include "editor/project_settings_editor.h" #include "scene/gui/box_container.h" #include "scene/gui/button.h" #include "scene/gui/control.h" @@ -94,6 +100,16 @@ void TaskTree::_update_item(TreeItem *p_item) { Ref task = p_item->get_metadata(0); ERR_FAIL_COND_MSG(!task.is_valid(), "Invalid task reference in metadata."); p_item->set_text(0, task->get_task_name()); + if (task->is_class_ptr(BTComment::get_class_ptr_static())) { + p_item->set_custom_font(0, (get_theme_font(SNAME("doc_italic"), SNAME("EditorFonts")))); + p_item->set_custom_color(0, get_theme_color(SNAME("disabled_font_color"), SNAME("Editor"))); + } else if (task->get_custom_name().is_empty()) { + p_item->set_custom_font(0, nullptr); + p_item->clear_custom_color(0); + } else { + p_item->set_custom_font(0, (get_theme_font(SNAME("bold"), SNAME("EditorFonts")))); + // p_item->set_custom_color(0, get_theme_color(SNAME("warning_color"), SNAME("Editor"))); + } String type_arg; if (task->get_script_instance() && !task->get_script_instance()->get_script()->get_path().is_empty()) { type_arg = task->get_script_instance()->get_script()->get_path(); @@ -341,12 +357,43 @@ TaskTree::~TaskTree() { //**** TaskTree ^ +//**** TaskButton + +Control *TaskButton::make_custom_tooltip(const String &p_text) const { + EditorHelpBit *help_bit = memnew(EditorHelpBit); + help_bit->get_rich_text()->set_custom_minimum_size(Size2(360 * EDSCALE, 1)); + + String help_text; + if (!p_text.is_empty()) { + help_text = p_text; + } else { + help_text = "[i]" + TTR("No description.") + "[/i]"; + } + + help_bit->set_text(help_text); + + return help_bit; +} + +//**** TaskButton ^ + //**** TaskSection -void TaskSection::_on_task_button_pressed(const StringName &p_task) { +void TaskSection::_on_task_button_pressed(const String &p_task) { emit_signal(SNAME("task_button_pressed"), p_task); } +void TaskSection::_on_task_button_gui_input(const Ref &p_event, const String &p_task) { + if (!p_event->is_pressed()) { + return; + } + + Ref mb = p_event; + if (mb.is_valid() && mb->get_button_index() == MouseButton::RIGHT) { + emit_signal(SNAME("task_button_rmb"), p_task); + } +} + void TaskSection::_on_header_pressed() { set_collapsed(!is_collapsed()); } @@ -368,12 +415,14 @@ void TaskSection::set_filter(String p_filter_text) { } } -void TaskSection::add_task_button(String p_name, const Ref &icon, Variant p_meta) { - Button *btn = memnew(Button); +void TaskSection::add_task_button(const String &p_name, const Ref &icon, const String &p_tooltip, Variant p_meta) { + TaskButton *btn = memnew(TaskButton); btn->set_text(p_name); btn->set_icon(icon); + btn->set_tooltip_text(p_tooltip); btn->add_theme_constant_override(SNAME("icon_max_width"), 16 * EDSCALE); // Force user icons to be of the proper size. - btn->connect("pressed", callable_mp(this, &TaskSection::_on_task_button_pressed).bind(p_meta)); + btn->connect(SNAME("pressed"), callable_mp(this, &TaskSection::_on_task_button_pressed).bind(p_meta)); + btn->connect(SNAME("gui_input"), callable_mp(this, &TaskSection::_on_task_button_gui_input).bind(p_meta)); tasks_container->add_child(btn); } @@ -395,6 +444,7 @@ void TaskSection::_notification(int p_what) { void TaskSection::_bind_methods() { ADD_SIGNAL(MethodInfo("task_button_pressed")); + ADD_SIGNAL(MethodInfo("task_button_rmb")); } TaskSection::TaskSection(String p_category_name) { @@ -415,11 +465,68 @@ TaskSection::~TaskSection() { //**** TaskPanel -void TaskPanel::_on_task_button_pressed(const StringName &p_task) { +void TaskPanel::_menu_action_selected(int p_id) { + ERR_FAIL_COND(context_task.is_empty()); + switch (p_id) { + case MENU_OPEN_DOC: { + String help_class; + if (context_task.begins_with("res://")) { + Ref