Compare commits

...

3 Commits

Author SHA1 Message Date
yds a84f154630 Preserve selection after drop 2024-09-11 11:39:28 -03:00
yds 230e481236 Add drag preview 2024-09-11 11:39:28 -03:00
yds 85fb267e3b Implement task multiple selection and drag and drop 2024-09-11 11:39:27 -03:00
6 changed files with 233 additions and 50 deletions

View File

@ -809,7 +809,7 @@ void LimboAIEditor::_on_visibility_changed() {
}
void LimboAIEditor::_on_header_pressed() {
task_tree->deselect();
task_tree->clear_selection();
#ifdef LIMBOAI_MODULE
if (task_tree->get_bt().is_valid()) {
task_tree->get_bt()->editor_set_section_unfold("blackboard_plan", true);
@ -842,35 +842,80 @@ void LimboAIEditor::_on_history_forward() {
EDIT_RESOURCE(history[idx_history]);
}
void LimboAIEditor::_on_task_dragged(Ref<BTTask> p_task, Ref<BTTask> p_to_task, int p_type) {
ERR_FAIL_COND(p_type < -1 || p_type > 1);
ERR_FAIL_COND(p_type != 0 && p_to_task->get_parent().is_null());
void LimboAIEditor::_on_tasks_dragged(const TypedArray<BTTask> &p_tasks, Ref<BTTask> p_to_task, int p_to_pos) {
ERR_FAIL_COND(p_to_task.is_null());
if (p_tasks.is_empty()) {
return;
}
if (p_task == p_to_task) {
// Filter tasks
Vector<Ref<BTTask>> tasks_list;
int no_effect = 0;
for (int i = 0; i < p_tasks.size(); i++) {
Ref<BTTask> task = p_tasks[i];
// Count tasks that don't change position
if (task == p_to_task) {
if (Math::abs(task->get_index() - p_to_pos) <= 1) {
++no_effect;
}
}
// Remove descendants of selected
bool remove = false;
for (int s_idx = 0; s_idx < p_tasks.size(); s_idx++) {
Ref<BTTask> selected = p_tasks[s_idx];
if (task->is_descendant_of(selected)) {
remove = true;
break;
}
}
if (!remove) {
tasks_list.push_back(task);
}
}
if (tasks_list.is_empty() || p_tasks.size() == no_effect) {
return;
}
EditorUndoRedoManager *undo_redo = _new_undo_redo_action(TTR("Drag BT Task"));
undo_redo->add_do_method(p_task->get_parent().ptr(), LW_NAME(remove_child), p_task);
if (p_type == 0) {
undo_redo->add_do_method(p_to_task.ptr(), LW_NAME(add_child), p_task);
undo_redo->add_undo_method(p_to_task.ptr(), LW_NAME(remove_child), p_task);
} else {
int drop_idx = p_to_task->get_index();
if (p_to_task->get_parent() == p_task->get_parent() && drop_idx > p_task->get_index()) {
// Apply changes in the task hierarchy.
int drop_idx = p_to_pos;
for (const Ref<BTTask> &task : tasks_list) {
if (task->get_parent() == p_to_task && drop_idx > task->get_index()) {
drop_idx -= 1;
}
if (p_type == -1) {
undo_redo->add_do_method(p_to_task->get_parent().ptr(), LW_NAME(add_child_at_index), p_task, drop_idx);
undo_redo->add_undo_method(p_to_task->get_parent().ptr(), LW_NAME(remove_child), p_task);
} else if (p_type == 1) {
undo_redo->add_do_method(p_to_task->get_parent().ptr(), LW_NAME(add_child_at_index), p_task, drop_idx + 1);
undo_redo->add_undo_method(p_to_task->get_parent().ptr(), LW_NAME(remove_child), p_task);
if (task == p_to_task) {
if (Math::abs(task->get_index() - p_to_pos) <= 1) {
++drop_idx;
continue;
}
}
undo_redo->add_undo_method(p_task->get_parent().ptr(), "add_child_at_index", p_task, p_task->get_index());
undo_redo->add_do_method(task->get_parent().ptr(), LW_NAME(remove_child), task);
undo_redo->add_do_method(p_to_task.ptr(), LW_NAME(add_child_at_index), task, drop_idx);
undo_redo->add_undo_method(p_to_task.ptr(), LW_NAME(remove_child), task);
++drop_idx;
}
// Readd tasks in later undo action so indexes match the old order.
drop_idx = p_to_pos;
for (const Ref<BTTask> &task : tasks_list) {
if (task->get_parent() == p_to_task && drop_idx > task->get_index()) {
drop_idx -= 1;
}
if (task == p_to_task) {
if (Math::abs(task->get_index() - p_to_pos) <= 1) {
++drop_idx;
continue;
}
}
undo_redo->add_undo_method(task->get_parent().ptr(), LW_NAME(add_child_at_index), task, task->get_index());
++drop_idx;
}
_commit_action_with_update(undo_redo);
}
@ -1383,7 +1428,7 @@ void LimboAIEditor::_notification(int p_what) {
load_btn->connect(LW_NAME(pressed), callable_mp(this, &LimboAIEditor::_popup_file_dialog).bind(load_dialog));
task_tree->connect("rmb_pressed", callable_mp(this, &LimboAIEditor::_on_tree_rmb));
task_tree->connect("task_selected", callable_mp(this, &LimboAIEditor::_on_tree_task_selected));
task_tree->connect("task_dragged", callable_mp(this, &LimboAIEditor::_on_task_dragged));
task_tree->connect("tasks_dragged", callable_mp(this, &LimboAIEditor::_on_tasks_dragged));
task_tree->connect("task_activated", callable_mp(this, &LimboAIEditor::_on_tree_task_activated));
task_tree->connect("probability_clicked", callable_mp(this, &LimboAIEditor::_action_selected).bind(ACTION_EDIT_PROBABILITY));
task_tree->connect("visibility_changed", callable_mp(this, &LimboAIEditor::_on_visibility_changed));

View File

@ -239,7 +239,7 @@ private:
void _on_save_pressed();
void _on_history_back();
void _on_history_forward();
void _on_task_dragged(Ref<BTTask> p_task, Ref<BTTask> p_to_task, int p_type);
void _on_tasks_dragged(const TypedArray<BTTask> &p_tasks, Ref<BTTask> p_to_task, int p_to_pos);
void _on_resources_reload(const PackedStringArray &p_resources);
void _on_filesystem_changed();
void _on_new_script_pressed();

View File

@ -21,11 +21,18 @@
#ifdef LIMBOAI_MODULE
#include "core/object/script_language.h"
#include "editor/themes/editor_scale.h"
#include "scene/gui/box_container.h"
#include "scene/gui/texture_rect.h"
#include "scene/gui/label.h"
#endif // LIMBOAI_MODULE
#ifdef LIMBOAI_GDEXTENSION
#include <godot_cpp/classes/editor_interface.hpp>
#include <godot_cpp/classes/script.hpp>
#include <godot_cpp/classes/h_box_container.hpp>
#include <godot_cpp/classes/v_box_container.hpp>
#include <godot_cpp/classes/texture_rect.hpp>
#include <godot_cpp/classes/label.hpp>
using namespace godot;
#endif // LIMBOAI_GDEXTENSION
@ -101,10 +108,7 @@ void TaskTree::_update_item(TreeItem *p_item) {
}
void TaskTree::_update_tree() {
Ref<BTTask> sel;
if (tree->get_selected()) {
sel = tree->get_selected()->get_metadata(0);
}
Vector<Ref<BTTask>> selection = get_selected_tasks();
tree->clear();
if (bt.is_null()) {
@ -117,9 +121,8 @@ void TaskTree::_update_tree() {
updating_tree = false;
}
TreeItem *item = _find_item(sel);
if (item) {
item->select(0);
for (const Ref<BTTask> &task : selection) {
add_selection(task);
}
}
@ -225,6 +228,22 @@ void TaskTree::update_task(const Ref<BTTask> &p_task) {
}
}
void TaskTree::add_selection(const Ref<BTTask> &p_task) {
ERR_FAIL_COND(p_task.is_null());
TreeItem *item = _find_item(p_task);
if (item) {
item->select(0);
}
}
void TaskTree::remove_selection(const Ref<BTTask> &p_task) {
ERR_FAIL_COND(p_task.is_null());
TreeItem *item = _find_item(p_task);
if (item) {
item->deselect(0);
}
}
Ref<BTTask> TaskTree::get_selected() const {
if (tree->get_selected()) {
return tree->get_selected()->get_metadata(0);
@ -232,10 +251,29 @@ Ref<BTTask> TaskTree::get_selected() const {
return nullptr;
}
void TaskTree::deselect() {
TreeItem *sel = tree->get_selected();
if (sel) {
sel->deselect(0);
Vector<Ref<BTTask>> TaskTree::get_selected_tasks() const {
Vector<Ref<BTTask>> selected_tasks;
TreeItem *next = tree->get_next_selected(nullptr);
while (next) {
Ref<BTTask> task = next->get_metadata(0);
if (task.is_valid()) {
selected_tasks.push_back(task);
}
next = tree->get_next_selected(next);
}
return selected_tasks;
}
void TaskTree::clear_selection() {
Vector<TreeItem*> selected_tasks;
TreeItem *next = tree->get_next_selected(nullptr);
while (next) {
Ref<BTTask> task = next->get_metadata(0);
if (task.is_valid()) {
remove_selection(task);
}
next = tree->get_next_selected(next);
}
}
@ -280,9 +318,50 @@ bool TaskTree::selected_has_probability() const {
Variant TaskTree::_get_drag_data_fw(const Point2 &p_point) {
if (editable && tree->get_item_at_position(p_point)) {
TypedArray<BTTask> selected_tasks;
Vector<Ref<Texture2D>> icons;
TreeItem *next = tree->get_next_selected(nullptr);
while (next) {
Ref<BTTask> task = next->get_metadata(0);
if (task.is_valid()) {
selected_tasks.push_back(task);
icons.push_back(next->get_icon(0));
}
next = tree->get_next_selected(next);
}
if (selected_tasks.is_empty()) {
return Variant();
}
VBoxContainer *vb = memnew(VBoxContainer);
int list_max = 10;
float opacity_step = 1.0f / list_max;
float opacity_item = 1.0f;
for (int i = 0; i < selected_tasks.size(); i++) {
Ref<BTTask> task = Object::cast_to<BTTask>(selected_tasks[i]);
if (i < list_max) {
HBoxContainer *hb = memnew(HBoxContainer);
TextureRect *tf = memnew(TextureRect);
int icon_size = get_theme_constant(LW_NAME(class_icon_size), LW_NAME(Editor));
tf->set_custom_minimum_size(Size2(icon_size, icon_size));
tf->set_stretch_mode(TextureRect::STRETCH_KEEP_ASPECT_CENTERED);
tf->set_expand_mode(TextureRect::EXPAND_IGNORE_SIZE);
tf->set_texture(icons[i]);
hb->add_child(tf);
Label *label = memnew(Label);
label->set_text(task->get_task_name());
hb->add_child(label);
vb->add_child(hb);
hb->set_modulate(Color(1, 1, 1, opacity_item));
opacity_item -= opacity_step;
}
}
set_drag_preview(vb);
Dictionary drag_data;
drag_data["type"] = "task";
drag_data["task"] = tree->get_item_at_position(p_point)->get_metadata(0);
drag_data["tasks"] = selected_tasks;
tree->set_drop_mode_flags(Tree::DROP_MODE_INBETWEEN | Tree::DROP_MODE_ON_ITEM);
return drag_data;
}
@ -295,7 +374,7 @@ bool TaskTree::_can_drop_data_fw(const Point2 &p_point, const Variant &p_data) c
}
Dictionary d = p_data;
if (!d.has("type") || !d.has("task")) {
if (!d.has("type") || !d.has("tasks")) {
return false;
}
@ -305,27 +384,79 @@ bool TaskTree::_can_drop_data_fw(const Point2 &p_point, const Variant &p_data) c
return false;
}
if (!item->get_parent() && section != 0) { // before/after root item
if (!item->get_parent() && section != 0) { // Before/after root item.
return false;
}
if (String(d["type"]) == "task") {
Ref<BTTask> task = d["task"];
TypedArray<BTTask> tasks = d["tasks"];
if (tasks.is_empty()) {
return false; // No tasks.
}
for (int i = 0; i < tasks.size(); i++) {
Ref<BTTask> task = tasks[i];
const Ref<BTTask> to_task = item->get_metadata(0);
if (task != to_task && !to_task->is_descendant_of(task)) {
return true;
if (to_task->is_descendant_of(task) || task == to_task ||
(task == to_task && task->get_index() + section >= to_task->get_index() && !item->is_collapsed() && item->get_child_count() > 0)) {
return false; // Don't drop as child of itself.
}
}
}
return false;
return true;
}
void TaskTree::_drop_data_fw(const Point2 &p_point, const Variant &p_data) {
Dictionary d = p_data;
TreeItem *item = tree->get_item_at_position(p_point);
if (item && d.has("task")) {
Ref<BTTask> task = d["task"];
emit_signal(LW_NAME(task_dragged), task, item->get_metadata(0), tree->get_drop_section_at_position(p_point));
int type = tree->get_drop_section_at_position(p_point);
ERR_FAIL_NULL(item);
ERR_FAIL_COND(type < -1 || type > 1);
if (item && d.has("tasks")) {
TypedArray<BTTask> tasks = d["tasks"];
int to_pos = -1;
Ref<BTTask> to_task = item->get_metadata(0);
ERR_FAIL_COND(to_task.is_null());
// The drop behavior depends on the TreeItem's state.
// Normalize and emit the parent task and position instead of exposing TreeItem.
switch (type) {
case 0: // Drop as last child of target.
to_pos = to_task->get_child_count();
break;
case -1: // Drop above target.
ERR_FAIL_COND_MSG(to_task->get_parent().is_null(), "Cannot perform drop above the root task!");
to_pos = MAX(0, to_task->get_index() - 1);
to_task = to_task->get_parent();
break;
case 1: // Drop below target.
if (item->get_child_count() == 0) {
to_pos = to_task->get_index() + 1;
to_task = to_task->get_parent();
break;
}
if (to_task->get_parent().is_null() || !item->is_collapsed()) { // Insert as first child of target.
to_pos = 0;
} else { // Insert as sibling of target.
TreeItem *lower_sibling = nullptr;
for (int i = to_task->get_index() + 1; i < to_task->get_parent()->get_child_count(); i++) {
TreeItem *c = item->get_parent()->get_child(i);
if (c->is_visible_in_tree()) {
lower_sibling = c;
break;
}
}
if (lower_sibling) {
to_pos = lower_sibling->get_index();
}
to_task = to_task->get_parent();
}
break;
}
emit_signal(LW_NAME(tasks_dragged), tasks, to_task, to_pos);
}
}
@ -385,7 +516,7 @@ void TaskTree::_notification(int p_what) {
case NOTIFICATION_READY: {
tree->connect("item_mouse_selected", callable_mp(this, &TaskTree::_on_item_mouse_selected));
// Note: CONNECT_DEFERRED is needed to avoid double updates with set_allow_reselect(true), which breaks folding/unfolding.
tree->connect("item_selected", callable_mp(this, &TaskTree::_on_item_selected), CONNECT_DEFERRED);
tree->connect("multi_selected", callable_mp(this, &TaskTree::_on_item_selected).unbind(3), CONNECT_DEFERRED);
tree->connect("item_activated", callable_mp(this, &TaskTree::_on_item_activated));
tree->connect("item_collapsed", callable_mp(this, &TaskTree::_on_item_collapsed));
} break;
@ -401,8 +532,10 @@ void TaskTree::_bind_methods() {
ClassDB::bind_method(D_METHOD("get_bt"), &TaskTree::get_bt);
ClassDB::bind_method(D_METHOD("update_tree"), &TaskTree::update_tree);
ClassDB::bind_method(D_METHOD("update_task", "task"), &TaskTree::update_task);
ClassDB::bind_method(D_METHOD("add_selection", "task"), &TaskTree::add_selection);
ClassDB::bind_method(D_METHOD("remove_selection", "task"), &TaskTree::remove_selection);
ClassDB::bind_method(D_METHOD("get_selected"), &TaskTree::get_selected);
ClassDB::bind_method(D_METHOD("deselect"), &TaskTree::deselect);
ClassDB::bind_method(D_METHOD("clear_selection"), &TaskTree::clear_selection);
ClassDB::bind_method(D_METHOD("_get_drag_data_fw"), &TaskTree::_get_drag_data_fw);
ClassDB::bind_method(D_METHOD("_can_drop_data_fw"), &TaskTree::_can_drop_data_fw);
@ -413,8 +546,7 @@ void TaskTree::_bind_methods() {
ADD_SIGNAL(MethodInfo("task_selected"));
ADD_SIGNAL(MethodInfo("task_activated"));
ADD_SIGNAL(MethodInfo("probability_clicked"));
ADD_SIGNAL(MethodInfo("task_dragged",
PropertyInfo(Variant::OBJECT, "task", PROPERTY_HINT_RESOURCE_TYPE, "BTTask"),
ADD_SIGNAL(MethodInfo("tasks_dragged", PropertyInfo(Variant::ARRAY, "tasks", PROPERTY_HINT_ARRAY_TYPE, RESOURCE_TYPE_HINT("BTTask")),
PropertyInfo(Variant::OBJECT, "to_task", PROPERTY_HINT_RESOURCE_TYPE, "BTTask"),
PropertyInfo(Variant::INT, "type")));
}
@ -432,6 +564,7 @@ TaskTree::TaskTree() {
tree->set_anchor(SIDE_BOTTOM, ANCHOR_END);
tree->set_allow_rmb_select(true);
tree->set_allow_reselect(true);
tree->set_select_mode(Tree::SelectMode::SELECT_MULTI);
tree->set_drag_forwarding(callable_mp(this, &TaskTree::_get_drag_data_fw), callable_mp(this, &TaskTree::_can_drop_data_fw), callable_mp(this, &TaskTree::_drop_data_fw));
}

View File

@ -90,8 +90,11 @@ public:
Ref<BehaviorTree> get_bt() const { return bt; }
void update_tree() { _update_tree(); }
void update_task(const Ref<BTTask> &p_task);
void add_selection(const Ref<BTTask> &p_task);
void remove_selection(const Ref<BTTask> &p_task);
Ref<BTTask> get_selected() const;
void deselect();
Vector<Ref<BTTask>> get_selected_tasks() const;
void clear_selection();
Rect2 get_selected_probability_rect() const;
double get_selected_probability_weight() const;

View File

@ -78,6 +78,7 @@ LimboStringNames::LimboStringNames() {
HeaderSmall = SN("HeaderSmall");
Help = SN("Help");
icon_max_width = SN("icon_max_width");
class_icon_size = SN("class_icon_size");
id_pressed = SN("id_pressed");
Info = SN("Info");
item_collapsed = SN("item_collapsed");
@ -127,7 +128,7 @@ LimboStringNames::LimboStringNames() {
task_activated = SN("task_activated");
task_button_pressed = SN("task_button_pressed");
task_button_rmb = SN("task_button_rmb");
task_dragged = SN("task_dragged");
tasks_dragged = SN("tasks_dragged");
task_meta = SN("task_meta");
task_selected = SN("task_selected");
text_changed = SN("text_changed");

View File

@ -94,6 +94,7 @@ public:
StringName HeaderSmall;
StringName Help;
StringName icon_max_width;
StringName class_icon_size;
StringName id_pressed;
StringName Info;
StringName item_collapsed;
@ -143,7 +144,7 @@ public:
StringName task_activated;
StringName task_button_pressed;
StringName task_button_rmb;
StringName task_dragged;
StringName tasks_dragged;
StringName task_meta;
StringName task_selected;
StringName text_changed;