From 73f51f23efc6c2a2fd54c6ee6634e51a1b6f3794 Mon Sep 17 00:00:00 2001 From: Serhii Snitsaruk Date: Sun, 28 Jan 2024 17:49:38 +0100 Subject: [PATCH] Implement BB variable property editor --- editor/blackboard_plan_editor.cpp | 22 ++- editor/blackboard_plan_editor.h | 9 + editor/editor_property_variable_name.cpp | 199 +++++++++++++++++++++++ editor/editor_property_variable_name.h | 99 +++++++++++ editor/limbo_ai_editor_plugin.cpp | 11 ++ editor/limbo_ai_editor_plugin.h | 2 + gdextension/limboai.gdextension | 4 + register_types.cpp | 3 + util/limbo_string_names.cpp | 6 + util/limbo_string_names.h | 6 + 10 files changed, 357 insertions(+), 4 deletions(-) create mode 100644 editor/editor_property_variable_name.cpp create mode 100644 editor/editor_property_variable_name.h diff --git a/editor/blackboard_plan_editor.cpp b/editor/blackboard_plan_editor.cpp index 2d67e51..dff457b 100644 --- a/editor/blackboard_plan_editor.cpp +++ b/editor/blackboard_plan_editor.cpp @@ -9,6 +9,8 @@ * ============================================================================= */ +#ifdef TOOLS_ENABLED + #include "blackboard_plan_editor.h" #include "../util/limbo_compat.h" @@ -36,6 +38,8 @@ using namespace godot; #endif // LIMBOAI_GDEXTENSION +BlackboardPlanEditor *BlackboardPlanEditor::singleton = nullptr; + void BlackboardPlanEditor::_add_var() { ERR_FAIL_NULL(plan); @@ -296,11 +300,22 @@ void BlackboardPlanEditor::_notification(int p_what) { ADD_STYLEBOX_OVERRIDE(header_row, LW_NAME(panel), theme_cache.header_style); } break; - case NOTIFICATION_READY: { + case NOTIFICATION_ENTER_TREE: { add_var_tool->connect(LW_NAME(pressed), callable_mp(this, &BlackboardPlanEditor::_add_var)); connect(LW_NAME(visibility_changed), callable_mp(this, &BlackboardPlanEditor::_visibility_changed)); type_menu->connect(LW_NAME(id_pressed), callable_mp(this, &BlackboardPlanEditor::_type_chosen)); hint_menu->connect(LW_NAME(id_pressed), callable_mp(this, &BlackboardPlanEditor::_hint_chosen)); + + for (int i = 0; i < PropertyHint::PROPERTY_HINT_MAX; i++) { + hint_menu->add_item(LimboUtility::get_singleton()->get_property_hint_text(PropertyHint(i)), i); + } + + singleton = this; + } break; + case NOTIFICATION_EXIT_TREE: { + if (singleton == this) { + singleton = nullptr; + } } break; } } @@ -381,9 +396,6 @@ BlackboardPlanEditor::BlackboardPlanEditor() { hint_menu = memnew(PopupMenu); add_child(hint_menu); - for (int i = 0; i < PropertyHint::PROPERTY_HINT_MAX; i++) { - hint_menu->add_item(LimboUtility::get_singleton()->get_property_hint_text(PropertyHint(i)), i); - } theme_cache.odd_style.instantiate(); theme_cache.even_style.instantiate(); @@ -469,3 +481,5 @@ EditorInspectorPluginBBPlan::EditorInspectorPluginBBPlan() { bg.a *= 0.2; toolbar_style->set_bg_color(bg); } + +#endif // TOOLS_ENABLED diff --git a/editor/blackboard_plan_editor.h b/editor/blackboard_plan_editor.h index 0cceb3a..619906f 100644 --- a/editor/blackboard_plan_editor.h +++ b/editor/blackboard_plan_editor.h @@ -12,6 +12,8 @@ #ifndef BLACKBOARD_PLAN_EDITOR_H #define BLACKBOARD_PLAN_EDITOR_H +#ifdef TOOLS_ENABLED + #include "../blackboard/blackboard_plan.h" #ifdef LIMBOAI_MODULE @@ -35,6 +37,9 @@ using namespace godot; class BlackboardPlanEditor : public AcceptDialog { GDCLASS(BlackboardPlanEditor, AcceptDialog); +private: + static BlackboardPlanEditor *singleton; + private: struct ThemeCache { Ref trash_icon; @@ -82,6 +87,8 @@ protected: void _notification(int p_what); public: + _FORCE_INLINE_ static BlackboardPlanEditor *get_singleton() { return singleton; } + void edit_plan(const Ref &p_plan); BlackboardPlanEditor(); @@ -114,4 +121,6 @@ public: EditorInspectorPluginBBPlan(); }; +#endif // TOOLS_ENABLED + #endif // BLACKBOARD_PLAN_EDITOR_H diff --git a/editor/editor_property_variable_name.cpp b/editor/editor_property_variable_name.cpp new file mode 100644 index 0000000..d5a7df6 --- /dev/null +++ b/editor/editor_property_variable_name.cpp @@ -0,0 +1,199 @@ +/** + * editor_property_variable_name.cpp + * ============================================================================= + * Copyright 2021-2024 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. + * ============================================================================= + */ + +#ifdef TOOLS_ENABLED + +#include "editor_property_variable_name.h" + +#include "../blackboard/bb_param/bb_param.h" +#include "../bt/tasks/bt_task.h" +#include "../util/limbo_compat.h" +#include "../util/limbo_string_names.h" +#include "../util/limbo_utility.h" +#include "blackboard_plan_editor.h" + +#ifdef LIMBOAI_MODULE +#include "scene/gui/box_container.h" +#include "scene/gui/button.h" +#include "scene/gui/line_edit.h" +#include "scene/gui/popup_menu.h" +#endif // LIMBOAI_MODULE + +#ifdef LIMBOAI_GDEXTENSION +#include +#endif // LIMBOAI_GDEXTENSION + +//***** EditorPropertyVariableName + +void EditorPropertyVariableName::_show_variables_popup() { + ERR_FAIL_NULL(plan); + + variables_popup->clear(); + variables_popup->reset_size(); + int idx = 0; + for (String var_name : plan->list_vars()) { + variables_popup->add_item(var_name, idx); + idx += 1; + } + + Transform2D xform = name_edit->get_screen_transform(); + Rect2 rect(xform.get_origin(), xform.get_scale() * name_edit->get_size()); + rect.position.y += rect.size.height; + rect.size.height = 0; + variables_popup->set_size(rect.size); + variables_popup->set_position(rect.position); + + variables_popup->popup(rect); +} + +void EditorPropertyVariableName::_name_changed(const String &p_new_name) { + emit_changed(get_edited_property(), p_new_name); + _update_status(); +} + +void EditorPropertyVariableName::_variable_selected(int p_id) { + _name_changed(plan->get_var_by_index(p_id).first); +} + +void EditorPropertyVariableName::_update_status() { + ERR_FAIL_NULL(plan); + if (plan->has_var(name_edit->get_text())) { + BUTTON_SET_ICON(status_btn, theme_cache.var_exists_icon); + status_btn->set_tooltip_text(TTR("This variable exists in the blackboard plan.\n\nClick to open blackboard plan.")); + } else { + BUTTON_SET_ICON(status_btn, theme_cache.var_not_found_icon); + status_btn->set_tooltip_text(TTR("No such variable exists in the blackboard plan!\n\nClick to open blackboard plan.")); + } +} + +void EditorPropertyVariableName::_status_pressed() { + BlackboardPlanEditor::get_singleton()->edit_plan(plan); + BlackboardPlanEditor::get_singleton()->popup_centered(); +} + +void EditorPropertyVariableName::_status_mouse_entered() { + ERR_FAIL_NULL(plan); + if (!plan->has_var(name_edit->get_text())) { + BUTTON_SET_ICON(status_btn, theme_cache.var_add_icon); + } +} + +void EditorPropertyVariableName::_status_mouse_exited() { + ERR_FAIL_NULL(plan); + _update_status(); +} + +#ifdef LIMBOAI_MODULE +void EditorPropertyVariableName::update_property() { +#elif LIMBOAI_GDEXTENSION +void EditorPropertyVariableName::_update_property() { +#endif // LIMBOAI_GDEXTENSION + String s = get_edited_object()->get(get_edited_property()); + if (name_edit->get_text() != s) { + int caret = name_edit->get_caret_column(); + name_edit->set_text(s); + name_edit->set_caret_column(caret); + } + name_edit->set_editable(!is_read_only()); + _update_status(); +} + +void EditorPropertyVariableName::setup(const Ref &p_plan) { + plan = p_plan; +} + +void EditorPropertyVariableName::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_READY: { + name_edit->connect(LW_NAME(text_changed), callable_mp(this, &EditorPropertyVariableName::_name_changed)); + variables_popup->connect(LW_NAME(id_pressed), callable_mp(this, &EditorPropertyVariableName::_variable_selected)); + drop_btn->connect(LW_NAME(pressed), callable_mp(this, &EditorPropertyVariableName::_show_variables_popup)); + status_btn->connect(LW_NAME(pressed), callable_mp(this, &EditorPropertyVariableName::_status_pressed)); + status_btn->connect(LW_NAME(mouse_entered), callable_mp(this, &EditorPropertyVariableName::_status_mouse_entered)); + status_btn->connect(LW_NAME(mouse_exited), callable_mp(this, &EditorPropertyVariableName::_status_mouse_exited)); + } break; + case NOTIFICATION_ENTER_TREE: { + BlackboardPlanEditor::get_singleton()->connect(LW_NAME(visibility_changed), callable_mp(this, &EditorPropertyVariableName::_update_status)); + } break; + case NOTIFICATION_EXIT_TREE: { + if (BlackboardPlanEditor::get_singleton()) { + BlackboardPlanEditor::get_singleton()->disconnect(LW_NAME(visibility_changed), callable_mp(this, &EditorPropertyVariableName::_update_status)); + } + } break; + case NOTIFICATION_THEME_CHANGED: { + BUTTON_SET_ICON(drop_btn, get_theme_icon(LW_NAME(GuiOptionArrow), LW_NAME(EditorIcons))); + theme_cache.var_add_icon = LimboUtility::get_singleton()->get_task_icon(LW_NAME(LimboVarAdd)); + theme_cache.var_exists_icon = LimboUtility::get_singleton()->get_task_icon(LW_NAME(LimboVarExists)); + theme_cache.var_not_found_icon = LimboUtility::get_singleton()->get_task_icon(LW_NAME(LimboVarNotFound)); + } break; + } +} + +EditorPropertyVariableName::EditorPropertyVariableName() { + HBoxContainer *hbox = memnew(HBoxContainer); + add_child(hbox); + hbox->add_theme_constant_override(LW_NAME(separation), 0); + + name_edit = memnew(LineEdit); + hbox->add_child(name_edit); + add_focusable(name_edit); + name_edit->set_h_size_flags(SIZE_EXPAND_FILL); + name_edit->set_placeholder(TTR("Variable name")); + + drop_btn = memnew(Button); + hbox->add_child(drop_btn); + drop_btn->set_flat(true); + drop_btn->set_focus_mode(FOCUS_NONE); + + status_btn = memnew(Button); + hbox->add_child(status_btn); + status_btn->set_flat(true); + status_btn->set_focus_mode(FOCUS_NONE); + + variables_popup = memnew(PopupMenu); + add_child(variables_popup); +} + +//***** EditorInspectorPluginVariableName + +#ifdef LIMBOAI_MODULE +bool EditorInspectorPluginVariableName::can_handle(Object *p_object) { +#elif LIMBOAI_GDEXTENSION +bool EditorInspectorPluginVariableName::_can_handle(Object *p_object) const { +#endif + Ref task = Object::cast_to(p_object); + if (task.is_valid()) { + return true; + } + Ref param = Object::cast_to(p_object); + if (param.is_valid()) { + return true; + } + return false; +} + +#ifdef LIMBOAI_MODULE +bool EditorInspectorPluginVariableName::parse_property(Object *p_object, const Variant::Type p_type, const String &p_path, const PropertyHint p_hint, const String &p_hint_text, const BitField p_usage, const bool p_wide) { +#elif LIMBOAI_GDEXTENSION +bool EditorInspectorPluginVariableName::_parse_property(Object *p_object, const Variant::Type p_type, const String &p_path, const PropertyHint p_hint, const String &p_hint_text, const BitField p_usage, const bool p_wide) { +#endif + if (p_type != Variant::Type::STRING || !(p_path.ends_with("_var") || p_path.ends_with("variable"))) { + return false; + } + + EditorPropertyVariableName *ed = memnew(EditorPropertyVariableName); + ed->setup(plan_getter.call()); + add_property_editor(p_path, ed); + + return true; +} + +#endif // TOOLS_ENABLED diff --git a/editor/editor_property_variable_name.h b/editor/editor_property_variable_name.h new file mode 100644 index 0000000..6a317c3 --- /dev/null +++ b/editor/editor_property_variable_name.h @@ -0,0 +1,99 @@ +/** + * editor_property_variable_name.h + * ============================================================================= + * Copyright 2021-2024 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. + * ============================================================================= + */ + +#ifndef EDITOR_PROPERTY_VARIABLE_NAME_H +#define EDITOR_PROPERTY_VARIABLE_NAME_H + +#ifdef TOOLS_ENABLED + +#include "../blackboard/blackboard_plan.h" + +#ifdef LIMBOAI_MODULE +#include "editor/editor_inspector.h" +#endif // LIMBOAI_MODULE + +#ifdef LIMBOAI_GDEXTENSION +#include +#include +#include +#include +using namespace godot; +#endif // LIMBOAI_GDEXTENSION + +class EditorPropertyVariableName : public EditorProperty { + GDCLASS(EditorPropertyVariableName, EditorProperty); + +private: + struct ThemeCache { + Ref var_exists_icon; + Ref var_not_found_icon; + Ref var_add_icon; + }; + ThemeCache theme_cache; + + Ref plan; + + LineEdit *name_edit; + Button *drop_btn; + Button *status_btn; + PopupMenu *variables_popup; + + void _show_variables_popup(); + void _name_changed(const String &p_new_name); + void _variable_selected(int p_id); + void _update_status(); + + void _status_pressed(); + void _status_mouse_entered(); + void _status_mouse_exited(); + +protected: + static void _bind_methods() {} + + void _notification(int p_what); + +public: +#ifdef LIMBOAI_MODULE + virtual void update_property() override; +#elif LIMBOAI_GDEXTENSION + virtual void _update_property() override; +#endif + + void setup(const Ref &p_plan); + EditorPropertyVariableName(); +}; + +class EditorInspectorPluginVariableName : public EditorInspectorPlugin { + GDCLASS(EditorInspectorPluginVariableName, EditorInspectorPlugin); + +private: + Callable plan_getter; + +protected: + static void _bind_methods() {} + +public: +#ifdef LIMBOAI_MODULE + virtual bool can_handle(Object *p_object) override; + virtual bool parse_property(Object *p_object, const Variant::Type p_type, const String &p_path, const PropertyHint p_hint, const String &p_hint_text, const BitField p_usage, const bool p_wide = false) override; +#elif LIMBOAI_GDEXTENSION + virtual bool _can_handle(Object *p_object) const override; + virtual bool _parse_property(Object *p_object, const Variant::Type p_type, const String &p_path, const PropertyHint p_hint, const String &p_hint_text, const BitField p_usage, const bool p_wide = false) override; +#endif + + void set_plan_getter(const Callable &p_getter) { plan_getter = p_getter; } + + EditorInspectorPluginVariableName() = default; +}; + +#endif // TOOLS_ENABLED + +#endif // EDITOR_PROPERTY_VARIABLE_NAME_H diff --git a/editor/limbo_ai_editor_plugin.cpp b/editor/limbo_ai_editor_plugin.cpp index 63f1796..d2b31ca 100644 --- a/editor/limbo_ai_editor_plugin.cpp +++ b/editor/limbo_ai_editor_plugin.cpp @@ -255,6 +255,13 @@ void LimboAIEditor::edit_bt(Ref p_behavior_tree, bool p_force_refr _update_header(); } +Ref LimboAIEditor::get_edited_blackboard_plan() { + if (task_tree->get_bt().is_valid()) { + return task_tree->get_bt()->get_blackboard_plan(); + } + return nullptr; +} + void LimboAIEditor::_mark_as_dirty(bool p_dirty) { Ref bt = task_tree->get_bt(); if (p_dirty && !dirty.has(bt)) { @@ -1135,6 +1142,7 @@ void LimboAIEditor::_bind_methods() { ClassDB::bind_method(D_METHOD("_resave_modified"), &LimboAIEditor::_resave_modified); ClassDB::bind_method(D_METHOD("_replace_task", "p_task", "p_by_task"), &LimboAIEditor::_replace_task); ClassDB::bind_method(D_METHOD("_popup_file_dialog"), &LimboAIEditor::_popup_file_dialog); + ClassDB::bind_method(D_METHOD("get_edited_blackboard_plan"), &LimboAIEditor::get_edited_blackboard_plan); } LimboAIEditor::LimboAIEditor() { @@ -1418,6 +1426,9 @@ void LimboAIEditorPlugin::_notification(int p_notification) { case NOTIFICATION_READY: { add_debugger_plugin(memnew(LimboDebuggerPlugin)); add_inspector_plugin(memnew(EditorInspectorPluginBBPlan)); + EditorInspectorPluginVariableName *var_plugin = memnew(EditorInspectorPluginVariableName); + var_plugin->set_plan_getter(Callable(limbo_ai_editor, "get_edited_blackboard_plan")); + add_inspector_plugin(var_plugin); #ifdef LIMBOAI_MODULE // ! Only used in the module version. add_inspector_plugin(memnew(EditorInspectorPluginBBParam)); diff --git a/editor/limbo_ai_editor_plugin.h b/editor/limbo_ai_editor_plugin.h index 42305f3..be609e6 100644 --- a/editor/limbo_ai_editor_plugin.h +++ b/editor/limbo_ai_editor_plugin.h @@ -16,6 +16,7 @@ #include "../bt/behavior_tree.h" #include "../bt/tasks/bt_task.h" +#include "editor_property_variable_name.h" #include "task_palette.h" #include "task_tree.h" @@ -211,6 +212,7 @@ protected: public: void set_plugin(EditorPlugin *p_plugin) { plugin = p_plugin; }; void edit_bt(Ref p_behavior_tree, bool p_force_refresh = false); + Ref get_edited_blackboard_plan(); void apply_changes(); diff --git a/gdextension/limboai.gdextension b/gdextension/limboai.gdextension index c71edd6..c217cad 100644 --- a/gdextension/limboai.gdextension +++ b/gdextension/limboai.gdextension @@ -68,6 +68,7 @@ BTTimeLimit = "res://addons/limboai/icons/BTTimeLimit.svg" BTWait = "res://addons/limboai/icons/BTWait.svg" BTWaitTicks = "res://addons/limboai/icons/BTWaitTicks.svg" BehaviorTree = "res://addons/limboai/icons/BehaviorTree.svg" +BlackboardPlan = "res://addons/limboai/icons/BlackboardPlan.svg" LimboAI = "res://addons/limboai/icons/LimboAI.svg" LimboDeselectAll = "res://addons/limboai/icons/LimboDeselectAll.svg" LimboExtraBlackboard = "res://addons/limboai/icons/LimboExtraBlackboard.svg" @@ -79,3 +80,6 @@ LimboPercent = "res://addons/limboai/icons/LimboPercent.svg" LimboSelectAll = "res://addons/limboai/icons/LimboSelectAll.svg" LimboSpecifyValue = "res://addons/limboai/icons/LimboSpecifyValue.svg" LimboState = "res://addons/limboai/icons/LimboState.svg" +LimboVarAdd = "res://addons/limboai/icons/LimboVarAdd.svg" +LimboVarExists = "res://addons/limboai/icons/LimboVarExists.svg" +LimboVarNotFound = "res://addons/limboai/icons/LimboVarNotFound.svg" diff --git a/register_types.cpp b/register_types.cpp index be7be65..8a7a95a 100644 --- a/register_types.cpp +++ b/register_types.cpp @@ -256,9 +256,12 @@ void initialize_limboai_module(ModuleInitializationLevel p_level) { GDREGISTER_CLASS(LimboDebuggerPlugin); GDREGISTER_CLASS(BlackboardPlanEditor); GDREGISTER_CLASS(EditorInspectorPluginBBPlan); + GDREGISTER_CLASS(EditorPropertyVariableName); + GDREGISTER_CLASS(EditorInspectorPluginVariableName); GDREGISTER_CLASS(LimboAIEditor); GDREGISTER_CLASS(LimboAIEditorPlugin); #endif // LIMBOAI_GDEXTENSION + EditorPlugins::add_by_type(); } diff --git a/util/limbo_string_names.cpp b/util/limbo_string_names.cpp index 25b55d1..cc2e155 100644 --- a/util/limbo_string_names.cpp +++ b/util/limbo_string_names.cpp @@ -81,6 +81,7 @@ LimboStringNames::LimboStringNames() { font_size = SN("font_size"); Forward = SN("Forward"); gui_input = SN("gui_input"); + GuiOptionArrow = SN("GuiOptionArrow"); GuiTreeArrowDown = SN("GuiTreeArrowDown"); GuiTreeArrowRight = SN("GuiTreeArrowRight"); HeaderSmall = SN("HeaderSmall"); @@ -94,10 +95,15 @@ LimboStringNames::LimboStringNames() { LimboExtraClock = SN("LimboExtraClock"); LimboPercent = SN("LimboPercent"); LimboSelectAll = SN("LimboSelectAll"); + LimboVarAdd = SN("LimboVarAdd"); + LimboVarExists = SN("LimboVarExists"); + LimboVarNotFound = SN("LimboVarNotFound"); LineEdit = SN("LineEdit"); Load = SN("Load"); managed = SN("managed"); mode_changed = SN("mode_changed"); + mouse_entered = SN("mouse_entered"); + mouse_exited = SN("mouse_exited"); MoveDown = SN("MoveDown"); MoveUp = SN("MoveUp"); New = SN("New"); diff --git a/util/limbo_string_names.h b/util/limbo_string_names.h index bccbafd..8652798 100644 --- a/util/limbo_string_names.h +++ b/util/limbo_string_names.h @@ -95,6 +95,7 @@ public: StringName font; StringName Forward; StringName gui_input; + StringName GuiOptionArrow; StringName GuiTreeArrowDown; StringName GuiTreeArrowRight; StringName HeaderSmall; @@ -109,10 +110,15 @@ public: StringName LimboExtractSubtree; StringName LimboPercent; StringName LimboSelectAll; + StringName LimboVarAdd; + StringName LimboVarExists; + StringName LimboVarNotFound; StringName LineEdit; StringName Load; StringName managed; StringName mode_changed; + StringName mouse_entered; + StringName mouse_exited; StringName MoveDown; StringName MoveUp; StringName New;