diff --git a/editor/limbo_ai_editor_plugin.cpp b/editor/limbo_ai_editor_plugin.cpp index 960f7e1..2dd901a 100644 --- a/editor/limbo_ai_editor_plugin.cpp +++ b/editor/limbo_ai_editor_plugin.cpp @@ -842,35 +842,80 @@ void LimboAIEditor::_on_history_forward() { EDIT_RESOURCE(history[idx_history]); } -void LimboAIEditor::_on_task_dragged(Ref p_task, Ref 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 &p_tasks, Ref 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> tasks_list; + int no_effect = 0; + for (int i = 0; i < p_tasks.size(); i++) { + Ref 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 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 &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_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 &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); } @@ -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)); diff --git a/editor/limbo_ai_editor_plugin.h b/editor/limbo_ai_editor_plugin.h index 5f53cbf..d2fb61b 100644 --- a/editor/limbo_ai_editor_plugin.h +++ b/editor/limbo_ai_editor_plugin.h @@ -239,7 +239,7 @@ private: void _on_save_pressed(); void _on_history_back(); void _on_history_forward(); - void _on_task_dragged(Ref p_task, Ref p_to_task, int p_type); + void _on_tasks_dragged(const TypedArray &p_tasks, Ref p_to_task, int p_to_pos); void _on_resources_reload(const PackedStringArray &p_resources); void _on_filesystem_changed(); void _on_new_script_pressed(); diff --git a/editor/task_tree.cpp b/editor/task_tree.cpp index 7f60119..ace959b 100644 --- a/editor/task_tree.cpp +++ b/editor/task_tree.cpp @@ -280,9 +280,25 @@ 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 selected_tasks; + Vector> icons; + TreeItem *next = tree->get_next_selected(nullptr); + while (next) { + Ref 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; 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 +311,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 +321,78 @@ 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 task = d["task"]; - const Ref to_task = item->get_metadata(0); - if (task != to_task && !to_task->is_descendant_of(task)) { - return true; + TypedArray tasks = d["tasks"]; + if (tasks.is_empty()) { + return false; // No tasks. + } + for (const Ref &task : tasks) { + const Ref 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) { Dictionary d = p_data; TreeItem *item = tree->get_item_at_position(p_point); - if (item && d.has("task")) { - Ref 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 tasks = d["tasks"]; + int to_pos = -1; + Ref 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: { 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; @@ -413,8 +480,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, MAKE_RESOURCE_TYPE_HINT("BTTask")), PropertyInfo(Variant::OBJECT, "to_task", PROPERTY_HINT_RESOURCE_TYPE, "BTTask"), PropertyInfo(Variant::INT, "type"))); } @@ -432,6 +498,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)); } diff --git a/util/limbo_string_names.cpp b/util/limbo_string_names.cpp index 0f5bab8..c749ccb 100644 --- a/util/limbo_string_names.cpp +++ b/util/limbo_string_names.cpp @@ -127,7 +127,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"); diff --git a/util/limbo_string_names.h b/util/limbo_string_names.h index 21de30e..d3d8014 100644 --- a/util/limbo_string_names.h +++ b/util/limbo_string_names.h @@ -143,7 +143,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;