engine: timeline panel core edit commands migrate to the facade (part 1)

- new batch primitives: split_clips (link-preserving, single undo
  command), delete_clips (gap replace + optional ripple with explicit
  region support), ripple_delete_range, marker_add_ex with color
- razor/split-at-playhead, clip delete, ripple-to-point, track delete,
  and the non-dialog marker path now issue facade commands instead of
  the app's own undo command classes
- batch operations deliberately produce one undo command per user
  action (deleting twenty clips is one entry, not twenty); selection
  and transition removal stay UI-side as documented leftovers
This commit is contained in:
2026-07-20 13:17:07 +08:00
parent 0a25d43218
commit 2aa7eec016
8 changed files with 528 additions and 51 deletions
+21 -2
View File
@@ -30,6 +30,7 @@
#include "common/current.h"
#include "dialog/markerproperties/markerpropertiesdialog.h"
#include "node/project/sequence/sequence.h"
#include "oakengine/timeline.h"
#include "timeline/timelineundoworkarea.h"
#include "widget/timeruler/timeruler.h"
@@ -751,17 +752,35 @@ void TimeBasedWidget::set_marker()
color, TimeRange(get_connected_node()->get_playhead(),
get_connected_node()->get_playhead()));
bool edited_in_dialog = false;
if (OAK_CONFIG("SetNameWithMarker").toBool()) {
MarkerPropertiesDialog mpd({ marker }, timebase(), this);
if (mpd.exec() != QDialog::Accepted) {
delete marker;
marker = nullptr;
} else {
edited_in_dialog = true;
}
}
if (marker) {
Core::instance()->undo_stack()->push(
new MarkerAddCommand(markers, marker), tr("Added Marker"));
if (edited_in_dialog) {
// The dialog pushed undo commands referencing this exact
// marker object, so it must be the one added to the list.
Core::instance()->undo_stack()->push(
new MarkerAddCommand(markers, marker), tr("Added Marker"));
} else {
// Pristine marker: add through the liboakengine C ABI
// facade (one undoable command) and drop the temporary.
oakengine_sequence_marker_add_ex(
reinterpret_cast<OakEngineSequence *>(
get_connected_node()),
Timecode::time_to_timestamp(marker->time().in(),
timebase(),
Timecode::k_round),
"", marker->color());
delete marker;
}
}
}
}
+57 -27
View File
@@ -47,6 +47,7 @@
#include "node/nodeundo.h"
#include "node/project/footage/footage.h"
#include "node/project/serializer/serializer.h"
#include "oakengine/timeline.h"
#include "render/audiowaveformcache.h"
#include "task/project/import/import.h"
#include "timeline/timelineundogeneral.h"
@@ -556,10 +557,20 @@ void TimelineWidget::split_at_playhead()
}
if (!blocks_to_split.isEmpty()) {
Core::instance()->undo_stack()->push(
new BlockSplitPreservingLinksCommand(blocks_to_split,
{ playhead_time }),
tr("Split Clips At Playhead"));
// Split through the liboakengine C ABI facade: one undoable,
// link-preserving command with the same semantics as the old
// app-side BlockSplitPreservingLinksCommand push.
QVector<OakEngineClip *> clips;
clips.reserve(blocks_to_split.size());
foreach (Block *b, blocks_to_split) {
clips.append(reinterpret_cast<OakEngineClip *>(
static_cast<ClipBlock *>(b)));
}
oakengine_sequence_split_clips(
reinterpret_cast<OakEngineSequence *>(sequence()), clips.data(),
clips.size(),
Timecode::time_to_timestamp(playhead_time, timebase(),
Timecode::k_round));
}
}
@@ -639,32 +650,47 @@ void TimelineWidget::DeleteSelected(bool ripple)
command->add_child(trc);
}
// Replace clips with gaps (effectively deleting them)
replace_blocks_with_gaps(clips_to_delete, true, command, false);
// Selection clearing and transition removal stay app-side (selection
// state and transition commands have no facade equivalent); the clip
// deletion core below goes through the facade and lands as one undoable
// command right after this one, keeping the undo order intact.
Core::instance()->undo_stack()->push(command, tr("Deleted Clips"));
// Insert ripple command now that it's all cleaned up gaps
TimelineRippleDeleteGapsAtRegionsCommand *ripple_command = nullptr;
Rational new_playhead = RATIONAL_MAX;
if (ripple) {
TimelineRippleDeleteGapsAtRegionsCommand::RangeList range_list;
foreach (Block *b, selected_list) {
range_list.append({ b->track(), b->range() });
new_playhead = qMin(new_playhead, b->in());
}
ripple_command = new TimelineRippleDeleteGapsAtRegionsCommand(
sequence(), range_list);
command->add_child(ripple_command);
// Delete the clips through the liboakengine C ABI facade (gap
// replacement + graph removal, optionally rippling the selected ranges
// closed), same semantics as the old in-command children.
QVector<OakEngineClip *> facade_clips;
facade_clips.reserve(clips_to_delete.size());
foreach (Block *b, clips_to_delete) {
facade_clips.append(
reinterpret_cast<OakEngineClip *>(static_cast<ClipBlock *>(b)));
}
Core::instance()->undo_stack()->push(command, tr("Deleted Clips"));
Rational new_playhead = RATIONAL_MAX;
QVector<int64_t> ripple_ranges;
if (ripple) {
foreach (Block *b, selected_list) {
ripple_ranges.append(int64_t(b->track()->type()));
ripple_ranges.append(b->track()->index());
ripple_ranges.append(Timecode::time_to_timestamp(
b->in(), timebase(), Timecode::k_round));
ripple_ranges.append(Timecode::time_to_timestamp(
b->out(), timebase(), Timecode::k_round));
new_playhead = qMin(new_playhead, b->in());
}
}
int rippled = 0;
oakengine_sequence_delete_clips(
reinterpret_cast<OakEngineSequence *>(sequence()),
facade_clips.data(), facade_clips.size(), ripple ? 1 : 0,
ripple ? ripple_ranges.constData() : nullptr,
ripple ? ripple_ranges.size() / 4 : 0, &rippled);
// Ensures any current drag operations are cancelled
clear_ghosts();
if (ripple_command && ripple_command->has_commands() &&
new_playhead != RATIONAL_MAX) {
if (ripple && rippled && new_playhead != RATIONAL_MAX) {
get_connected_node()->set_playhead(new_playhead);
}
}
@@ -2535,10 +2561,14 @@ void TimelineWidget::ripple_to(Timeline::MovementMode mode)
Rational in_ripple = qMin(closest_point_to_playhead, playhead_time);
Rational out_ripple = qMax(closest_point_to_playhead, playhead_time);
TimelineRippleRemoveAreaCommand *c =
new TimelineRippleRemoveAreaCommand(sequence(), in_ripple, out_ripple);
Core::instance()->undo_stack()->push(c, tr("Rippled Clip(s) To Point"));
// Ripple the region out through the liboakengine C ABI facade (one
// undoable all-tracks ripple, same as the old
// TimelineRippleRemoveAreaCommand push).
oakengine_sequence_ripple_delete_range(
reinterpret_cast<OakEngineSequence *>(sequence()),
Timecode::time_to_timestamp(in_ripple, timebase(), Timecode::k_round),
Timecode::time_to_timestamp(out_ripple, timebase(),
Timecode::k_round));
// If we rippled, ump to where new cut is if applicable
if (mode == Timeline::k_trim_in) {
+16 -6
View File
@@ -21,8 +21,7 @@
#include "razor.h"
#include "node/nodeundo.h"
#include "timeline/timelineundosplit.h"
#include "oakengine/timeline.h"
#include "widget/timelinewidget/timelinewidget.h"
namespace olive
@@ -94,10 +93,21 @@ void RazorTool::mouse_release(TimelineViewMouseEvent *event)
split_tracks_.clear();
if (!blocks_to_split.isEmpty()) {
Core::instance()->undo_stack()->push(
new BlockSplitPreservingLinksCommand(blocks_to_split,
{ split_time }),
qApp->translate("RazorTool", "Split Clips"));
// Split through the liboakengine C ABI facade: one undoable,
// link-preserving command with the same semantics as the old
// app-side BlockSplitPreservingLinksCommand push.
QVector<OakEngineClip *> clips;
clips.reserve(blocks_to_split.size());
foreach (Block *b, blocks_to_split) {
if (ClipBlock *clip = dynamic_cast<ClipBlock *>(b)) {
clips.append(reinterpret_cast<OakEngineClip *>(clip));
}
}
oakengine_sequence_split_clips(
reinterpret_cast<OakEngineSequence *>(parent()->sequence()),
clips.data(), clips.size(),
Timecode::time_to_timestamp(split_time, parent()->timebase(),
Timecode::k_round));
}
dragging_ = false;
@@ -29,6 +29,7 @@
#include <QtMath>
#include "core.h"
#include "oakengine/timeline.h"
#include "timeline/timelineundogeneral.h"
#include "ui/icons/icons.h"
#include "widget/menu/menu.h"
@@ -159,9 +160,11 @@ void TrackViewItem::show_context_menu(const QPoint &p)
void TrackViewItem::delete_track()
{
emit about_to_delete_track(track_);
Core::instance()->undo_stack()->push(
new TimelineRemoveTrackCommand(track_),
tr("Deleted Track \"%1\"").arg(track_->get_label_or_name()));
// Through the liboakengine C ABI facade (one undoable command, same as
// the old TimelineRemoveTrackCommand push).
oakengine_sequence_remove_track(
reinterpret_cast<OakEngineSequence *>(track_->sequence()),
int(track_->type()), track_->index());
}
void TrackViewItem::delete_all_empty_tracks()