Zrythm v2.0.0-DEV
a highly automated and intuitive digital audio workstation
Loading...
Searching...
No Matches
undo_system

Zrythm Undo/Redo Architecture

1. Guiding Principles

  • Separation of Concerns:
    • Model layer: pure data & properties, no undo awareness.
    • Commands: immutable, reversible single-concern mutations.
    • Undo-Stack: owns commands, persistence, global hooks.
    • Actions (Operators): thin, stateless façade that only produces commands.
    • UI (QML): declarative views that use operators to edit values, zero direct model mutation.
    • Factories: handle creation and initialization of model objects.
  • No global state: one undo-stack per project, exposed via standard QML type registration.
  • Tooling-friendly: no root-context properties, no singletons.

2. Module Map & Dependencies

graph TD
dsp[zrythm::dsp] --> model[zrythm::model]
model --> factories[zrythm::model Factories]
model --> commands[zrythm::commands]
commands --> undo[zrythm::undo]
undo --> actions[zrythm::actions]
actions --> gui[zrythm::gui]
Module Responsibility Public API Examples
zrythm::dsp Low-level DSP types & processor graphs ProcessorParameter
zrythm::model Data objects (Track, Clip, Plugin) Track::setName()
zrythm::model::Factories Object creation & initialization TrackFactory::create()
zrythm::commands Concrete QUndoCommand subclasses RenameTrackCmd, AddTrackCmd
zrythm::undo Undo-stack container, save/load, hooks UndoStack QML type
zrythm::actions Controllers that expose semantic actions TrackOperator::rename(), ProjectActions.addMidiTrack()
zrythm::gui Qt-Quick UI, makes changes via operators TrackView.qml

3. Architecture Overview

The architecture supports two primary operations:

  1. Editing existing objects via Operator classes (stateful, per-object)
  2. Adding new objects via Action functions (stateless, global)

Object Editing Flow

sequenceDiagram
participant Q as QML UI
participant O as Operator
participant S as UndoStack
participant C as Command
participant M as Model
Q->>O: rename("Intro")
O->>C: new RenameTrackCmd(...)
O->>S: push(C)
S->>C: redo()
C->>M: setName("Intro")
M-->>Q: nameChanged()

Object Creation Flow

sequenceDiagram
participant Q as QML UI
participant A as ProjectActions
participant F as Factory
participant S as UndoStack
participant C as Command
participant M as Model
participant R as Registry
Q->>A: addMidiTrack()
A->>F: create(registry)
F->>R: createObject()
R-->>F: newObjectId
F-->>A: newObjectId
A->>C: new AddTrackCmd(project, newObjectId)
A->>S: push(C)
S->>C: redo()
C->>M: tracks.add(newObjectId)
M-->>Q: tracksChanged()

4. Component Responsibilities

Models (zrythm::model)

  • Represent the persistent state of the application
  • Contain business logic and data (Track, Clip, Project, etc.)
  • All instances owned by a central Registry with unique, reference-counted IDs
  • No knowledge of undo/redo systems or UI

Factories (zrythm::model)

  • Handle creation and initial configuration of model objects
  • Reside alongside the model classes they create
  • Assign unique IDs from the Registry
  • Set up default state and internal connections

Commands (zrythm::commands)

  • Perform single, reversible operations on models
  • Derive from QUndoCommand
  • Implement redo() (apply change) and undo() (revert change)
  • Dumb and focused - know how to apply/revert changes but not why

Actions (zrythm::actions)

Two types of actions:

  1. Operators: Stateful, per-object actions for editing (e.g., TrackOperator)
  2. Global Actions: Stateless functions for object creation (e.g., ProjectActions.addMidiTrack())

Responsibilities:

  • Provide high-level, QML-friendly API
  • Orchestrate complex operations (potentially multiple commands)
  • Use Factories to create new objects
  • Create Commands and push to UndoStack

Undo Stack (zrythm::undo)

  • Owns and manages command lifecycle
  • Handles persistence and serialization
  • Provides global hooks for undo/redo events
  • Exposed as QML type for UI integration

5. QML Integration Patterns

Editing Existing Objects

// TrackView.qml
import Zrythm 1.0
required property UndoStack projectUndo
required property Track currentTrack
TrackOperator {
id: trackOp
track: currentTrack
undoStack: projectUndo
}
TextField {
text: trackOp.track.name
onEditingFinished: trackOp.rename(text)
}

Adding New Objects

// ProjectToolbar.qml
import Zrythm 1.0
required property UndoStack projectUndo
Button {
text: "Add MIDI Track"
onClicked: ProjectActions.addMidiTrack(projectUndo)
}

6. Checklist for New Undoable Action

For Editing Existing Objects:

  1. Add model setter (if not present)
  2. Add command class in zrythm::commands
  3. Implement operator method in zrythm::actions
  4. Register command for (de)serialization
  5. Add unit tests for command + operator
  6. Expose operator to QML if UI needs it

For Adding New Objects:

  1. Add factory class in zrythm::model::Factories
  2. Add command class in zrythm::commands
  3. Implement action function in zrythm::actions
  4. Register command for (de)serialization
  5. Add unit tests for factory, command and action
  6. Expose action to QML

7. Non-Undoable State

  • View-only flags (visibility, window geometry) bypass operators; mutate model or view-model directly.

8. Key Design Principles

  1. Single Responsibility: Each component has a clear, distinct purpose
  2. Separation of Concerns: Logic for creation, policy, and mechanism is isolated
  3. Testability:
    • Commands can be tested with mock objects
    • Actions can be tested by checking which commands they push
    • Factories can be tested for proper object initialization
  4. Lifetime Management: Registry and reference-counted IDs ensure object safety
  5. UI Agnosticism: Core Model and Command logic is independent of QML/Qt Widgets

This architecture ensures that Zrythm's codebase remains robust, flexible, and maintainable as it grows in complexity.