diff --git a/blackboard/blackboard_plan.cpp b/blackboard/blackboard_plan.cpp index 839a172..2a9723c 100644 --- a/blackboard/blackboard_plan.cpp +++ b/blackboard/blackboard_plan.cpp @@ -106,7 +106,10 @@ bool BlackboardPlan::_get(const StringName &p_name, Variant &r_ret) const { if (has_mapping(p_name)) { r_ret = "Mapped to " + LimboUtility::get_singleton()->decorate_var(parent_scope_mapping[p_name]); } else if (has_property_binding(p_name)) { - r_ret = "Bound to " + property_bindings[p_name]; + const NodePath &binding = property_bindings[p_name]; + String path_str = (String)binding.get_name(binding.get_name_count() - 1) + + ":" + (String)binding.get_concatenated_subnames(); + r_ret = "Bound to " + path_str; } else { r_ret = var_map[p_name].get_value(); } @@ -194,7 +197,7 @@ void BlackboardPlan::_get_property_list(List *p_list) const { PropertyUsageFlags usage = has_property_binding(p.first) ? PROPERTY_USAGE_DEFAULT : PROPERTY_USAGE_EDITOR; // PROPERTY_HINT_LINK is used to signal that NodePath should point to a property. // Our inspector plugin will know how to handle it. - p_list->push_back(PropertyInfo(Variant::NODE_PATH, "binding/" + p.first, PROPERTY_HINT_LINK, "", usage)); + p_list->push_back(PropertyInfo(Variant::NODE_PATH, "binding/" + p.first, PROPERTY_HINT_LINK, itos(p.second.get_type()), usage)); } } diff --git a/editor/editor_property_property_path.cpp b/editor/editor_property_property_path.cpp new file mode 100644 index 0000000..e2e2239 --- /dev/null +++ b/editor/editor_property_property_path.cpp @@ -0,0 +1,227 @@ +/** + * editor_property_path.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. + * ============================================================================= + */ + +#include "editor_property_property_path.h" + +#include "../util/limbo_compat.h" +#include "../util/limbo_string_names.h" +#include "core/variant/variant.h" + +#ifdef LIMBOAI_MODULE +#include "editor/editor_data.h" +#include "editor/editor_interface.h" +#include "servers/display_server.h" +#endif + +#ifdef LIMBOAI_GDEXTENSION +#include +#include +#include +#include +#include +#include +#include +#endif // LIMBOAI_MODULE + +#ifdef TOOLS_ENABLED + +namespace { + +Node *_get_base_node(Object *p_edited_object, SceneTree *p_scene_tree) { + Node *base_node = Object::cast_to(p_edited_object); + if (!base_node) { + base_node = Object::cast_to(EditorInterface::get_singleton()->get_inspector()->get_edited_object()); + } + if (!base_node) { + base_node = p_scene_tree->get_edited_scene_root(); + } + return base_node; +} + +} // unnamed namespace + +Node *EditorPropertyPropertyPath::_get_selected_node() { + NodePath path = get_edited_object()->get(get_edited_property()); + if (path.is_empty()) { + return nullptr; + } + + Node *base_node = _get_base_node(get_edited_object(), get_tree()); + Node *selected_node = base_node->get_node_or_null(path); + return selected_node; +} + +void EditorPropertyPropertyPath::_action_selected(int p_idx) { + switch (p_idx) { + case ACTION_CLEAR: { + emit_changed(get_edited_property(), NodePath()); + } break; + case ACTION_COPY: { + DisplayServer::get_singleton()->clipboard_set(get_edited_object()->get(get_edited_property())); + } break; + case ACTION_EDIT: { + assign_button->hide(); + action_menu->hide(); + path_edit->show(); + path_edit->set_text(get_edited_object()->get(get_edited_property())); + path_edit->grab_focus(); + } break; + case ACTION_SELECT: { + Node *selected_node = _get_selected_node(); + if (selected_node) { + EditorInterface::get_singleton()->get_selection()->clear(); + EditorInterface::get_singleton()->get_selection()->add_node(selected_node); + } + } break; + } +} + +void EditorPropertyPropertyPath::_accept_text() { + path_edit->hide(); + assign_button->show(); + action_menu->show(); + emit_changed(get_edited_property(), path_edit->get_text()); +} + +void EditorPropertyPropertyPath::_property_selected(const NodePath &p_property_path, const NodePath &p_node_path) { + if (p_property_path.is_empty()) { + return; + } + Node *base_node = _get_base_node(get_edited_object(), get_tree()); + ERR_FAIL_NULL(base_node); + Node *chosen_node = get_tree()->get_edited_scene_root()->get_node_or_null(p_node_path); + ERR_FAIL_NULL(chosen_node); + NodePath path = String(base_node->get_path_to(chosen_node)) + String(p_property_path); + + emit_changed(get_edited_property(), path); + update_property(); +} + +void EditorPropertyPropertyPath::_node_selected(const NodePath &p_path) { + if (p_path.is_empty()) { + return; + } + Node *selected_node = get_tree()->get_edited_scene_root()->get_node_or_null(p_path); + ERR_FAIL_NULL(selected_node); + EditorInterface::get_singleton()->popup_property_selector( + selected_node, + callable_mp(this, &EditorPropertyPropertyPath::_property_selected).bind(p_path), + valid_types); +} + +void EditorPropertyPropertyPath::_choose_property() { + EditorInterface::get_singleton()->popup_node_selector(callable_mp(this, &EditorPropertyPropertyPath::_node_selected)); +} + +void EditorPropertyPropertyPath::_set_read_only(bool p_read_only) { + assign_button->set_disabled(p_read_only); + action_menu->set_disabled(p_read_only); +} + +#ifdef LIMBOAI_MODULE +void EditorPropertyPropertyPath::update_property() { +#elif LIMBOAI_GDEXTENSION +void EditorPropertyPropertyPath::_update_property() { +#endif + NodePath path = get_edited_object()->get(get_edited_property()); + if (path.is_empty()) { + assign_button->set_text(TTR("Bind...")); + } else { + String text = (String)path.get_name(path.get_name_count() - 1) + + ":" + (String)path.get_concatenated_subnames(); + assign_button->set_text(text); + assign_button->set_tooltip_text(path); + } +} + +void EditorPropertyPropertyPath::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_ENTER_TREE: + case NOTIFICATION_THEME_CHANGED: { + BUTTON_SET_ICON(action_menu, get_theme_icon(LW_NAME(GuiTabMenuHl), LW_NAME(EditorIcons))); + action_menu->get_popup()->set_item_icon(ACTION_CLEAR, get_theme_icon(LW_NAME(Clear), LW_NAME(EditorIcons))); + action_menu->get_popup()->set_item_icon(ACTION_COPY, get_theme_icon(LW_NAME(ActionCopy), LW_NAME(EditorIcons))); + action_menu->get_popup()->set_item_icon(ACTION_EDIT, get_theme_icon(LW_NAME(Edit), LW_NAME(EditorIcons))); + action_menu->get_popup()->set_item_icon(ACTION_SELECT, get_theme_icon(LW_NAME(ExternalLink), LW_NAME(EditorIcons))); + } break; + } +} + +void EditorPropertyPropertyPath::setup(const PackedInt32Array &p_valid_types) { + valid_types = p_valid_types; +} + +EditorPropertyPropertyPath::EditorPropertyPropertyPath() { + HBoxContainer *hb = memnew(HBoxContainer); + add_child(hb); + hb->add_theme_constant_override(LW_NAME(separation), 0); + + assign_button = memnew(Button); + hb->add_child(assign_button); + assign_button->set_flat(true); + assign_button->set_text(TTR("Bind...")); + assign_button->set_clip_text(true); + assign_button->set_h_size_flags(SIZE_EXPAND_FILL); + assign_button->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED); + assign_button->connect(LW_NAME(pressed), callable_mp(this, &EditorPropertyPropertyPath::_choose_property)); + + path_edit = memnew(LineEdit); + hb->add_child(path_edit); + path_edit->set_h_size_flags(SIZE_EXPAND_FILL); + path_edit->connect(LW_NAME(focus_exited), callable_mp(this, &EditorPropertyPropertyPath::_accept_text)); + path_edit->connect(LW_NAME(text_submitted), callable_mp(this, &EditorPropertyPropertyPath::_accept_text).unbind(1)); + path_edit->hide(); + + action_menu = memnew(MenuButton); + action_menu->get_popup()->add_item(TTR("Clear"), ACTION_CLEAR); + action_menu->get_popup()->add_item(TTR("Copy as Text"), ACTION_COPY); + action_menu->get_popup()->add_item(TTR("Edit"), ACTION_EDIT); + action_menu->get_popup()->add_item(TTR("Show Node in Tree"), ACTION_SELECT); + action_menu->get_popup()->connect(LW_NAME(id_pressed), callable_mp(this, &EditorPropertyPropertyPath::_action_selected)); + hb->add_child(action_menu); +} + +//***** EditorInspectorPluginPropertyPath + +#ifdef LIMBOAI_MODULE +bool EditorInspectorPluginPropertyPath::can_handle(Object *p_object) { +#elif LIMBOAI_GDEXTENSION +bool EditorInspectorPluginPropertyPath::_can_handle(Object *p_object) const { +#endif + return true; +} + +#ifdef LIMBOAI_MODULE +bool EditorInspectorPluginPropertyPath::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 EditorInspectorPluginPropertyPath::_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::NODE_PATH || p_hint != PROPERTY_HINT_LINK) { + return false; + } + + EditorPropertyPropertyPath *ed = memnew(EditorPropertyPropertyPath); + + PackedInt32Array valid_types; + Vector type_specifiers = p_hint_text.split(","); + for (const String &t : type_specifiers) { + if (t.is_numeric()) { + valid_types.append(t.to_int()); + } + } + ed->setup(valid_types); + + add_property_editor(p_path, ed); + + return true; +} + +#endif // TOOLS_ENABLED diff --git a/editor/editor_property_property_path.h b/editor/editor_property_property_path.h new file mode 100644 index 0000000..2172511 --- /dev/null +++ b/editor/editor_property_property_path.h @@ -0,0 +1,99 @@ +/** + * editor_property_path.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_PROPERTY_PATH +#define EDITOR_PROPERTY_PROPERTY_PATH + +#include "core/variant/variant.h" +#ifdef TOOLS_ENABLED + +#ifdef LIMBOAI_MODULE +#include "editor/editor_inspector.h" +#include "scene/gui/line_edit.h" +#include "scene/gui/menu_button.h" +#endif // LIMBOAI_MODULE + +#ifdef LIMBOAI_GDEXTENSION +#include +#include +#include +#include +#include +using namespace godot; +#endif // LIMBOAI_GDEXTENSION + +// Specialized property editor for NodePath properties that represent a path to a specific property instead of just a node. +// Handles NodePath properties that have PROPERTY_HINT_LINK. +// Hint string can list the valid Variant types as comma-separated integers. +class EditorPropertyPropertyPath : public EditorProperty { + GDCLASS(EditorPropertyPropertyPath, EditorProperty); + +private: + enum Action { + ACTION_CLEAR, + ACTION_COPY, + ACTION_EDIT, + ACTION_SELECT, + }; + + Button *assign_button; + MenuButton *action_menu; + LineEdit *path_edit; + + PackedInt32Array valid_types; + + Node *_get_selected_node(); + void _action_selected(int p_idx); + void _accept_text(); + void _property_selected(const NodePath &p_property_path, const NodePath &p_node_path); + void _node_selected(const NodePath &p_path); + void _choose_property(); + +protected: + static void _bind_methods() {} + void _notification(int p_what); + +public: + // Note: Needs to be public in GDExtension. + virtual void _set_read_only(bool p_read_only) override; + +#ifdef LIMBOAI_MODULE + virtual void update_property() override; +#elif LIMBOAI_GDEXTENSION + virtual void _update_property() override; +#endif + + void setup(const PackedInt32Array &p_valid_types); + EditorPropertyPropertyPath(); +}; + +class EditorInspectorPluginPropertyPath : public EditorInspectorPlugin { + GDCLASS(EditorInspectorPluginPropertyPath, EditorInspectorPlugin); + +private: +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 + + EditorInspectorPluginPropertyPath() = default; +}; + +#endif // TOOLS_ENABLED + +#endif // EDITOR_PROPERTY_PROPERTY_PATH diff --git a/editor/limbo_ai_editor_plugin.cpp b/editor/limbo_ai_editor_plugin.cpp index d6d59d6..e50b06e 100644 --- a/editor/limbo_ai_editor_plugin.cpp +++ b/editor/limbo_ai_editor_plugin.cpp @@ -25,6 +25,7 @@ #include "blackboard_plan_editor.h" #include "debugger/limbo_debugger_plugin.h" #include "editor_property_bb_param.h" +#include "editor_property_property_path.h" #ifdef LIMBOAI_MODULE #include "core/config/project_settings.h" @@ -1918,9 +1919,13 @@ 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_editor_plan_provider(Callable(limbo_ai_editor, "get_edited_blackboard_plan")); add_inspector_plugin(var_plugin); + + EditorInspectorPluginPropertyPath *path_plugin = memnew(EditorInspectorPluginPropertyPath); + add_inspector_plugin(path_plugin); #ifdef LIMBOAI_MODULE // ! Only used in the module version. EditorInspectorPluginBBParam *param_plugin = memnew(EditorInspectorPluginBBParam); diff --git a/register_types.cpp b/register_types.cpp index 2bcce87..451e072 100644 --- a/register_types.cpp +++ b/register_types.cpp @@ -120,6 +120,7 @@ #endif // LIMBOAI_MODULE #ifdef LIMBOAI_GDEXTENSION +#include "editor/editor_property_property_path.h" #include #include #include @@ -263,6 +264,7 @@ void initialize_limboai_module(ModuleInitializationLevel p_level) { GDREGISTER_CLASS(LimboDebuggerPlugin); GDREGISTER_CLASS(BlackboardPlanEditor); GDREGISTER_CLASS(EditorInspectorPluginBBPlan); + GDREGISTER_CLASS(EditorPropertyPropertyPath); GDREGISTER_CLASS(EditorPropertyVariableName); GDREGISTER_CLASS(EditorInspectorPluginVariableName); GDREGISTER_CLASS(OwnerPicker); diff --git a/util/limbo_string_names.cpp b/util/limbo_string_names.cpp index 22f099a..793a7da 100644 --- a/util/limbo_string_names.cpp +++ b/util/limbo_string_names.cpp @@ -47,6 +47,7 @@ LimboStringNames::LimboStringNames() { button_up = SN("button_up"); call_deferred = SN("call_deferred"); changed = SN("changed"); + Clear = SN("Clear"); Close = SN("Close"); dark_color_2 = SN("dark_color_2"); Debug = SN("Debug"); @@ -67,6 +68,7 @@ LimboStringNames::LimboStringNames() { EVENT_FINISHED = SN("finished"); EVENT_SUCCESS = SN("success"); exited = SN("exited"); + ExternalLink = SN("ExternalLink"); favorite_tasks_changed = SN("favorite_tasks_changed"); Favorites = SN("Favorites"); FlatButton = SN("FlatButton"); @@ -78,6 +80,7 @@ LimboStringNames::LimboStringNames() { freed = SN("freed"); gui_input = SN("gui_input"); GuiOptionArrow = SN("GuiOptionArrow"); + GuiTabMenuHl = SN("GuiTabMenuHl"); GuiTreeArrowDown = SN("GuiTreeArrowDown"); GuiTreeArrowRight = SN("GuiTreeArrowRight"); HeaderSmall = SN("HeaderSmall"); diff --git a/util/limbo_string_names.h b/util/limbo_string_names.h index f6eca76..3b538cd 100644 --- a/util/limbo_string_names.h +++ b/util/limbo_string_names.h @@ -63,6 +63,7 @@ public: StringName button_up; StringName call_deferred; StringName changed; + StringName Clear; StringName Close; StringName dark_color_2; StringName Debug; @@ -83,6 +84,7 @@ public: StringName EVENT_FINISHED; StringName EVENT_SUCCESS; StringName exited; + StringName ExternalLink; StringName favorite_tasks_changed; StringName Favorites; StringName FlatButton; @@ -94,6 +96,7 @@ public: StringName freed; StringName gui_input; StringName GuiOptionArrow; + StringName GuiTabMenuHl; StringName GuiTreeArrowDown; StringName GuiTreeArrowRight; StringName HeaderSmall;