Implement task multiple selection and drag and drop

This commit is contained in:
yds 2024-09-09 15:19:03 -03:00
parent 1cb85807dd
commit 85fb267e3b
5 changed files with 148 additions and 36 deletions

View File

@ -842,35 +842,80 @@ void LimboAIEditor::_on_history_forward() {
EDIT_RESOURCE(history[idx_history]); EDIT_RESOURCE(history[idx_history]);
} }
void LimboAIEditor::_on_task_dragged(Ref<BTTask> p_task, Ref<BTTask> p_to_task, int p_type) { void LimboAIEditor::_on_tasks_dragged(const TypedArray<BTTask> &p_tasks, Ref<BTTask> p_to_task, int p_to_pos) {
ERR_FAIL_COND(p_type < -1 || p_type > 1); ERR_FAIL_COND(p_to_task.is_null());
ERR_FAIL_COND(p_type != 0 && p_to_task->get_parent().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; return;
} }
EditorUndoRedoManager *undo_redo = _new_undo_redo_action(TTR("Drag BT Task")); 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) { // Apply changes in the task hierarchy.
undo_redo->add_do_method(p_to_task.ptr(), LW_NAME(add_child), p_task); int drop_idx = p_to_pos;
undo_redo->add_undo_method(p_to_task.ptr(), LW_NAME(remove_child), p_task); for (const Ref<BTTask> &task : tasks_list) {
} else { if (task->get_parent() == p_to_task && drop_idx > task->get_index()) {
int drop_idx = p_to_task->get_index();
if (p_to_task->get_parent() == p_task->get_parent() && drop_idx > p_task->get_index()) {
drop_idx -= 1; drop_idx -= 1;
} }
if (p_type == -1) { if (task == p_to_task) {
undo_redo->add_do_method(p_to_task->get_parent().ptr(), LW_NAME(add_child_at_index), p_task, drop_idx); if (Math::abs(task->get_index() - p_to_pos) <= 1) {
undo_redo->add_undo_method(p_to_task->get_parent().ptr(), LW_NAME(remove_child), p_task); ++drop_idx;
} else if (p_type == 1) { continue;
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);
} }
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;
} }
undo_redo->add_undo_method(p_task->get_parent().ptr(), "add_child_at_index", p_task, p_task->get_index()); // 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(), "add_child_at_index", task, task->get_index());
++drop_idx;
}
_commit_action_with_update(undo_redo); _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)); 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("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_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("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("probability_clicked", callable_mp(this, &LimboAIEditor::_action_selected).bind(ACTION_EDIT_PROBABILITY));
task_tree->connect("visibility_changed", callable_mp(this, &LimboAIEditor::_on_visibility_changed)); 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_save_pressed();
void _on_history_back(); void _on_history_back();
void _on_history_forward(); 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_resources_reload(const PackedStringArray &p_resources);
void _on_filesystem_changed(); void _on_filesystem_changed();
void _on_new_script_pressed(); void _on_new_script_pressed();

View File

@ -280,9 +280,25 @@ bool TaskTree::selected_has_probability() const {
Variant TaskTree::_get_drag_data_fw(const Point2 &p_point) { Variant TaskTree::_get_drag_data_fw(const Point2 &p_point) {
if (editable && tree->get_item_at_position(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();
}
Dictionary drag_data; Dictionary drag_data;
drag_data["type"] = "task"; 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); tree->set_drop_mode_flags(Tree::DROP_MODE_INBETWEEN | Tree::DROP_MODE_ON_ITEM);
return drag_data; return drag_data;
} }
@ -295,7 +311,7 @@ bool TaskTree::_can_drop_data_fw(const Point2 &p_point, const Variant &p_data) c
} }
Dictionary d = p_data; Dictionary d = p_data;
if (!d.has("type") || !d.has("task")) { if (!d.has("type") || !d.has("tasks")) {
return false; return false;
} }
@ -305,27 +321,78 @@ bool TaskTree::_can_drop_data_fw(const Point2 &p_point, const Variant &p_data) c
return false; return false;
} }
if (!item->get_parent() && section != 0) { // before/after root item if (!item->get_parent() && section != 0) { // Before/after root item.
return false; return false;
} }
if (String(d["type"]) == "task") { if (String(d["type"]) == "task") {
Ref<BTTask> task = d["task"]; TypedArray<BTTask> tasks = d["tasks"];
const Ref<BTTask> to_task = item->get_metadata(0); if (tasks.is_empty()) {
if (task != to_task && !to_task->is_descendant_of(task)) { return false; // No tasks.
return true; }
for (const Ref<BTTask> &task : tasks) {
const Ref<BTTask> to_task = item->get_metadata(0);
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) { void TaskTree::_drop_data_fw(const Point2 &p_point, const Variant &p_data) {
Dictionary d = p_data; Dictionary d = p_data;
TreeItem *item = tree->get_item_at_position(p_point); TreeItem *item = tree->get_item_at_position(p_point);
if (item && d.has("task")) { int type = tree->get_drop_section_at_position(p_point);
Ref<BTTask> task = d["task"]; ERR_FAIL_NULL(item);
emit_signal(LW_NAME(task_dragged), task, item->get_metadata(0), tree->get_drop_section_at_position(p_point)); 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 +452,7 @@ void TaskTree::_notification(int p_what) {
case NOTIFICATION_READY: { case NOTIFICATION_READY: {
tree->connect("item_mouse_selected", callable_mp(this, &TaskTree::_on_item_mouse_selected)); 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. // 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_activated", callable_mp(this, &TaskTree::_on_item_activated));
tree->connect("item_collapsed", callable_mp(this, &TaskTree::_on_item_collapsed)); tree->connect("item_collapsed", callable_mp(this, &TaskTree::_on_item_collapsed));
} break; } break;
@ -413,8 +480,7 @@ void TaskTree::_bind_methods() {
ADD_SIGNAL(MethodInfo("task_selected")); ADD_SIGNAL(MethodInfo("task_selected"));
ADD_SIGNAL(MethodInfo("task_activated")); ADD_SIGNAL(MethodInfo("task_activated"));
ADD_SIGNAL(MethodInfo("probability_clicked")); ADD_SIGNAL(MethodInfo("probability_clicked"));
ADD_SIGNAL(MethodInfo("task_dragged", ADD_SIGNAL(MethodInfo("tasks_dragged", PropertyInfo(Variant::ARRAY, "tasks", PROPERTY_HINT_ARRAY_TYPE, MAKE_RESOURCE_TYPE_HINT("BTTask")),
PropertyInfo(Variant::OBJECT, "task", PROPERTY_HINT_RESOURCE_TYPE, "BTTask"),
PropertyInfo(Variant::OBJECT, "to_task", PROPERTY_HINT_RESOURCE_TYPE, "BTTask"), PropertyInfo(Variant::OBJECT, "to_task", PROPERTY_HINT_RESOURCE_TYPE, "BTTask"),
PropertyInfo(Variant::INT, "type"))); PropertyInfo(Variant::INT, "type")));
} }
@ -432,6 +498,7 @@ TaskTree::TaskTree() {
tree->set_anchor(SIDE_BOTTOM, ANCHOR_END); tree->set_anchor(SIDE_BOTTOM, ANCHOR_END);
tree->set_allow_rmb_select(true); tree->set_allow_rmb_select(true);
tree->set_allow_reselect(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)); 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

@ -127,7 +127,7 @@ LimboStringNames::LimboStringNames() {
task_activated = SN("task_activated"); task_activated = SN("task_activated");
task_button_pressed = SN("task_button_pressed"); task_button_pressed = SN("task_button_pressed");
task_button_rmb = SN("task_button_rmb"); task_button_rmb = SN("task_button_rmb");
task_dragged = SN("task_dragged"); tasks_dragged = SN("tasks_dragged");
task_meta = SN("task_meta"); task_meta = SN("task_meta");
task_selected = SN("task_selected"); task_selected = SN("task_selected");
text_changed = SN("text_changed"); text_changed = SN("text_changed");

View File

@ -143,7 +143,7 @@ public:
StringName task_activated; StringName task_activated;
StringName task_button_pressed; StringName task_button_pressed;
StringName task_button_rmb; StringName task_button_rmb;
StringName task_dragged; StringName tasks_dragged;
StringName task_meta; StringName task_meta;
StringName task_selected; StringName task_selected;
StringName text_changed; StringName text_changed;