Merge pull request #87250 from CookieBadger/animation-copy-paste-keyframe

Implement consistent functionality for select, copy, paste, and duplicate in AnimationPlayer
This commit is contained in:
Rémi Verschelde 2024-02-12 13:33:57 +01:00
commit 75255bd15c
No known key found for this signature in database
GPG Key ID: C3336907360768E1
4 changed files with 595 additions and 184 deletions

View File

@ -834,16 +834,22 @@ void AnimationBezierTrackEdit::gui_input(const Ref<InputEvent> &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<InputEvent> &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<InputEvent> &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<InputEvent> &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<InputEvent> p_event) {
Ref<InputEventMouseMotion> 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<int, real_t> &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<AnimationTrackEditor::SelectedKey, AnimationTrackEditor::KeyInfo> 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<Pair<int, float>> 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<int, float> 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<int, float> &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() {

View File

@ -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<Animation> &p_anim);
void _select_at_anim(const Ref<Animation> &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<Animation> 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);

View File

@ -2686,16 +2686,22 @@ void AnimationTrackEdit::gui_input(const Ref<InputEvent> &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();
}
@ -2822,71 +2828,8 @@ void AnimationTrackEdit::gui_input(const Ref<InputEvent> &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();
}
}
@ -2902,12 +2845,19 @@ void AnimationTrackEdit::gui_input(const Ref<InputEvent> &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);
@ -3030,6 +2980,75 @@ void AnimationTrackEdit::gui_input(const Ref<InputEvent> &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();
@ -3156,7 +3175,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"));
@ -3230,9 +3256,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"));
}
@ -4389,6 +4416,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);
}
@ -4576,8 +4607,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);
}
@ -5483,7 +5515,7 @@ void AnimationTrackEditor::_scroll_input(const Ref<InputEvent> &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.
}
@ -5584,9 +5616,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<SelectedKey, KeyInfo>::Element *E = selection.back(); E; E = E->prev()) {
@ -5602,9 +5633,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<SelectedKey, KeyInfo>::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"));
@ -5615,21 +5669,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<int, float> p;
@ -5660,6 +5720,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<SelectedKey, KeyInfo>::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<SelectedKey, KeyInfo> &p_keys) {
key_clipboard.keys.clear();
key_clipboard.top_track = p_top_track;
for (RBMap<SelectedKey, KeyInfo>::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<Pair<int, float>> 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<int, float> 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<int, float> &E : new_selection_values) {
undo_redo->add_do_method(this, "_select_at_anim", animation, E.first, E.second);
}
for (RBMap<SelectedKey, KeyInfo>::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());
@ -6084,19 +6321,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();
@ -6740,8 +6980,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);

View File

@ -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<Texture2D> _get_key_type_icon() const;
@ -376,6 +379,7 @@ public:
class AnimationTrackEditor : public VBoxContainer {
GDCLASS(AnimationTrackEditor, VBoxContainer);
friend class AnimationTimelineEdit;
friend class AnimationBezierTrackEdit;
Ref<Animation> animation;
bool read_only = false;
@ -571,7 +575,13 @@ class AnimationTrackEditor : public VBoxContainer {
void _cleanup_animation(Ref<Animation> 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;
@ -601,8 +611,23 @@ class AnimationTrackEditor : public VBoxContainer {
Vector<Key> keys;
};
Vector<TrackClipboard> track_clipboard;
struct KeyClipboard {
int top_track;
struct Key {
Animation::TrackType track_type;
int track;
float time = 0;
float transition = 0;
Variant value;
};
Vector<Key> keys;
};
Vector<TrackClipboard> track_clipboard;
KeyClipboard key_clipboard;
void _set_key_clipboard(int p_top_track, float p_top_time, RBMap<SelectedKey, KeyInfo> &p_keymap);
void _insert_animation_key(NodePath p_path, const Variant &p_value);
void _pick_track_filter_text_changed(const String &p_newtext);
@ -623,13 +648,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,
@ -673,6 +699,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;