From efef8a201aa00b38aa89a61b58c5a1b19d552e58 Mon Sep 17 00:00:00 2001 From: Ramon Klauck Date: Mon, 4 Aug 2025 19:15:00 +0200 Subject: [PATCH] VSE: delete keyframes from preview This PR makes it easier to delete strip keyframes from the preview. It works similarly to the 3D viewport and also works with keying sets. Pressing "alt + I" deletes the keyframe on the current frame of the selected strips, when a keyset is active it only deletes the keyframes of the selected keyset. Pull Request: https://projects.blender.org/blender/blender/pulls/140385 --- .../keyconfig/keymap_data/blender_default.py | 1 + scripts/startup/bl_ui/space_sequencer.py | 1 + .../blender/editors/animation/anim_intern.hh | 1 + source/blender/editors/animation/anim_ops.cc | 1 + .../blender/editors/animation/keyframing.cc | 190 +++++++++++++++++- 5 files changed, 191 insertions(+), 3 deletions(-) diff --git a/scripts/presets/keyconfig/keymap_data/blender_default.py b/scripts/presets/keyconfig/keymap_data/blender_default.py index 86a07f1f56b..7b22ac40fe2 100644 --- a/scripts/presets/keyconfig/keymap_data/blender_default.py +++ b/scripts/presets/keyconfig/keymap_data/blender_default.py @@ -3244,6 +3244,7 @@ def km_sequencer_preview(params): ("anim.keyframe_insert", {"type": 'I', "value": 'PRESS'}, None), ("anim.keying_set_active_set", {"type": 'K', "value": 'PRESS', "shift": True}, None), ("anim.keyframe_insert_menu", {"type": 'K', "value": 'PRESS'}, {"properties": [("always_prompt", True)]}), + ("anim.keyframe_delete_vse", {"type": 'I', "value": 'PRESS', "alt": True}, None), *_template_items_context_menu("SEQUENCER_MT_preview_context_menu", params.context_menu_event), ]) diff --git a/scripts/startup/bl_ui/space_sequencer.py b/scripts/startup/bl_ui/space_sequencer.py index 8e69e7892e7..75ab7fa073e 100644 --- a/scripts/startup/bl_ui/space_sequencer.py +++ b/scripts/startup/bl_ui/space_sequencer.py @@ -985,6 +985,7 @@ class SEQUENCER_MT_strip_animation(Menu): layout.operator("anim.keyframe_insert", text="Insert Keyframe") layout.operator("anim.keyframe_insert_menu", text="Insert Keyframe with Keying Set").always_prompt = True layout.operator("anim.keying_set_active_set", text="Change Keying Set...") + layout.operator("anim.keyframe_delete_vse", text="Delete Keyframes...") class SEQUENCER_MT_strip_input(Menu): diff --git a/source/blender/editors/animation/anim_intern.hh b/source/blender/editors/animation/anim_intern.hh index 368350bcc08..043df9e71e3 100644 --- a/source/blender/editors/animation/anim_intern.hh +++ b/source/blender/editors/animation/anim_intern.hh @@ -45,6 +45,7 @@ void ANIM_OT_keyframe_delete_by_name(wmOperatorType *ot); void ANIM_OT_keyframe_insert_menu(wmOperatorType *ot); void ANIM_OT_keyframe_delete_v3d(wmOperatorType *ot); +void ANIM_OT_keyframe_delete_vse(wmOperatorType *ot); void ANIM_OT_keyframe_clear_v3d(wmOperatorType *ot); /** \} */ diff --git a/source/blender/editors/animation/anim_ops.cc b/source/blender/editors/animation/anim_ops.cc index 9b0ca3afeac..4dc3000f7aa 100644 --- a/source/blender/editors/animation/anim_ops.cc +++ b/source/blender/editors/animation/anim_ops.cc @@ -1374,6 +1374,7 @@ void ED_operatortypes_anim() WM_operatortype_append(ANIM_OT_keyframe_delete); WM_operatortype_append(ANIM_OT_keyframe_insert_menu); WM_operatortype_append(ANIM_OT_keyframe_delete_v3d); + WM_operatortype_append(ANIM_OT_keyframe_delete_vse); WM_operatortype_append(ANIM_OT_keyframe_clear_v3d); WM_operatortype_append(ANIM_OT_keyframe_insert_button); WM_operatortype_append(ANIM_OT_keyframe_delete_button); diff --git a/source/blender/editors/animation/keyframing.cc b/source/blender/editors/animation/keyframing.cc index b4a3325a8a8..25cdf1d148b 100644 --- a/source/blender/editors/animation/keyframing.cc +++ b/source/blender/editors/animation/keyframing.cc @@ -12,6 +12,7 @@ #include "MEM_guardedalloc.h" +#include "BLI_listbase.h" #include "BLI_string.h" #include "BLT_translation.hh" @@ -55,6 +56,8 @@ #include "ANIM_keyingsets.hh" #include "ANIM_rna.hh" +#include "SEQ_relations.hh" + #include "UI_interface.hh" #include "UI_interface_layout.hh" #include "UI_resources.hh" @@ -65,6 +68,7 @@ #include "RNA_access.hh" #include "RNA_define.hh" #include "RNA_enum_types.hh" +#include "RNA_path.hh" #include "RNA_prototypes.hh" #include "anim_intern.hh" @@ -604,14 +608,14 @@ static wmOperatorStatus delete_key_using_keying_set(bContext *C, wmOperator *op, int num_channels; const bool confirm = op->flag & OP_IS_INVOKE; - /* try to delete keyframes for the channels specified by KeyingSet */ + /* Try to delete keyframes for the channels specified by KeyingSet. */ num_channels = blender::animrig::apply_keyingset( C, nullptr, ks, blender::animrig::ModifyKeyMode::DELETE_KEY, cfra); if (G.debug & G_DEBUG) { printf("KeyingSet '%s' - Successfully removed %d Keyframes\n", ks->name, num_channels); } - /* report failure or do updates? */ + /* Report failure or do updates? */ if (num_channels < 0) { BKE_report(op->reports, RPT_ERROR, "No suitable context info for active keying set"); return OPERATOR_CANCELLED; @@ -619,10 +623,14 @@ static wmOperatorStatus delete_key_using_keying_set(bContext *C, wmOperator *op, if (num_channels > 0) { WM_event_add_notifier(C, NC_ANIMATION | ND_KEYFRAME | NA_REMOVED, nullptr); + + /* VSE notifiers. */ + WM_event_add_notifier(C, NC_SCENE | ND_SEQUENCER, scene); + WM_event_add_notifier(C, NC_ANIMATION, nullptr); } if (confirm) { - /* if called by invoke (from the UI), make a note that we've removed keyframes */ + /* If called by invoke (from the UI), make a note that we've removed keyframes. */ if (num_channels > 0) { BKE_reportf(op->reports, RPT_INFO, @@ -811,6 +819,34 @@ void ANIM_OT_keyframe_clear_v3d(wmOperatorType *ot) WM_operator_properties_confirm_or_exec(ot); } +static blender::Vector get_selected_strips_rna_paths( + blender::Vector &selection) +{ + blender::Vector selected_strips_rna_paths; + for (PointerRNA &id_ptr : selection) { + if (RNA_struct_is_a(id_ptr.type, &RNA_Strip)) { + std::optional rna_path = RNA_path_from_ID_to_struct(&id_ptr); + selected_strips_rna_paths.append(*rna_path); + } + } + return selected_strips_rna_paths; +} + +static void invalidate_strip_caches(blender::Vector selection, Scene *scene) +{ + for (PointerRNA &id_ptr : selection) { + if (RNA_struct_is_a(id_ptr.type, &RNA_Strip)) { + ::Strip *strip = static_cast<::Strip *>(id_ptr.data); + blender::seq::relations_invalidate_cache(scene, strip); + } + } +} + +static bool fcurve_belongs_to_strip(const FCurve &fcurve, const std::string &strip_path) +{ + return fcurve.rna_path && + std::strncmp(fcurve.rna_path, strip_path.c_str(), strip_path.length()) == 0; +} static bool can_delete_key(FCurve *fcu, Object *ob, ReportList *reports) { /* don't touch protected F-Curves */ @@ -853,6 +889,154 @@ static bool can_delete_key(FCurve *fcu, Object *ob, ReportList *reports) return true; } +static bool can_delete_scene_key(FCurve *fcu, Scene *scene, wmOperator *op) +{ + /* Don't touch protected F-Curves. */ + if (BKE_fcurve_is_protected(fcu)) { + BKE_reportf(op->reports, + RPT_WARNING, + "Not deleting keyframe for locked F-Curve '%s', scene '%s'", + fcu->rna_path, + scene->id.name + 2); + return false; + } + return true; +} + +static wmOperatorStatus delete_key_vse_without_keying_set(bContext *C, wmOperator *op) +{ + using namespace blender::animrig; + Scene *scene = CTX_data_scene(C); + const float cfra = BKE_scene_frame_get(scene); + + blender::Vector selection; + blender::Vector selected_strips_rna_paths; + get_selection(C, &selection); + selected_strips_rna_paths = get_selected_strips_rna_paths(selection); + + if (selected_strips_rna_paths.is_empty()) { + BKE_reportf(op->reports, RPT_WARNING, "No strips selected"); + return OPERATOR_CANCELLED; + } + + const bool confirm = op->flag & OP_IS_INVOKE; + if (!scene->adt || !scene->adt->action || (scene->adt->slot_handle == Slot::unassigned)) { + BKE_reportf(op->reports, RPT_ERROR, "Scene has no animation data or active action"); + return OPERATOR_CANCELLED; + } + + AnimData *adt = scene->adt; + bAction *act = adt->action; + Action &action = act->wrap(); + + const float cfra_unmap = BKE_nla_tweakedit_remap(adt, cfra, NLATIME_CONVERT_UNMAP); + + blender::VectorSet modified_strips; + blender::Vector modified_fcurves; + + foreach_fcurve_in_action_slot(action, adt->slot_handle, [&](FCurve &fcurve) { + std::string changed_strip; + for (const std::string &strip_path : selected_strips_rna_paths) { + if (fcurve_belongs_to_strip(fcurve, strip_path)) { + changed_strip = strip_path; + break; + } + } + if (!can_delete_scene_key(&fcurve, scene, op) || changed_strip.empty()) { + return; + } + if (blender::animrig::fcurve_delete_keyframe_at_time(&fcurve, cfra_unmap)) { + modified_fcurves.append(&fcurve); + modified_strips.add(changed_strip); + } + }); + + for (FCurve *fcurve : modified_fcurves) { + if (BKE_fcurve_is_empty(fcurve)) { + action_fcurve_remove(action, *fcurve); + } + } + + if (scene->adt->action) { + /* The Action might have been unassigned, if it is legacy and the last + * F-Curve was removed. */ + DEG_id_tag_update(&scene->adt->action->id, ID_RECALC_ANIMATION_NO_FLUSH); + } + + if (!modified_strips.is_empty()) { + /* Key-frames on strips has been moved, so make sure related editors are informed. */ + WM_event_add_notifier(C, NC_SCENE | ND_SEQUENCER, scene); + WM_event_add_notifier(C, NC_ANIMATION, nullptr); + } + + invalidate_strip_caches(selection, scene); + + if (confirm) { + /* If called by invoke (from the UI), make a note that we've removed keyframes. */ + if (modified_strips.is_empty()) { + BKE_reportf(op->reports, + RPT_WARNING, + "No keyframes removed from %ld strip(s)", + selected_strips_rna_paths.size()); + return OPERATOR_CANCELLED; + } + + BKE_reportf(op->reports, + RPT_INFO, + "%ld strip(s) successfully had %ld keyframes removed", + modified_strips.size(), + modified_fcurves.size()); + } + + return OPERATOR_FINISHED; +} + +static wmOperatorStatus delete_key_vse_exec(bContext *C, wmOperator *op) +{ + Scene *scene = CTX_data_scene(C); + KeyingSet *ks = blender::animrig::scene_get_active_keyingset(scene); + + if (ks == nullptr) { + return delete_key_vse_without_keying_set(C, op); + } + + return delete_key_using_keying_set(C, op, ks); +} + +static wmOperatorStatus delete_key_vse_invoke(bContext *C, + wmOperator *op, + const wmEvent * /*event*/) +{ + if (RNA_boolean_get(op->ptr, "confirm")) { + return WM_operator_confirm_ex(C, + op, + IFACE_("Delete keyframes from selected strips?"), + nullptr, + IFACE_("Delete"), + ALERT_ICON_NONE, + false); + } + return delete_key_vse_exec(C, op); +} + +void ANIM_OT_keyframe_delete_vse(wmOperatorType *ot) +{ + /* identifiers */ + ot->name = "Delete Keyframe"; + ot->description = "Remove keyframes on current frame for selected strips"; + ot->idname = "ANIM_OT_keyframe_delete_vse"; + + /* callbacks */ + ot->invoke = delete_key_vse_invoke; + ot->exec = delete_key_vse_exec; + + ot->poll = ED_operator_areaactive; + + /* flags */ + ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO; + WM_operator_properties_confirm_or_exec(ot); +} + static wmOperatorStatus delete_key_v3d_without_keying_set(bContext *C, wmOperator *op) { using namespace blender::animrig;