diff --git a/editor/animation_bezier_editor.cpp b/editor/animation_bezier_editor.cpp index 4849a6824cf..e011f394d06 100644 --- a/editor/animation_bezier_editor.cpp +++ b/editor/animation_bezier_editor.cpp @@ -834,16 +834,22 @@ void AnimationBezierTrackEdit::gui_input(const Ref &p_event) { } if (p_event->is_pressed()) { - if (ED_GET_SHORTCUT("animation_editor/duplicate_selection")->matches_event(p_event)) { + if (ED_GET_SHORTCUT("animation_editor/duplicate_selected_keys")->matches_event(p_event)) { if (!read_only) { - duplicate_selection(); + duplicate_selected_keys(-1.0); + } + accept_event(); + } + if (ED_GET_SHORTCUT("animation_editor/copy_selected_keys")->matches_event(p_event)) { + if (!read_only) { + copy_selected_keys(); } accept_event(); } - if (ED_GET_SHORTCUT("animation_editor/delete_selection")->matches_event(p_event)) { + if (ED_GET_SHORTCUT("animation_editor/paste_keys")->matches_event(p_event)) { if (!read_only) { - delete_selection(); + paste_keys(-1.0); } accept_event(); } @@ -946,11 +952,21 @@ void AnimationBezierTrackEdit::gui_input(const Ref &p_event) { if (!read_only) { Vector2 popup_pos = get_screen_position() + mb->get_position(); + bool selected = _try_select_at_ui_pos(mb->get_position(), mb->is_shift_pressed(), false); + menu->clear(); menu->add_icon_item(bezier_icon, TTR("Insert Key Here"), MENU_KEY_INSERT); - if (selection.size()) { + if (selected || selection.size()) { menu->add_separator(); menu->add_icon_item(get_editor_theme_icon(SNAME("Duplicate")), TTR("Duplicate Selected Key(s)"), MENU_KEY_DUPLICATE); + menu->add_icon_item(get_editor_theme_icon(SNAME("ActionCopy")), TTR("Copy Selected Key(s)"), MENU_KEY_COPY); + } + + if (editor->is_key_clipboard_active()) { + menu->add_icon_item(get_editor_theme_icon(SNAME("ActionPaste")), TTR("Paste Key(s)"), MENU_KEY_PASTE); + } + + if (selected || selection.size()) { menu->add_separator(); menu->add_icon_item(get_editor_theme_icon(SNAME("Remove")), TTR("Delete Selected Key(s)"), MENU_KEY_DELETE); menu->add_separator(); @@ -1092,50 +1108,16 @@ void AnimationBezierTrackEdit::gui_input(const Ref &p_event) { } } - for (int i = 0; i < edit_points.size(); i++) { - //first check point - //command makes it ignore the main point, so control point editors can be force-edited - //path 2D editing in the 3D and 2D editors works the same way - if (!mb->is_command_or_control_pressed()) { - if (edit_points[i].point_rect.has_point(mb->get_position())) { - IntPair pair = IntPair(edit_points[i].track, edit_points[i].key); - if (mb->is_shift_pressed()) { - //add to selection - if (selection.has(pair)) { - selection.erase(pair); - } else { - selection.insert(pair); - } - queue_redraw(); - select_single_attempt = IntPair(-1, -1); - } else if (selection.has(pair)) { - moving_selection_attempt = true; - moving_selection = false; - moving_selection_from_key = pair.second; - moving_selection_from_track = pair.first; - moving_handle_track = pair.first; - moving_handle_left = animation->bezier_track_get_key_in_handle(pair.first, pair.second); - moving_handle_right = animation->bezier_track_get_key_out_handle(pair.first, pair.second); - moving_selection_offset = Vector2(); - select_single_attempt = pair; - queue_redraw(); - } else { - moving_selection_attempt = true; - moving_selection = true; - moving_selection_from_key = pair.second; - moving_selection_from_track = pair.first; - moving_selection_offset = Vector2(); - moving_handle_track = pair.first; - moving_handle_left = animation->bezier_track_get_key_in_handle(pair.first, pair.second); - moving_handle_right = animation->bezier_track_get_key_out_handle(pair.first, pair.second); - selection.clear(); - selection.insert(pair); - set_animation_and_track(animation, pair.first, read_only); - } - return; - } + // First, check keyframe. + // Command/Control makes it ignore the keyframe, so control point editors can be force-edited. + if (!mb->is_command_or_control_pressed()) { + if (_try_select_at_ui_pos(mb->get_position(), mb->is_shift_pressed(), true)) { + return; } + } + // Second, check handles. + for (int i = 0; i < edit_points.size(); i++) { if (!read_only) { if (edit_points[i].in_rect.has_point(mb->get_position())) { moving_handle = -1; @@ -1494,6 +1476,52 @@ void AnimationBezierTrackEdit::gui_input(const Ref &p_event) { } } +bool AnimationBezierTrackEdit::_try_select_at_ui_pos(const Point2 &p_pos, bool p_aggregate, bool p_deselectable) { + for (int i = 0; i < edit_points.size(); i++) { + // Path 2D editing in the 3D and 2D editors works the same way. (?) + if (edit_points[i].point_rect.has_point(p_pos)) { + IntPair pair = IntPair(edit_points[i].track, edit_points[i].key); + if (p_aggregate) { + // Add to selection. + if (selection.has(pair)) { + if (p_deselectable) { + selection.erase(pair); + } + } else { + selection.insert(pair); + } + queue_redraw(); + select_single_attempt = IntPair(-1, -1); + } else { + if (p_deselectable) { + moving_selection_attempt = true; + moving_selection_from_key = pair.second; + moving_selection_from_track = pair.first; + moving_selection_offset = Vector2(); + moving_handle_track = pair.first; + moving_handle_left = animation->bezier_track_get_key_in_handle(pair.first, pair.second); + moving_handle_right = animation->bezier_track_get_key_out_handle(pair.first, pair.second); + + if (selection.has(pair)) { + select_single_attempt = pair; + moving_selection = false; + } else { + moving_selection = true; + } + } + + set_animation_and_track(animation, pair.first, read_only); + if (p_deselectable || !selection.has(pair)) { + selection.clear(); + selection.insert(pair); + } + } + return true; + } + } + return false; +} + void AnimationBezierTrackEdit::_pan_callback(Vector2 p_scroll_vec, Ref p_event) { Ref mm = p_event; if (mm.is_valid()) { @@ -1526,29 +1554,37 @@ void AnimationBezierTrackEdit::_zoom_callback(float p_zoom_factor, Vector2 p_ori queue_redraw(); } +Array AnimationBezierTrackEdit::make_default_bezier_key(float p_value) { + Array new_point; + new_point.resize(5); + + new_point[0] = p_value; + new_point[1] = -0.25; + new_point[2] = 0; + new_point[3] = 0.25; + new_point[4] = 0; + + return new_point; +} + +float AnimationBezierTrackEdit::get_bezier_key_value(Array p_bezier_key_array) { + return p_bezier_key_array[0]; +} + void AnimationBezierTrackEdit::_menu_selected(int p_index) { + int limit = timeline->get_name_limit(); + + real_t time = ((menu_insert_key.x - limit) / timeline->get_zoom_scale()) + timeline->get_value(); + + while (animation->track_find_key(selected_track, time, Animation::FIND_MODE_APPROX) != -1) { + time += 0.001; + } + switch (p_index) { case MENU_KEY_INSERT: { if (animation->get_track_count() > 0) { - Array new_point; - new_point.resize(5); - float h = (get_size().height / 2.0 - menu_insert_key.y) * timeline_v_zoom + timeline_v_scroll; - - new_point[0] = h; - new_point[1] = -0.25; - new_point[2] = 0; - new_point[3] = 0.25; - new_point[4] = 0; - - int limit = timeline->get_name_limit(); - - real_t time = ((menu_insert_key.x - limit) / timeline->get_zoom_scale()) + timeline->get_value(); - - while (animation->track_find_key(selected_track, time, Animation::FIND_MODE_APPROX) != -1) { - time += 0.001; - } - + Array new_point = make_default_bezier_key(h); EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); undo_redo->create_action(TTR("Add Bezier Point")); undo_redo->add_do_method(animation.ptr(), "track_insert_key", selected_track, time, new_point); @@ -1558,11 +1594,17 @@ void AnimationBezierTrackEdit::_menu_selected(int p_index) { } } break; case MENU_KEY_DUPLICATE: { - duplicate_selection(); + duplicate_selected_keys(time); } break; case MENU_KEY_DELETE: { delete_selection(); } break; + case MENU_KEY_COPY: { + copy_selected_keys(); + } break; + case MENU_KEY_PASTE: { + paste_keys(time); + } break; case MENU_KEY_SET_HANDLE_FREE: { _change_selected_keys_handle_mode(Animation::HANDLE_MODE_FREE); } break; @@ -1584,7 +1626,7 @@ void AnimationBezierTrackEdit::_menu_selected(int p_index) { } } -void AnimationBezierTrackEdit::duplicate_selection() { +void AnimationBezierTrackEdit::duplicate_selected_keys(real_t p_ofs) { if (selection.size() == 0) { return; } @@ -1604,7 +1646,8 @@ void AnimationBezierTrackEdit::duplicate_selection() { for (SelectionSet::Element *E = selection.back(); E; E = E->prev()) { real_t t = animation->track_get_key_time(E->get().first, E->get().second); - real_t dst_time = t + (timeline->get_play_position() - top_time); + real_t insert_pos = p_ofs >= 0 ? p_ofs : timeline->get_play_position(); + real_t dst_time = t + (insert_pos - top_time); int existing_idx = animation->track_find_key(E->get().first, dst_time, Animation::FIND_MODE_APPROX); undo_redo->add_do_method(animation.ptr(), "track_insert_key", E->get().first, dst_time, animation->track_get_key_value(E->get().first, E->get().second), animation->track_get_key_transition(E->get().first, E->get().second)); @@ -1620,25 +1663,117 @@ void AnimationBezierTrackEdit::duplicate_selection() { } } - undo_redo->commit_action(); + undo_redo->add_do_method(this, "_clear_selection_for_anim", animation); + undo_redo->add_undo_method(this, "_clear_selection_for_anim", animation); - //reselect duplicated - - selection.clear(); + // Reselect duplicated. for (const Pair &E : new_selection_values) { - int track = E.first; - real_t time = E.second; - - int existing_idx = animation->track_find_key(track, time, Animation::FIND_MODE_APPROX); - - if (existing_idx == -1) { - continue; - } - - selection.insert(IntPair(track, existing_idx)); + undo_redo->add_do_method(this, "_select_at_anim", animation, E.first, E.second); + } + for (SelectionSet::Element *E = selection.back(); E; E = E->prev()) { + real_t time = animation->track_get_key_time(E->get().first, E->get().second); + undo_redo->add_undo_method(this, "_select_at_anim", animation, E->get().first, time); } - queue_redraw(); + undo_redo->add_do_method(this, "queue_redraw"); + undo_redo->add_undo_method(this, "queue_redraw"); + undo_redo->commit_action(); +} + +void AnimationBezierTrackEdit::copy_selected_keys() { + if (selection.is_empty()) { + return; + } + + float top_time = 1e10; + for (SelectionSet::Element *E = selection.back(); E; E = E->prev()) { + float t = animation->track_get_key_time(E->get().first, E->get().second); + if (t < top_time) { + top_time = t; + } + } + + RBMap keys; + for (SelectionSet::Element *E = selection.back(); E; E = E->prev()) { + AnimationTrackEditor::SelectedKey sk; + AnimationTrackEditor::KeyInfo ki; + sk.track = E->get().first; + sk.key = E->get().second; + ki.pos = animation->track_get_key_time(E->get().first, E->get().second); + keys.insert(sk, ki); + } + editor->_set_key_clipboard(selected_track, top_time, keys); +} + +void AnimationBezierTrackEdit::paste_keys(real_t p_ofs) { + if (editor->is_key_clipboard_active() && animation.is_valid() && (selected_track >= 0 && selected_track < animation->get_track_count())) { + EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); + undo_redo->create_action(TTR("Animation Paste Keys")); + + bool same_track = true; + bool all_compatible = true; + + for (int i = 0; i < editor->key_clipboard.keys.size(); i++) { + const AnimationTrackEditor::KeyClipboard::Key key = editor->key_clipboard.keys[i]; + + if (key.track != 0) { + same_track = false; + break; + } + + if (!editor->_is_track_compatible(selected_track, key.value.get_type(), key.track_type)) { + all_compatible = false; + break; + } + } + + ERR_FAIL_COND_MSG(!all_compatible, "Paste failed: Not all animation keys were compatible with their target tracks"); + if (!same_track) { + WARN_PRINT("Pasted animation keys from multiple tracks into single Bezier track"); + } + + List> new_selection_values; + for (int i = 0; i < editor->key_clipboard.keys.size(); i++) { + const AnimationTrackEditor::KeyClipboard::Key key = editor->key_clipboard.keys[i]; + + float insert_pos = p_ofs >= 0 ? p_ofs : timeline->get_play_position(); + float dst_time = key.time + insert_pos; + + int existing_idx = animation->track_find_key(selected_track, dst_time, Animation::FIND_MODE_APPROX); + + Variant value = key.value; + if (key.track_type != Animation::TYPE_BEZIER) { + value = make_default_bezier_key(key.value); + } + + undo_redo->add_do_method(animation.ptr(), "track_insert_key", selected_track, dst_time, value, key.transition); + undo_redo->add_undo_method(animation.ptr(), "track_remove_key_at_time", selected_track, dst_time); + + Pair p; + p.first = selected_track; + p.second = dst_time; + new_selection_values.push_back(p); + + if (existing_idx != -1) { + undo_redo->add_undo_method(animation.ptr(), "track_insert_key", selected_track, dst_time, animation->track_get_key_value(selected_track, existing_idx), animation->track_get_key_transition(selected_track, existing_idx)); + } + } + + undo_redo->add_do_method(this, "_clear_selection_for_anim", animation); + undo_redo->add_undo_method(this, "_clear_selection_for_anim", animation); + + // Reselect pasted. + for (const Pair &E : new_selection_values) { + undo_redo->add_do_method(this, "_select_at_anim", animation, E.first, E.second); + } + for (SelectionSet::Element *E = selection.back(); E; E = E->prev()) { + undo_redo->add_undo_method(this, "_select_at_anim", animation, E->get().first, animation->track_get_key_time(E->get().first, E->get().second)); + } + + undo_redo->commit_action(); + + queue_redraw(); + } } void AnimationBezierTrackEdit::delete_selection() { diff --git a/editor/animation_bezier_editor.h b/editor/animation_bezier_editor.h index 4952869943c..109ba0d7808 100644 --- a/editor/animation_bezier_editor.h +++ b/editor/animation_bezier_editor.h @@ -42,6 +42,8 @@ class AnimationBezierTrackEdit : public Control { enum { MENU_KEY_INSERT, MENU_KEY_DUPLICATE, + MENU_KEY_COPY, + MENU_KEY_PASTE, MENU_KEY_DELETE, MENU_KEY_SET_HANDLE_FREE, MENU_KEY_SET_HANDLE_LINEAR, @@ -139,6 +141,7 @@ class AnimationBezierTrackEdit : public Control { void _clear_selection(); void _clear_selection_for_anim(const Ref &p_anim); void _select_at_anim(const Ref &p_anim, int p_track, real_t p_pos); + bool _try_select_at_ui_pos(const Point2 &p_pos, bool p_aggregate, bool p_deselectable); void _change_selected_keys_handle_mode(Animation::HandleMode p_mode, bool p_auto = false); Vector2 menu_insert_key; @@ -190,6 +193,9 @@ protected: void _notification(int p_what); public: + static Array make_default_bezier_key(float p_value); + static float get_bezier_key_value(Array p_bezier_key_array); + virtual String get_tooltip(const Point2 &p_pos) const override; Ref get_animation() const; @@ -205,7 +211,9 @@ public: void set_play_position(real_t p_pos); void update_play_position(); - void duplicate_selection(); + void duplicate_selected_keys(real_t p_ofs); + void copy_selected_keys(); + void paste_keys(real_t p_ofs); void delete_selection(); void _bezier_track_insert_key(int p_track, double p_time, real_t p_value, const Vector2 &p_in_handle, const Vector2 &p_out_handle, const Animation::HandleMode p_handle_mode); diff --git a/editor/animation_track_editor.cpp b/editor/animation_track_editor.cpp index 41a6d778601..6144dd19ca3 100644 --- a/editor/animation_track_editor.cpp +++ b/editor/animation_track_editor.cpp @@ -2685,16 +2685,22 @@ void AnimationTrackEdit::gui_input(const Ref &p_event) { ERR_FAIL_COND(p_event.is_null()); if (p_event->is_pressed()) { - if (ED_GET_SHORTCUT("animation_editor/duplicate_selection")->matches_event(p_event)) { + if (ED_GET_SHORTCUT("animation_editor/duplicate_selected_keys")->matches_event(p_event)) { if (!read_only) { - emit_signal(SNAME("duplicate_request")); + emit_signal(SNAME("duplicate_request"), -1.0); + } + accept_event(); + } + if (ED_GET_SHORTCUT("animation_editor/copy_selected_keys")->matches_event(p_event)) { + if (!read_only) { + emit_signal(SNAME("copy_request")); } accept_event(); } - if (ED_GET_SHORTCUT("animation_editor/duplicate_selection_transposed")->matches_event(p_event)) { + if (ED_GET_SHORTCUT("animation_editor/paste_keys")->matches_event(p_event)) { if (!read_only) { - emit_signal(SNAME("duplicate_transpose_request")); + emit_signal(SNAME("paste_request"), -1.0); } accept_event(); } @@ -2821,71 +2827,8 @@ void AnimationTrackEdit::gui_input(const Ref &p_event) { } } - // Check keyframes. - - if (!animation->track_is_compressed(track)) { // Selecting compressed keyframes for editing is not possible. - - float scale = timeline->get_zoom_scale(); - int limit = timeline->get_name_limit(); - int limit_end = get_size().width - timeline->get_buttons_width(); - // Left Border including space occupied by keyframes on t=0. - int limit_start_hitbox = limit - type_icon->get_width(); - - if (pos.x >= limit_start_hitbox && pos.x <= limit_end) { - int key_idx = -1; - float key_distance = 1e20; - - // Select should happen in the opposite order of drawing for more accurate overlap select. - for (int i = animation->track_get_key_count(track) - 1; i >= 0; i--) { - Rect2 rect = get_key_rect(i, scale); - float offset = animation->track_get_key_time(track, i) - timeline->get_value(); - offset = offset * scale + limit; - rect.position.x += offset; - - if (rect.has_point(pos)) { - if (is_key_selectable_by_distance()) { - float distance = ABS(offset - pos.x); - if (key_idx == -1 || distance < key_distance) { - key_idx = i; - key_distance = distance; - } - } else { - // First one does it. - key_idx = i; - break; - } - } - } - - if (key_idx != -1) { - if (mb->is_command_or_control_pressed() || mb->is_shift_pressed()) { - if (editor->is_key_selected(track, key_idx)) { - emit_signal(SNAME("deselect_key"), key_idx); - } else { - emit_signal(SNAME("select_key"), key_idx, false); - moving_selection_attempt = true; - select_single_attempt = -1; - moving_selection_from_ofs = (mb->get_position().x - limit) / timeline->get_zoom_scale(); - } - } else { - if (!editor->is_key_selected(track, key_idx)) { - emit_signal(SNAME("select_key"), key_idx, true); - select_single_attempt = -1; - } else { - select_single_attempt = key_idx; - } - - moving_selection_attempt = true; - moving_selection_from_ofs = (mb->get_position().x - limit) / timeline->get_zoom_scale(); - } - - if (read_only) { - moving_selection_attempt = false; - moving_selection_from_ofs = 0.0f; - } - accept_event(); - } - } + if (_try_select_at_ui_pos(pos, mb->is_command_or_control_pressed() || mb->is_shift_pressed(), true)) { + accept_event(); } } @@ -2901,12 +2844,19 @@ void AnimationTrackEdit::gui_input(const Ref &p_event) { menu->connect("id_pressed", callable_mp(this, &AnimationTrackEdit::_menu_selected)); } + bool selected = _try_select_at_ui_pos(pos, mb->is_command_or_control_pressed() || mb->is_shift_pressed(), false); + menu->clear(); menu->add_icon_item(get_editor_theme_icon(SNAME("Key")), TTR("Insert Key"), MENU_KEY_INSERT); - if (editor->is_selection_active()) { + if (selected || editor->is_selection_active()) { menu->add_separator(); menu->add_icon_item(get_editor_theme_icon(SNAME("Duplicate")), TTR("Duplicate Key(s)"), MENU_KEY_DUPLICATE); - + menu->add_icon_item(get_editor_theme_icon(SNAME("ActionCopy")), TTR("Copy Key(s)"), MENU_KEY_COPY); + } + if (editor->is_key_clipboard_active()) { + menu->add_icon_item(get_editor_theme_icon(SNAME("ActionPaste")), TTR("Paste Key(s)"), MENU_KEY_PASTE); + } + if (selected || editor->is_selection_active()) { AnimationPlayer *player = AnimationPlayerEditor::get_singleton()->get_player(); if (!player->has_animation(SceneStringNames::get_singleton()->RESET) || animation != player->get_animation(SceneStringNames::get_singleton()->RESET)) { menu->add_icon_item(get_editor_theme_icon(SNAME("Reload")), TTR("Add RESET Value(s)"), MENU_KEY_ADD_RESET); @@ -3029,6 +2979,75 @@ void AnimationTrackEdit::gui_input(const Ref &p_event) { } } +bool AnimationTrackEdit::_try_select_at_ui_pos(const Point2 &p_pos, bool p_aggregate, bool p_deselectable) { + if (!animation->track_is_compressed(track)) { // Selecting compressed keyframes for editing is not possible. + float scale = timeline->get_zoom_scale(); + int limit = timeline->get_name_limit(); + int limit_end = get_size().width - timeline->get_buttons_width(); + // Left Border including space occupied by keyframes on t=0. + int limit_start_hitbox = limit - type_icon->get_width(); + + if (p_pos.x >= limit_start_hitbox && p_pos.x <= limit_end) { + int key_idx = -1; + float key_distance = 1e20; + + // Select should happen in the opposite order of drawing for more accurate overlap select. + for (int i = animation->track_get_key_count(track) - 1; i >= 0; i--) { + Rect2 rect = get_key_rect(i, scale); + float offset = animation->track_get_key_time(track, i) - timeline->get_value(); + offset = offset * scale + limit; + rect.position.x += offset; + + if (rect.has_point(p_pos)) { + if (is_key_selectable_by_distance()) { + float distance = ABS(offset - p_pos.x); + if (key_idx == -1 || distance < key_distance) { + key_idx = i; + key_distance = distance; + } + } else { + // First one does it. + key_idx = i; + break; + } + } + } + + if (key_idx != -1) { + if (p_aggregate) { + if (editor->is_key_selected(track, key_idx)) { + if (p_deselectable) { + emit_signal(SNAME("deselect_key"), key_idx); + } + } else { + emit_signal(SNAME("select_key"), key_idx, false); + moving_selection_attempt = true; + select_single_attempt = -1; + moving_selection_from_ofs = (p_pos.x - limit) / timeline->get_zoom_scale(); + } + } else { + if (!editor->is_key_selected(track, key_idx)) { + emit_signal(SNAME("select_key"), key_idx, true); + select_single_attempt = -1; + } else { + select_single_attempt = key_idx; + } + + moving_selection_attempt = true; + moving_selection_from_ofs = (p_pos.x - limit) / timeline->get_zoom_scale(); + } + + if (read_only) { + moving_selection_attempt = false; + moving_selection_from_ofs = 0.0f; + } + return true; + } + } + } + return false; +} + Variant AnimationTrackEdit::get_drag_data(const Point2 &p_point) { if (!clicking_on_name) { return Variant(); @@ -3154,7 +3173,14 @@ void AnimationTrackEdit::_menu_selected(int p_index) { emit_signal(SNAME("insert_key"), insert_at_pos); } break; case MENU_KEY_DUPLICATE: { - emit_signal(SNAME("duplicate_request")); + emit_signal(SNAME("duplicate_request"), insert_at_pos); + } break; + case MENU_KEY_COPY: { + emit_signal(SNAME("copy_request")); + } break; + + case MENU_KEY_PASTE: { + emit_signal(SNAME("paste_request"), insert_at_pos); } break; case MENU_KEY_ADD_RESET: { emit_signal(SNAME("create_reset_request")); @@ -3228,9 +3254,10 @@ void AnimationTrackEdit::_bind_methods() { ADD_SIGNAL(MethodInfo("move_selection_commit")); ADD_SIGNAL(MethodInfo("move_selection_cancel")); - ADD_SIGNAL(MethodInfo("duplicate_request")); + ADD_SIGNAL(MethodInfo("duplicate_request", PropertyInfo(Variant::FLOAT, "offset"))); ADD_SIGNAL(MethodInfo("create_reset_request")); - ADD_SIGNAL(MethodInfo("duplicate_transpose_request")); + ADD_SIGNAL(MethodInfo("copy_request")); + ADD_SIGNAL(MethodInfo("paste_request", PropertyInfo(Variant::FLOAT, "offset"))); ADD_SIGNAL(MethodInfo("delete_request")); } @@ -4383,6 +4410,10 @@ bool AnimationTrackEditor::is_selection_active() const { return selection.size(); } +bool AnimationTrackEditor::is_key_clipboard_active() const { + return key_clipboard.keys.size(); +} + bool AnimationTrackEditor::is_snap_enabled() const { return snap->is_pressed() ^ Input::get_singleton()->is_key_pressed(Key::CMD_OR_CTRL); } @@ -4570,8 +4601,9 @@ void AnimationTrackEditor::_update_tracks() { track_edit->connect("move_selection_commit", callable_mp(this, &AnimationTrackEditor::_move_selection_commit)); track_edit->connect("move_selection_cancel", callable_mp(this, &AnimationTrackEditor::_move_selection_cancel)); - track_edit->connect("duplicate_request", callable_mp(this, &AnimationTrackEditor::_edit_menu_pressed).bind(EDIT_DUPLICATE_SELECTION), CONNECT_DEFERRED); - track_edit->connect("duplicate_transpose_request", callable_mp(this, &AnimationTrackEditor::_edit_menu_pressed).bind(EDIT_DUPLICATE_TRANSPOSED), CONNECT_DEFERRED); + track_edit->connect("duplicate_request", callable_mp(this, &AnimationTrackEditor::_anim_duplicate_keys).bind(i), CONNECT_DEFERRED); + track_edit->connect("copy_request", callable_mp(this, &AnimationTrackEditor::_edit_menu_pressed).bind(EDIT_COPY_KEYS), CONNECT_DEFERRED); + track_edit->connect("paste_request", callable_mp(this, &AnimationTrackEditor::_anim_paste_keys).bind(i), CONNECT_DEFERRED); track_edit->connect("create_reset_request", callable_mp(this, &AnimationTrackEditor::_edit_menu_pressed).bind(EDIT_ADD_RESET_KEY), CONNECT_DEFERRED); track_edit->connect("delete_request", callable_mp(this, &AnimationTrackEditor::_edit_menu_pressed).bind(EDIT_DELETE_SELECTION), CONNECT_DEFERRED); } @@ -5477,7 +5509,7 @@ void AnimationTrackEditor::_scroll_input(const Ref &p_event) { if (_get_track_selected() == -1 && track_edits.size() > 0) { // Minimal hack to make shortcuts work. track_edits[track_edits.size() - 1]->grab_focus(); } - } else { + } else if (!mb->is_command_or_control_pressed() && !mb->is_shift_pressed()) { _clear_selection(true); // Clear it. } @@ -5578,9 +5610,8 @@ void AnimationTrackEditor::_bezier_track_set_key_handle_mode(Animation *p_anim, p_anim->bezier_track_set_key_handle_mode(p_track, p_index, p_mode, p_set_mode); } -void AnimationTrackEditor::_anim_duplicate_keys(bool transpose) { - // Duplicait! - if (selection.size() && animation.is_valid() && (!transpose || (_get_track_selected() >= 0 && _get_track_selected() < animation->get_track_count()))) { +void AnimationTrackEditor::_anim_duplicate_keys(float p_ofs, int p_track) { + if (selection.size() && animation.is_valid()) { int top_track = 0x7FFFFFFF; float top_time = 1e10; for (RBMap::Element *E = selection.back(); E; E = E->prev()) { @@ -5596,9 +5627,32 @@ void AnimationTrackEditor::_anim_duplicate_keys(bool transpose) { } ERR_FAIL_COND(top_track == 0x7FFFFFFF || top_time == 1e10); - // + int start_track = p_track; + if (p_track == -1) { // Duplicating from shortcut or Edit menu. + bool is_valid_track_selected = _get_track_selected() >= 0 && _get_track_selected() < animation->get_track_count(); + start_track = is_valid_track_selected ? _get_track_selected() : top_track; + } - int start_track = transpose ? _get_track_selected() : top_track; + bool all_compatible = true; + + for (RBMap::Element *E = selection.back(); E; E = E->prev()) { + const SelectedKey &sk = E->key(); + int dst_track = sk.track + (start_track - top_track); + + if (dst_track < 0 || dst_track >= animation->get_track_count()) { + all_compatible = false; + break; + } + + Variant::Type value_type = animation->track_get_key_value(sk.track, sk.key).get_type(); + Animation::TrackType track_type = animation->track_get_type(sk.track); + if (!_is_track_compatible(dst_track, value_type, track_type)) { + all_compatible = false; + break; + } + } + + ERR_FAIL_COND_MSG(!all_compatible, "Duplicate failed: Not all animation keys were compatible with their target tracks"); EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); undo_redo->create_action(TTR("Animation Duplicate Keys")); @@ -5609,21 +5663,27 @@ void AnimationTrackEditor::_anim_duplicate_keys(bool transpose) { const SelectedKey &sk = E->key(); float t = animation->track_get_key_time(sk.track, sk.key); + float insert_pos = p_ofs >= 0 ? p_ofs : timeline->get_play_position(); - float dst_time = t + (timeline->get_play_position() - top_time); + float dst_time = t + (insert_pos - top_time); int dst_track = sk.track + (start_track - top_track); if (dst_track < 0 || dst_track >= animation->get_track_count()) { continue; } - if (animation->track_get_type(dst_track) != animation->track_get_type(sk.track)) { - continue; - } - int existing_idx = animation->track_find_key(dst_track, dst_time, Animation::FIND_MODE_APPROX); - undo_redo->add_do_method(animation.ptr(), "track_insert_key", dst_track, dst_time, animation->track_get_key_value(E->key().track, E->key().key), animation->track_get_key_transition(E->key().track, E->key().key)); + Variant value = animation->track_get_key_value(sk.track, sk.key); + bool key_is_bezier = animation->track_get_type(sk.track) == Animation::TYPE_BEZIER; + bool track_is_bezier = animation->track_get_type(dst_track) == Animation::TYPE_BEZIER; + if (key_is_bezier && !track_is_bezier) { + value = AnimationBezierTrackEdit::get_bezier_key_value(value); + } else if (!key_is_bezier && track_is_bezier) { + value = AnimationBezierTrackEdit::make_default_bezier_key(value); + } + + undo_redo->add_do_method(animation.ptr(), "track_insert_key", dst_track, dst_time, value, animation->track_get_key_transition(E->key().track, E->key().key)); undo_redo->add_undo_method(animation.ptr(), "track_remove_key_at_time", dst_track, dst_time); Pair p; @@ -5654,6 +5714,183 @@ void AnimationTrackEditor::_anim_duplicate_keys(bool transpose) { } } +void AnimationTrackEditor::_anim_copy_keys() { + if (is_selection_active() && animation.is_valid()) { + int top_track = 0x7FFFFFFF; + float top_time = 1e10; + for (RBMap::Element *E = selection.back(); E; E = E->prev()) { + const SelectedKey &sk = E->key(); + + float t = animation->track_get_key_time(sk.track, sk.key); + if (t < top_time) { + top_time = t; + } + if (sk.track < top_track) { + top_track = sk.track; + } + } + + ERR_FAIL_COND(top_track == 0x7FFFFFFF || top_time == 1e10); + + _set_key_clipboard(top_track, top_time, selection); + } +} + +void AnimationTrackEditor::_set_key_clipboard(int p_top_track, float p_top_time, RBMap &p_keys) { + key_clipboard.keys.clear(); + key_clipboard.top_track = p_top_track; + for (RBMap::Element *E = p_keys.back(); E; E = E->prev()) { + KeyClipboard::Key k; + k.value = animation->track_get_key_value(E->key().track, E->key().key); + k.transition = animation->track_get_key_transition(E->key().track, E->key().key); + k.time = E->value().pos - p_top_time; + k.track = E->key().track - p_top_track; + k.track_type = animation->track_get_type(E->key().track); + + key_clipboard.keys.push_back(k); + } +} + +void AnimationTrackEditor::_anim_paste_keys(float p_ofs, int p_track) { + if (is_key_clipboard_active() && animation.is_valid()) { + int start_track = p_track; + if (p_track == -1) { // Pasting from shortcut or Edit menu. + bool is_valid_track_selected = _get_track_selected() >= 0 && _get_track_selected() < animation->get_track_count(); + start_track = is_valid_track_selected ? _get_track_selected() : key_clipboard.top_track; + } + + bool all_compatible = true; + + for (int i = 0; i < key_clipboard.keys.size(); i++) { + const KeyClipboard::Key key = key_clipboard.keys[i]; + + int dst_track = key.track + start_track; + + if (dst_track < 0 || dst_track >= animation->get_track_count()) { + all_compatible = false; + break; + } + + if (!_is_track_compatible(dst_track, key.value.get_type(), key.track_type)) { + all_compatible = false; + break; + } + } + + ERR_FAIL_COND_MSG(!all_compatible, "Paste failed: Not all animation keys were compatible with their target tracks"); + + EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); + undo_redo->create_action(TTR("Animation Paste Keys")); + List> new_selection_values; + + for (int i = 0; i < key_clipboard.keys.size(); i++) { + const KeyClipboard::Key key = key_clipboard.keys[i]; + + float insert_pos = p_ofs >= 0 ? p_ofs : timeline->get_play_position(); + + float dst_time = key.time + insert_pos; + int dst_track = key.track + start_track; + + int existing_idx = animation->track_find_key(dst_track, dst_time, Animation::FIND_MODE_APPROX); + + Variant value = key.value; + bool key_is_bezier = key.track_type == Animation::TYPE_BEZIER; + bool track_is_bezier = animation->track_get_type(dst_track) == Animation::TYPE_BEZIER; + if (key_is_bezier && !track_is_bezier) { + value = AnimationBezierTrackEdit::get_bezier_key_value(value); + } else if (!key_is_bezier && track_is_bezier) { + value = AnimationBezierTrackEdit::make_default_bezier_key(value); + } + + undo_redo->add_do_method(animation.ptr(), "track_insert_key", dst_track, dst_time, value, key.transition); + undo_redo->add_undo_method(animation.ptr(), "track_remove_key_at_time", dst_track, dst_time); + + Pair p; + p.first = dst_track; + p.second = dst_time; + new_selection_values.push_back(p); + + if (existing_idx != -1) { + undo_redo->add_undo_method(animation.ptr(), "track_insert_key", dst_track, dst_time, animation->track_get_key_value(dst_track, existing_idx), animation->track_get_key_transition(dst_track, existing_idx)); + } + } + + undo_redo->add_do_method(this, "_clear_selection_for_anim", animation); + undo_redo->add_undo_method(this, "_clear_selection_for_anim", animation); + + // Reselect pasted. + for (const Pair &E : new_selection_values) { + undo_redo->add_do_method(this, "_select_at_anim", animation, E.first, E.second); + } + for (RBMap::Element *E = selection.back(); E; E = E->prev()) { + undo_redo->add_undo_method(this, "_select_at_anim", animation, E->key().track, E->get().pos); + } + + undo_redo->add_do_method(this, "_redraw_tracks"); + undo_redo->add_undo_method(this, "_redraw_tracks"); + undo_redo->commit_action(); + } +} + +bool AnimationTrackEditor::_is_track_compatible(int p_target_track_idx, Variant::Type p_source_value_type, Animation::TrackType p_source_track_type) { + if (animation.is_valid()) { + Animation::TrackType target_track_type = animation->track_get_type(p_target_track_idx); + bool track_types_equal = target_track_type == p_source_track_type; + bool is_source_vector3_type = p_source_track_type == Animation::TYPE_POSITION_3D || p_source_track_type == Animation::TYPE_SCALE_3D || p_source_track_type == Animation::TYPE_ROTATION_3D; + bool is_source_bezier = p_source_track_type == Animation::TYPE_BEZIER; + switch (target_track_type) { + case Animation::TYPE_POSITION_3D: + case Animation::TYPE_SCALE_3D: + return p_source_value_type == Variant::VECTOR3; + case Animation::TYPE_ROTATION_3D: + return p_source_value_type == Variant::QUATERNION; + case Animation::TYPE_BEZIER: + return track_types_equal || p_source_value_type == Variant::FLOAT; + case Animation::TYPE_VALUE: + if (track_types_equal || is_source_vector3_type || is_source_bezier) { + bool path_valid = false; + Variant::Type property_type = Variant::NIL; + + AnimationPlayerEditor *ape = AnimationPlayerEditor::get_singleton(); + if (ape) { + AnimationPlayer *ap = ape->get_player(); + if (ap) { + NodePath npath = animation->track_get_path(p_target_track_idx); + Node *a_ap_root_node = ap->get_node(ap->get_root_node()); + Node *nd = nullptr; + // We must test that we have a valid a_ap_root_node before trying to access its content to init the nd Node. + if (a_ap_root_node) { + nd = a_ap_root_node->get_node(NodePath(npath.get_concatenated_names())); + } + if (nd) { + StringName prop = npath.get_concatenated_subnames(); + PropertyInfo prop_info; + path_valid = ClassDB::get_property_info(nd->get_class(), prop, &prop_info); + property_type = prop_info.type; + } + } + } + + if (path_valid) { + if (is_source_bezier) + p_source_value_type = Variant::FLOAT; + return property_type == p_source_value_type; + } else { + if (animation->track_get_key_count(p_target_track_idx) > 0) { + Variant::Type first_key_type = animation->track_get_key_value(p_target_track_idx, 0).get_type(); + return first_key_type == p_source_value_type; + } + return true; // Type is Undefined. + } + } + return false; + default: // Works for TYPE_ANIMATION; TYPE_AUDIO; TYPE_CALL_METHOD; BLEND_SHAPE. + return track_types_equal; + } + } + return false; +} + void AnimationTrackEditor::_edit_menu_about_to_popup() { AnimationPlayer *player = AnimationPlayerEditor::get_singleton()->get_player(); edit->get_popup()->set_item_disabled(edit->get_popup()->get_item_index(EDIT_APPLY_RESET), !player->can_apply_reset()); @@ -6078,19 +6315,22 @@ void AnimationTrackEditor::_edit_menu_pressed(int p_option) { } break; - case EDIT_DUPLICATE_SELECTION: { + case EDIT_DUPLICATE_SELECTED_KEYS: { if (bezier_edit->is_visible()) { - bezier_edit->duplicate_selection(); + bezier_edit->duplicate_selected_keys(-1.0); break; } - _anim_duplicate_keys(false); + _anim_duplicate_keys(-1.0, -1.0); } break; - case EDIT_DUPLICATE_TRANSPOSED: { + case EDIT_COPY_KEYS: { if (bezier_edit->is_visible()) { - EditorNode::get_singleton()->show_warning(TTR("This option does not work for Bezier editing, as it's only a single track.")); + bezier_edit->copy_selected_keys(); break; } - _anim_duplicate_keys(true); + _anim_copy_keys(); + } break; + case EDIT_PASTE_KEYS: { + _anim_paste_keys(-1.0, -1.0); } break; case EDIT_ADD_RESET_KEY: { EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); @@ -6734,8 +6974,9 @@ AnimationTrackEditor::AnimationTrackEditor() { edit->get_popup()->add_separator(); edit->get_popup()->add_item(TTR("Make Easing Selection"), EDIT_EASE_SELECTION); edit->get_popup()->add_separator(); - edit->get_popup()->add_shortcut(ED_SHORTCUT("animation_editor/duplicate_selection", TTR("Duplicate Selection"), KeyModifierMask::CMD_OR_CTRL | Key::D), EDIT_DUPLICATE_SELECTION); - edit->get_popup()->add_shortcut(ED_SHORTCUT("animation_editor/duplicate_selection_transposed", TTR("Duplicate Transposed"), KeyModifierMask::SHIFT | KeyModifierMask::CMD_OR_CTRL | Key::D), EDIT_DUPLICATE_TRANSPOSED); + edit->get_popup()->add_shortcut(ED_SHORTCUT("animation_editor/duplicate_selected_keys", TTR("Duplicate Selected Keys"), KeyModifierMask::CMD_OR_CTRL | Key::D), EDIT_DUPLICATE_SELECTED_KEYS); + edit->get_popup()->add_shortcut(ED_SHORTCUT("animation_editor/copy_selected_keys", TTR("Copy Selected Keys"), KeyModifierMask::CMD_OR_CTRL | Key::C), EDIT_COPY_KEYS); + edit->get_popup()->add_shortcut(ED_SHORTCUT("animation_editor/paste_keys", TTR("Paste Keys"), KeyModifierMask::CMD_OR_CTRL | Key::V), EDIT_PASTE_KEYS); edit->get_popup()->add_shortcut(ED_SHORTCUT("animation_editor/add_reset_value", TTR("Add RESET Value(s)"))); edit->get_popup()->add_separator(); edit->get_popup()->add_shortcut(ED_SHORTCUT("animation_editor/delete_selection", TTR("Delete Selection"), Key::KEY_DELETE), EDIT_DELETE_SELECTION); diff --git a/editor/animation_track_editor.h b/editor/animation_track_editor.h index b5242e2f677..8653034f89b 100644 --- a/editor/animation_track_editor.h +++ b/editor/animation_track_editor.h @@ -230,6 +230,8 @@ class AnimationTrackEdit : public Control { MENU_LOOP_CLAMP, MENU_KEY_INSERT, MENU_KEY_DUPLICATE, + MENU_KEY_COPY, + MENU_KEY_PASTE, MENU_KEY_ADD_RESET, MENU_KEY_DELETE, MENU_USE_BLEND_ENABLED, @@ -275,6 +277,7 @@ class AnimationTrackEdit : public Control { void _path_submitted(const String &p_text); void _play_position_draw(); bool _is_value_key_valid(const Variant &p_key_value, Variant::Type &r_valid_type) const; + bool _try_select_at_ui_pos(const Point2 &p_pos, bool p_aggregate, bool p_deselectable); Ref _get_key_type_icon() const; @@ -375,6 +378,7 @@ public: class AnimationTrackEditor : public VBoxContainer { GDCLASS(AnimationTrackEditor, VBoxContainer); friend class AnimationTimelineEdit; + friend class AnimationBezierTrackEdit; Ref animation; bool read_only = false; @@ -570,7 +574,13 @@ class AnimationTrackEditor : public VBoxContainer { void _cleanup_animation(Ref p_animation); - void _anim_duplicate_keys(bool transpose); + void _anim_duplicate_keys(float p_ofs, int p_track); + + void _anim_copy_keys(); + + bool _is_track_compatible(int p_target_track_idx, Variant::Type p_source_value_type, Animation::TrackType p_source_track_type); + + void _anim_paste_keys(float p_ofs, int p_track); void _view_group_toggle(); Button *view_group = nullptr; @@ -600,8 +610,23 @@ class AnimationTrackEditor : public VBoxContainer { Vector keys; }; - Vector track_clipboard; + struct KeyClipboard { + int top_track; + struct Key { + Animation::TrackType track_type; + int track; + float time = 0; + float transition = 0; + Variant value; + }; + Vector keys; + }; + + Vector track_clipboard; + KeyClipboard key_clipboard; + + void _set_key_clipboard(int p_top_track, float p_top_time, RBMap &p_keymap); void _insert_animation_key(NodePath p_path, const Variant &p_value); void _pick_track_filter_text_changed(const String &p_newtext); @@ -622,13 +647,14 @@ public: EDIT_COPY_TRACKS, EDIT_COPY_TRACKS_CONFIRM, EDIT_PASTE_TRACKS, + EDIT_COPY_KEYS, + EDIT_PASTE_KEYS, EDIT_SCALE_SELECTION, EDIT_SCALE_FROM_CURSOR, EDIT_SCALE_CONFIRM, EDIT_EASE_SELECTION, EDIT_EASE_CONFIRM, - EDIT_DUPLICATE_SELECTION, - EDIT_DUPLICATE_TRANSPOSED, + EDIT_DUPLICATE_SELECTED_KEYS, EDIT_ADD_RESET_KEY, EDIT_DELETE_SELECTION, EDIT_GOTO_NEXT_STEP, @@ -672,6 +698,7 @@ public: bool is_key_selected(int p_track, int p_key) const; bool is_selection_active() const; + bool is_key_clipboard_active() const; bool is_moving_selection() const; bool is_snap_enabled() const; float get_moving_selection_offset() const;