Implement task multiple selection and drag and drop
This commit is contained in:
parent
1cb85807dd
commit
48d1536aeb
|
@ -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());
|
// Re-add 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));
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in New Issue