Design View: no undo block may open over the reclaim span, and a reapply closes none

Every opener ends the span first, so a tag fired mid-drain no longer hijacks the
switch's point. One stated criterion governs both lane sites; no-op actions discard.
This commit is contained in:
2026-08-07 15:13:52 -04:00
parent 39529a5ac4
commit b2f6b3f1d0
9 changed files with 210 additions and 76 deletions
+23 -4
View File
@@ -27,6 +27,21 @@ bool transportBlocksModeSwitch(ReaProject* proj);
// one-shot signal on a call that would be refused — see main.cpp's load glue.
bool modeApplyInProgress();
// What one applyMode did.
//
// `wroteProjectState` counts REAPER writes ONLY — a driven flag, a lane parameter,
// a parent's visibility. Model moves (a snapshot stored, a record's flag half spent,
// the active mode set) are deliberately excluded: they reach disk through
// persistViewState, which every caller runs OUTSIDE its undo block, so no point can
// carry them and counting them would only mint an empty one. It is the answer a
// caller holding its own block needs for the discard decision (design_view_actions'
// ActionUndoBlock); the solo pair cannot affect it, since only a real switch drives
// solo and a real switch mints regardless.
struct ModeApplyResult {
bool applied = false;
bool wroteProjectState = false;
};
// Captures each about-to-park track's prior flag values into `model`, caches/clears
// the outgoing mode's solo state and replays the incoming mode's, runs planToggle,
// applies the park/restore flag writes plus managed-lane and parent-visibility
@@ -41,16 +56,20 @@ bool modeApplyInProgress();
// A REAL switch therefore RETURNS WITH ITS UNDO BLOCK STILL OPEN (fx_reclaim's
// UndoSpan): the reclaimer closes it once the parking has drained, which is what keeps
// the switch and its FX writes ONE point rather than two. A reapply opens none, so a
// caller that wants its own act undoable opens its own (design_view_actions does).
// caller that wants its own act undoable opens its own (design_view_actions does) —
// and, because REAPER's blocks are a depth counter, ENDS THE SPAN BEFORE IT OPENS
// THAT BLOCK. A reapply never closes a span, precisely because it can be running
// inside such a caller's block.
//
// Returns false (no mutation) if `targetModeId` isn't registered, if this is a real
// switch (target != active) while the transport is running, if it is re-entered
// `applied` is false (no mutation) if `targetModeId` isn't registered, if this is a
// real switch (target != active) while the transport is running, if it is re-entered
// (modeApplyInProgress), or if the project does not validate. A reapply (target ==
// active) is never gated and never touches solo. `proj` == nullptr means the current
// project, resolved ONCE and used for every call this apply makes.
//
// Either way — applied or refused — this drops the FX reclaimer's derived queue, which
// is why callers may safely discard the return after mutating the model.
bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject* proj);
ModeApplyResult applyMode(ViewModeModel& model, const std::string& targetModeId,
ReaProject* proj);
} // namespace reasampler