Specialized inspector property editor for NodePath

This commit is contained in:
Serhii Snitsaruk 2024-11-11 08:24:54 +01:00
parent 057d4e669c
commit bc7f677810
No known key found for this signature in database
GPG Key ID: A965EF8799FFEC2D
7 changed files with 344 additions and 2 deletions

View File

@ -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<PropertyInfo> *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));
}
}

View File

@ -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 <godot_cpp/classes/display_server.hpp>
#include <godot_cpp/classes/editor_inspector.hpp>
#include <godot_cpp/classes/editor_interface.hpp>
#include <godot_cpp/classes/editor_selection.hpp>
#include <godot_cpp/classes/h_box_container.hpp>
#include <godot_cpp/classes/popup_menu.hpp>
#include <godot_cpp/classes/scene_tree.hpp>
#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<Node>(p_edited_object);
if (!base_node) {
base_node = Object::cast_to<Node>(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<PropertyUsageFlags> 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<PropertyUsageFlags> 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<String> 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

View File

@ -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 <godot_cpp/classes/button.hpp>
#include <godot_cpp/classes/editor_inspector_plugin.hpp>
#include <godot_cpp/classes/editor_property.hpp>
#include <godot_cpp/classes/line_edit.hpp>
#include <godot_cpp/classes/menu_button.hpp>
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<PropertyUsageFlags> 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<PropertyUsageFlags> p_usage, const bool p_wide = false) override;
#endif
EditorInspectorPluginPropertyPath() = default;
};
#endif // TOOLS_ENABLED
#endif // EDITOR_PROPERTY_PROPERTY_PATH

View File

@ -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);

View File

@ -120,6 +120,7 @@
#endif // LIMBOAI_MODULE
#ifdef LIMBOAI_GDEXTENSION
#include "editor/editor_property_property_path.h"
#include <godot_cpp/classes/engine.hpp>
#include <godot_cpp/core/class_db.hpp>
#include <godot_cpp/core/memory.hpp>
@ -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);

View File

@ -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");

View File

@ -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;