#pragma once // REAPER-facing shell of Design View: reads the live folder tree, runs // ViewModeModel's pure planner, and applies the resulting flag / lane / parent // writes — under ONE undo block on a real switch, under none at all on a reapply. // See src/shell/view/CLAUDE.md for the enforced invariants (never touch // master/mute, solo cached per mode and never lost, record-based restore). #include #include "core/view/view_mode_model.h" // Forward-declared to keep this header SDK-free; the .cpp includes the real SDK header. class ReaProject; namespace reasampler { // True while `proj`'s transport is playing or recording — the condition under which // applyMode refuses a real mode switch. Exposed so the panel can paint the footer's // [Arrange|Design] segments disabled BEFORE the click rather than only refusing on // it. `proj` is passed through to GetPlayStateEx as-is; the SDK documents proj=0 // meaning "current project" for GetTrack but is silent on GetPlayStateEx, so // `nullptr` meaning the current project here is inferred by analogy, not confirmed. bool transportBlocksModeSwitch(ReaProject* proj); // True while an applyMode call is on the stack. applyMode fails closed when // re-entered, and this lets a caller with no fallback test BEFORE it spends a // 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 // writes, then sets the active mode. ONE switch is ONE undo point; everything it // moved rolls back in a single Ctrl-Z. A REAPPLY leaves no point at all — it opens no // block, because none of what it writes mints one on its own. // // FX are NOT written here. Offlining a parked chain is the reclaimer's job, off this // path: TrackFX_SetOffline was 1897ms of a measured 1942ms switch, against 0.02ms for // all the flag work below. Do not reintroduce a synchronous FX write. // // 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) — // 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. // // `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. ModeApplyResult applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject* proj); } // namespace reasampler