Skip to content

Improve timeline undo performance - #1897

Open
ddennedy wants to merge 13 commits into
masterfrom
optimizied-timeline-undo
Open

Improve timeline undo performance#1897
ddennedy wants to merge 13 commits into
masterfrom
optimizied-timeline-undo

Conversation

@ddennedy

@ddennedy ddennedy commented Aug 17, 2026

Copy link
Copy Markdown
Member

This pull request significantly improves the undo/redo system for timeline editing commands by making undo operations more efficient and context-aware. The changes focus on capturing only the affected tracks or clips for undo, rather than restoring entire tracks unnecessarily. This results in faster, more reliable undos and better user experience. Additionally, command descriptions are now copied to UndoHelper to show on a progress dialogs when things are slow. Things are usually slow on big projects especially when Ripple All Tracks is on.

Undo/Redo System Improvements

  • Most timeline commands now call m_undoHelper.recordBeforeState() with a set of affected track indices, instead of always restoring whole tracks. This enables more efficient, fine-grained undo operations.

  • For commands that can affect multiple tracks (such as ripple edits or move/align commands), logic was added to determine the precise scope of affected tracks and fall back to whole-track restores only when necessary (e.g., ripple-all-tracks is enabled).


Here's the complete picture of every timeline command's undo strategy on the branch now.

Fine-grained (fast per-clip undo — no RestoreTracks)

Use UndoHelper with NoHints/SkipXML:

Command Notes
AppendCommand NoHints
OverwriteCommand NoHints (snapshots the replaced/split span)
LiftCommand NoHints (snapshots lifted clip)
RemoveCommand NoHints when not ripple-all
MergeCommand NoHints
SplitCommand NoHints (snapshots split clip)
InsertCommand NoHints when not ripple-all (snapshots split clip)
AddTransitionCommand NoHints when not ripple-all (snapshots mixed clips)
TrimClipInCommand NoHints when not ripple
TrimClipOutCommand SkipXML when not ripple
UpdateCommand NoHints
DetachAudioCommand NoHints
ReplaceCommand NoHints

Still RestoreTracks (whole-track rebuild)

These are the genuinely track-wide or cross-track cases where fine-grained doesn't apply:

Command When Why it needs it
MoveClipCommand always can move across tracks / reorder
RemoveTrackCommand always the whole track is cleared
AlignClipsCommand always repositions many clips (lift+overwrite loop)
InsertCommand ripple-all-tracks shifts clips on other tracks
RemoveCommand ripple-all-tracks shifts other tracks
AddTransitionCommand ripple-all-tracks shifts other tracks
TrimClipInCommand ripple ripple shifts the track (and others if ripple-all)

Don't use UndoHelper at all (own inverse logic)

GroupCommand, UngroupCommand, NameTrackCommand, MuteTrackCommand, HideTrackCommand, CompositeTrackCommand, LockTrackCommand, FadeInCommand, FadeOutCommand, TrimTransitionInCommand, TrimTransitionOutCommand, ResizeTransitionCommand, AddTransitionByTrimInCommand, AddTransitionByTrimOutCommand, RemoveTransitionByTrimInCommand, RemoveTransitionByTrimOutCommand, AddTrackCommand, InsertTrackCommand, MoveTrackCommand, ChangeBlendModeCommand, ChangeTransitionPropertyCommand, ApplyFiltersCommand, ChangeGainCommand.


Progress Dialog

  • All timeline commands now propagate their undo/redo text label via m_undoHelper.setText(text()) to show descriptions in the progress dialogs.

Improved Undo Data Capture

  • For commands that split, overwrite, or otherwise modify clips, the system now snapshots only the affected clips’ XML, enabling cheap and accurate restoration without parsing the entire track.

Refactoring for Move/Align Commands

  • The MoveClipCommand and AlignClipsCommand now defer the call to recordBeforeState() until the redo step, after the set of affected clips/tracks is known, further optimizing the undo scope.

- Rework timeline undo/redo to snapshot and restore whole affected
track playlists instead of diffing clips individually.
- Preserve clip UUIDs across XML round-trips using a temporary
serialized property so restored clips keep their identities.
- Update timeline command call sites to pass known track scope,
reducing unnecessary full-timeline scanning for common edits.
- Add a small History undo view wrapper that defers repeated
track-filter and background-duration adjustment work during
multi-step History dock jumps.
Reintroduce fine-grained per-clip undo/redo alongside the whole-track
snapshot restore so single-clip edits no longer rebuild an entire track.
UndoHelper regains OptimizationHints (NoHints/SkipXML/RestoreTracks): the
fine-grained path diffs clips by uuid and replays only what changed,
sourcing restored content from a lazily-parsed shadow of each track's
before-state XML; RestoreTracks keeps the whole-track rebuild for
structurally complex edits. Use RestoreTracks for insert, overwrite,
lift, remove, move, split, align, add-transition, remove-track, and
ripple trims; NoHints/SkipXML for trims and other localized edits.
@ddennedy
ddennedy requested a balanced review from Copilot August 17, 2026 00:40
@ddennedy ddennedy added this to the v26.9 milestone Aug 17, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Optimizes timeline undo/redo by limiting snapshots to affected tracks or clips and batching History dock operations.

Changes:

  • Adds fine-grained snapshot and restoration strategies.
  • Batches multi-step History operations and defers expensive refreshes.
  • Adds progress reporting and XML-call instrumentation.

Reviewed changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/shotcut_mlt_properties.h Adds temporary serializable UUID property.
src/models/multitrackmodel.h Declares bulk-update state and API.
src/models/multitrackmodel.cpp Implements deferred updates and refreshes.
src/mltcontroller.h Exposes XML-call instrumentation.
src/mltcontroller.cpp Counts XML serialization calls.
src/mainwindow.cpp Installs the specialized History view.
src/docks/timelinedock.h Adds deferred selection state.
src/docks/timelinedock.cpp Defers validation and scopes trim snapshots.
src/docks/historyundoview.h Declares bulk-aware History view.
src/docks/historyundoview.cpp Brackets History interactions as bulk updates.
src/dialogs/longuitask.h Increases progress-dialog responsiveness.
src/commands/undohelper.h Defines scoped snapshot strategies.
src/commands/undohelper.cpp Implements fine-grained and whole-track restoration.
src/commands/timelinecommands.h Adds move-command track scoping.
src/commands/timelinecommands.cpp Applies scoped undo capture across commands.
src/CMakeLists.txt Builds the new History view.
Suppressed comments (1)

src/commands/timelinecommands.cpp:1153

  • TrimClipOutCommand also never calls setText() on its helper. If its fine-grained undo falls back to restoreAffectedTracks(), the progress dialog therefore displays the incomplete label Undo . Propagate the command text when creating or injecting the trim helper.
        m_undoHelper.reset(new UndoHelper(m_model));
        if (!m_ripple)
            m_undoHelper->setHints(UndoHelper::SkipXML);
        m_undoHelper->recordBeforeState(m_rippleAllTracks ? QSet<int>() : QSet<int>{m_trackIndex});

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/commands/undohelper.cpp Outdated
Comment thread src/commands/timelinecommands.cpp
Comment thread src/docks/historyundoview.cpp Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated no new comments.

Suppressed comments (1)

src/docks/historyundoview.cpp:45

  • Every key press in the History view enters a bulk update, including keys such as Shift or an unhandled character that do not change the undo-stack index. endBulkUpdate() nevertheless always calls MLT.refreshConsumer() and emits bulkUpdateFinished(), so these no-op keys trigger an expensive timeline refresh. Restrict bulk updates to index-changing navigation, or track whether the stack index changed and skip the final flush when no command was replayed.
    m_model->beginBulkUpdate();
    QUndoView::keyPressEvent(event);
    m_model->endBulkUpdate();

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants