Editor: Implement Cut-Copy-Paste

Resolves #14
This commit is contained in:
Serhii Snitsaruk 2024-02-08 17:10:31 +01:00
parent fca8aea99c
commit d38e6f97f1
5 changed files with 101 additions and 31 deletions

View File

@ -76,15 +76,35 @@ Array BTTask::_get_children() const {
}
void BTTask::_set_children(Array p_children) {
data.children.clear();
const int num_children = p_children.size();
int num_null = 0;
data.children.clear();
data.children.resize(num_children);
for (int i = 0; i < num_children; i++) {
Variant task_var = p_children[i];
Ref<BTTask> task_ref = task_var;
task_ref->data.parent = this;
task_ref->data.index = i;
data.children.set(i, task_var);
Ref<BTTask> task = p_children[i];
if (task.is_null()) {
ERR_PRINT("Invalid BTTask reference.");
num_null += 1;
continue;
}
if (task->data.parent != nullptr && task->data.parent != this) {
task = task->clone();
if (task.is_null()) {
// * BTComment::clone() returns nullptr at runtime - we omit those.
num_null += 1;
continue;
}
}
int idx = i - num_null;
task->data.parent = this;
task->data.index = idx;
data.children.set(idx, task);
}
if (num_null > 0) {
data.children.resize(num_children - num_null);
}
}
@ -145,24 +165,8 @@ void BTTask::initialize(Node *p_agent, const Ref<Blackboard> &p_blackboard) {
Ref<BTTask> BTTask::clone() const {
Ref<BTTask> inst = duplicate(false);
inst->data.parent = nullptr;
inst->data.agent = nullptr;
inst->data.blackboard.unref();
int num_null = 0;
for (int i = 0; i < data.children.size(); i++) {
Ref<BTTask> c = get_child(i)->clone();
if (c.is_valid()) {
c->data.parent = inst.ptr();
c->data.index = i;
inst->data.children.set(i - num_null, c);
} else {
num_null += 1;
}
}
if (num_null > 0) {
// * BTComment tasks return nullptr at runtime - we remove those.
inst->data.children.resize(data.children.size() - num_null);
}
// * Children are duplicated via children property. See _set_children().
#ifdef LIMBOAI_MODULE
// Make BBParam properties unique.
@ -279,9 +283,9 @@ void BTTask::add_child_at_index(Ref<BTTask> p_child, int p_idx) {
if (p_idx < 0 || p_idx > data.children.size()) {
p_idx = data.children.size();
}
data.children.insert(p_idx, p_child);
p_child->data.parent = this;
p_child->data.index = p_idx;
data.children.insert(p_idx, p_child);
for (int i = p_idx + 1; i < data.children.size(); i++) {
get_child(i)->data.index = i;
}

View File

@ -76,7 +76,7 @@ _FORCE_INLINE_ String _get_script_template_path() {
return templates_search_path.path_join("BTTask").path_join("custom_task.gd");
}
void LimboAIEditor::_add_task(const Ref<BTTask> &p_task) {
void LimboAIEditor::_add_task(const Ref<BTTask> &p_task, bool p_as_sibling) {
if (task_tree->get_bt().is_null()) {
return;
}
@ -98,8 +98,8 @@ void LimboAIEditor::_add_task(const Ref<BTTask> &p_task) {
undo_redo->add_do_method(task_tree->get_bt().ptr(), LW_NAME(set_root_task), p_task);
undo_redo->add_undo_method(task_tree->get_bt().ptr(), LW_NAME(set_root_task), task_tree->get_bt()->get_root_task());
} else {
if (Input::get_singleton()->is_key_pressed(LW_KEY(SHIFT)) && selected->get_parent().is_valid()) {
// When shift is pressed, insert task after the currently selected and on the same level.
if (p_as_sibling && selected.is_valid() && selected->get_parent().is_valid()) {
// Insert task after the currently selected and on the same level (usually when shift is pressed).
parent = selected->get_parent();
insert_idx = selected->get_index() + 1;
}
@ -113,6 +113,12 @@ void LimboAIEditor::_add_task(const Ref<BTTask> &p_task) {
_mark_as_dirty(true);
}
void LimboAIEditor::_add_task_with_prototype(const Ref<BTTask> &p_prototype) {
Ref<BTTask> selected = task_tree->get_selected();
bool as_sibling = Input::get_singleton()->is_key_pressed(LW_KEY(SHIFT));
_add_task(p_prototype->clone(), as_sibling);
}
Ref<BTTask> LimboAIEditor::_create_task_by_class_or_path(const String &p_class_or_path) const {
Ref<BTTask> ret;
@ -139,7 +145,9 @@ Ref<BTTask> LimboAIEditor::_create_task_by_class_or_path(const String &p_class_o
}
void LimboAIEditor::_add_task_by_class_or_path(const String &p_class_or_path) {
_add_task(_create_task_by_class_or_path(p_class_or_path));
Ref<BTTask> selected = task_tree->get_selected();
bool as_sibling = Input::get_singleton()->is_key_pressed(LW_KEY(SHIFT));
_add_task(_create_task_by_class_or_path(p_class_or_path), as_sibling);
}
void LimboAIEditor::_remove_task(const Ref<BTTask> &p_task) {
@ -368,6 +376,14 @@ void LimboAIEditor::_process_shortcut_input(const Ref<InputEvent> &p_event) {
if (LW_IS_SHORTCUT("limbo_ai/rename_task", p_event)) {
_action_selected(ACTION_RENAME);
} else if (LW_IS_SHORTCUT("limbo_ai/cut_task", p_event)) {
_action_selected(ACTION_CUT);
} else if (LW_IS_SHORTCUT("limbo_ai/copy_task", p_event)) {
_action_selected(ACTION_COPY);
} else if (LW_IS_SHORTCUT("limbo_ai/paste_task", p_event)) {
_action_selected(ACTION_PASTE);
} else if (LW_IS_SHORTCUT("limbo_ai/paste_task_after", p_event)) {
_action_selected(ACTION_PASTE_AFTER);
} else if (LW_IS_SHORTCUT("limbo_ai/move_task_up", p_event)) {
_action_selected(ACTION_MOVE_UP);
} else if (LW_IS_SHORTCUT("limbo_ai/move_task_down", p_event)) {
@ -404,6 +420,14 @@ void LimboAIEditor::_on_tree_rmb(const Vector2 &p_menu_pos) {
menu->add_icon_item(theme_cache.doc_icon, TTR("Open Documentation"), ACTION_OPEN_DOC);
menu->set_item_disabled(menu->get_item_index(ACTION_EDIT_SCRIPT), task->get_script() == Variant());
menu->add_separator();
menu->add_icon_shortcut(theme_cache.cut_icon, LW_GET_SHORTCUT("limbo_ai/cut_task"), ACTION_CUT);
menu->add_icon_shortcut(theme_cache.copy_icon, LW_GET_SHORTCUT("limbo_ai/copy_task"), ACTION_COPY);
menu->add_icon_shortcut(theme_cache.paste_icon, LW_GET_SHORTCUT("limbo_ai/paste_task"), ACTION_PASTE);
menu->add_icon_shortcut(theme_cache.paste_icon, LW_GET_SHORTCUT("limbo_ai/paste_task_after"), ACTION_PASTE_AFTER);
menu->set_item_disabled(ACTION_PASTE, clipboard_task.is_null());
menu->set_item_disabled(ACTION_PASTE_AFTER, clipboard_task.is_null());
menu->add_separator();
menu->add_icon_shortcut(theme_cache.move_task_up_icon, LW_GET_SHORTCUT("limbo_ai/move_task_up"), ACTION_MOVE_UP);
menu->add_icon_shortcut(theme_cache.move_task_down_icon, LW_GET_SHORTCUT("limbo_ai/move_task_down"), ACTION_MOVE_DOWN);
@ -476,6 +500,22 @@ void LimboAIEditor::_action_selected(int p_id) {
LimboUtility::get_singleton()->open_doc_class(help_class);
} break;
case ACTION_COPY: {
Ref<BTTask> sel = task_tree->get_selected();
if (sel.is_valid()) {
clipboard_task = sel->clone();
}
} break;
case ACTION_PASTE: {
if (clipboard_task.is_valid()) {
_add_task(clipboard_task->clone(), false);
}
} break;
case ACTION_PASTE_AFTER: {
if (clipboard_task.is_valid()) {
_add_task(clipboard_task->clone(), true);
}
} break;
case ACTION_MOVE_UP: {
Ref<BTTask> sel = task_tree->get_selected();
if (sel.is_valid() && sel->get_parent().is_valid()) {
@ -554,9 +594,14 @@ void LimboAIEditor::_action_selected(int p_id) {
extract_dialog->popup_centered_ratio();
}
} break;
case ACTION_CUT:
case ACTION_REMOVE: {
Ref<BTTask> sel = task_tree->get_selected();
if (sel.is_valid()) {
if (p_id == ACTION_CUT) {
clipboard_task = sel->clone();
}
undo_redo->create_action(TTR("Remove BT Task"));
if (sel->is_root()) {
undo_redo->add_do_method(task_tree->get_bt().ptr(), LW_NAME(set_root_task), Variant());
@ -1050,6 +1095,9 @@ void LimboAIEditor::_do_update_theme_item_cache() {
theme_cache.remove_task_icon = get_theme_icon(LW_NAME(Remove), LW_NAME(EditorIcons));
theme_cache.rename_task_icon = get_theme_icon(LW_NAME(Rename), LW_NAME(EditorIcons));
theme_cache.change_type_icon = get_theme_icon(LW_NAME(Reload), LW_NAME(EditorIcons));
theme_cache.cut_icon = get_theme_icon(LW_NAME(ActionCut), LW_NAME(EditorIcons));
theme_cache.copy_icon = get_theme_icon(LW_NAME(ActionCopy), LW_NAME(EditorIcons));
theme_cache.paste_icon = get_theme_icon(LW_NAME(ActionPaste), LW_NAME(EditorIcons));
theme_cache.behavior_tree_icon = LimboUtility::get_singleton()->get_task_icon("BehaviorTree");
theme_cache.percent_icon = LimboUtility::get_singleton()->get_task_icon("LimboPercent");
@ -1161,6 +1209,10 @@ LimboAIEditor::LimboAIEditor() {
LW_SHORTCUT("limbo_ai/move_task_down", TTR("Move Down"), (Key)(LW_KEY_MASK(CMD_OR_CTRL) | LW_KEY(DOWN)));
LW_SHORTCUT("limbo_ai/duplicate_task", TTR("Duplicate"), (Key)(LW_KEY_MASK(CMD_OR_CTRL) | LW_KEY(D)));
LW_SHORTCUT("limbo_ai/remove_task", TTR("Remove"), Key::KEY_DELETE);
LW_SHORTCUT("limbo_ai/cut_task", TTR("Cut"), (Key)(LW_KEY_MASK(CMD_OR_CTRL) | LW_KEY(X)));
LW_SHORTCUT("limbo_ai/copy_task", TTR("Copy"), (Key)(LW_KEY_MASK(CMD_OR_CTRL) | LW_KEY(C)));
LW_SHORTCUT("limbo_ai/paste_task", TTR("Paste"), (Key)(LW_KEY_MASK(CMD_OR_CTRL) | LW_KEY(V)));
LW_SHORTCUT("limbo_ai/paste_task_after", TTR("Paste After Selected"), (Key)(LW_KEY_MASK(CMD_OR_CTRL) | LW_KEY_MASK(SHIFT) | LW_KEY(V)));
LW_SHORTCUT("limbo_ai/new_behavior_tree", TTR("New Behavior Tree"), (Key)(LW_KEY_MASK(CMD_OR_CTRL) | LW_KEY_MASK(ALT) | LW_KEY(N)));
LW_SHORTCUT("limbo_ai/save_behavior_tree", TTR("Save Behavior Tree"), (Key)(LW_KEY_MASK(CMD_OR_CTRL) | LW_KEY_MASK(ALT) | LW_KEY(S)));

View File

@ -77,6 +77,10 @@ private:
ACTION_CHANGE_TYPE,
ACTION_EDIT_SCRIPT,
ACTION_OPEN_DOC,
ACTION_CUT,
ACTION_COPY,
ACTION_PASTE,
ACTION_PASTE_AFTER,
ACTION_MOVE_UP,
ACTION_MOVE_DOWN,
ACTION_DUPLICATE,
@ -109,12 +113,16 @@ private:
Ref<Texture2D> change_type_icon;
Ref<Texture2D> extract_subtree_icon;
Ref<Texture2D> behavior_tree_icon;
Ref<Texture2D> cut_icon;
Ref<Texture2D> copy_icon;
Ref<Texture2D> paste_icon;
} theme_cache;
EditorPlugin *plugin;
Vector<Ref<BehaviorTree>> history;
int idx_history;
HashSet<Ref<BehaviorTree>> dirty;
Ref<BTTask> clipboard_task;
VBoxContainer *vbox;
Button *header;
@ -155,11 +163,11 @@ private:
AcceptDialog *info_dialog;
void _add_task(const Ref<BTTask> &p_task);
void _add_task(const Ref<BTTask> &p_task, bool p_as_sibling);
void _add_task_with_prototype(const Ref<BTTask> &p_prototype);
Ref<BTTask> _create_task_by_class_or_path(const String &p_class_or_path) const;
void _add_task_by_class_or_path(const String &p_class_or_path);
void _remove_task(const Ref<BTTask> &p_task);
_FORCE_INLINE_ void _add_task_with_prototype(const Ref<BTTask> &p_prototype) { _add_task(p_prototype->clone()); }
void _update_header() const;
void _update_history_buttons();
void _update_favorite_tasks();

View File

@ -42,6 +42,9 @@ LimboStringNames::LimboStringNames() {
_update_banners = SN("_update_banners");
_weight_ = SN("_weight_");
accent_color = SN("accent_color");
ActionCopy = SN("ActionCopy");
ActionCut = SN("ActionCut");
ActionPaste = SN("ActionPaste");
Add = SN("Add");
add_child = SN("add_child");
add_child_at_index = SN("add_child_at_index");

View File

@ -56,6 +56,9 @@ public:
StringName _update;
StringName _weight_;
StringName accent_color;
StringName ActionCopy;
StringName ActionCut;
StringName ActionPaste;
StringName add_child_at_index;
StringName add_child;
StringName Add;