// See view.h. Compiled into the reaper_reasampler module; includes // reaper_plugin_functions.h without REAPERAPI_IMPLEMENT (main.cpp owns that). // Tree arithmetic lives in view_tree (pure), one track's park/restore in view_park // and the lane writes in view_lanes; this file owns the plan iteration, parent // visibility, and the one undo block they all ride. #include "shell/view/view.h" #include #include #include #include #include "core/view/solo_cache.h" #include "core/view/view_tree.h" #include "shell/view/fx_reclaim.h" #include "shell/view/view_lanes.h" #include "shell/view/view_park.h" #include "shell/view/view_reconcile.h" #include "shell/view/view_solo.h" #include "shell/view/view_tracks.h" #define REAPERAPI_MINIMAL #define REAPERAPI_WANT_EnumProjects #define REAPERAPI_WANT_GetPlayStateEx #define REAPERAPI_WANT_Undo_BeginBlock2 #define REAPERAPI_WANT_Undo_EndBlock2 #define REAPERAPI_WANT_TrackList_AdjustWindows #define REAPERAPI_WANT_UpdateArrange #define REAPERAPI_WANT_UpdateTimeline #define REAPERAPI_WANT_ValidatePtr2 #include "reaper_plugin_functions.h" namespace reasampler { using view::buildFolderTree; using view::TrackFolderEntry; namespace { // GetPlayStateEx bitmask. SDK: &1 playing, &2 paused, &4 recording — paused is // deliberately excluded, nothing is moving there. constexpr int kTransportMoving = 1 | 4; // applyMode is reachable from the load tick, the actions and render_in_place. Nothing // it calls is known to pump the message loop now that the FX writes are gone, but a // re-entrant apply would plan against the same model the outer loop is iterating and // consume records out from under it — one dropped call is cheaper than that. Fail closed. bool g_applying = false; struct ApplyLatch { ApplyLatch() { g_applying = true; } ~ApplyLatch() { g_applying = false; } ApplyLatch(const ApplyLatch&) = delete; ApplyLatch& operator=(const ApplyLatch&) = delete; }; } // namespace bool transportBlocksModeSwitch(ReaProject* proj) { return (GetPlayStateEx(proj) & kTransportMoving) != 0; } bool modeApplyInProgress() { return g_applying; } namespace { bool applyModeImpl(ViewModeModel& model, const std::string& targetModeId, ReaProject* proj) { if (g_applying) return false; // see ApplyLatch; same fail-closed shape as the guards below ApplyLatch latch; if (!model.modes().contains(targetModeId)) { return false; // reject before touching the project — no partial apply } // Pinned ONCE: resolving "current project" independently at the begin and the end // would let a tab switch mid-apply open the block on one project and close it on // another. ReaProject* const project = proj ? proj : EnumProjects(-1, nullptr, 0); if (!ValidatePtr2(nullptr, project, "ReaProject*")) return false; // The discriminator behind both halves of the contract in view.h. A gate placed // unconditionally here would break tagging and the project-load reapply during // playback; a solo round on every reapply would flicker the user's solo on every // membership edit. const std::string outgoingModeId = model.activeModeId(); const bool realSwitch = targetModeId != outgoingModeId; if (realSwitch && transportBlocksModeSwitch(project)) { return false; // same fail-closed shape as the mode-exists guard above } TrackHandles handleByGuid; std::vector entries = readFolderEntries(project, handleByGuid); const TrackByGuid trackByGuid = indexHandles(handleByGuid); FolderTree tree = buildFolderTree(entries); // Prune records for tracks no longer in the live enumeration before planning // (membership is intentionally left alone — see model.reconcile). Because // reapply-on-load routes through applyMode, this also reconciles on project open. std::set liveGuids; for (const auto& kv : handleByGuid) liveGuids.insert(kv.first); model.reconcile(liveGuids); // Read before any write, and while activeModeId() still names the mode being left. const std::map outgoingSolo = realSwitch ? view::soloedTracks(readTrackSolos(handleByGuid)) : std::map{}; TogglePlan plan = model.planToggle(tree, targetModeId); // Target is guaranteed registered (checked at entry); fall back to the id // defensively if that ever changes. const Mode* targetMode = model.modes().query(targetModeId); const std::string undoLabel = "ReaSampler: activate " + (targetMode ? targetMode->displayName : targetModeId) + " view"; bool laneModeChanged = false; bool wrote = false; // anything at all this apply changed — see applyMintsUndoPoint std::vector exposedNames; // named while handles are fresh, reported after the block Undo_BeginBlock2(project); { UiRefreshHold uiHold; // every write below lands with the TCP/MCP frozen // DISJOIN THE SOLO SURFACES. Both this clear and the replay after // setActiveMode ride the existing undo block — one mode toggle stays one Ctrl-Z. if (realSwitch) { model.soloCache().store(outgoingModeId, outgoingSolo); clearTrackSolos(handleByGuid, outgoingSolo); } // PARK / RESTORE: view_park owns one track's whole move. Flags only — the // parked chain stays online until the reclaimer reaches it. for (const TrackPlan& tp : plan.park) { if (tp.flags.empty()) continue; // every op in a TrackPlan targets one track const std::string& guid = tp.flags.front().guid; MediaTrack* tr = resolve(trackByGuid, guid); if (!tr) continue; // stale/deleted GUID — prune const TrackParkResult r = parkTrack(model, guid, tr, tp.flags); if (r.wrote) wrote = true; // Named here, not after the block: TrackList_AdjustWindows below can // invalidate the handle. if (r.exposed) exposedNames.push_back(trackDisplayName(tr)); } for (const TrackPlan& tp : plan.restore) { if (tp.flags.empty()) continue; const std::string& guid = tp.flags.front().guid; MediaTrack* tr = resolve(trackByGuid, guid); if (!tr) continue; // stale/deleted GUID — prune if (restoreTrack(model, guid, tr, tp.flags)) wrote = true; } // MANAGED LANES: the active mode's lane plays+shows, every other managed lane // is silenced+hidden. Empty on a D1-only project — byte-identical there. const LaneApplyResult lanes = applyLaneOps(trackByGuid, plan.lanes); laneModeChanged = lanes.freeMode; if (lanes.wrote) wrote = true; // PARENT VISIBILITY (never parked): recomputed every toggle, never recorded. // Only the two visibility flags — never mainSend/FX on a parent. std::set visible = model.visibleTracks(tree, targetModeId); for (const FolderNode& node : tree.nodes) { if (!node.isParent) continue; MediaTrack* tr = resolve(trackByGuid, node.guid); if (!tr) continue; // stale GUID — prune const double show = visible.count(node.guid) ? 1.0 : 0.0; if (writeIfChanged(tr, "B_SHOWINTCP", show)) wrote = true; if (writeIfChanged(tr, "B_SHOWINMIXER", show)) wrote = true; } model.setActiveMode(targetModeId); // Replay + consume, before the single relayout below picks the change up. // Dropped against `visible` (computed above for parent visibility), not the // park plan: a folder parent can be hidden without being parked, and a hidden // track must not receive a replayed solo it carries no visible control to undo. if (realSwitch) { if (const std::map* cached = model.soloCache().query(targetModeId)) { restoreTrackSolos(handleByGuid, *cached, liveGuids, visible); model.soloCache().clear(targetModeId); } } } // Force REAPER to rebuild the TCP/MCP now rather than on the next user // interaction — AdjustWindows(false) is the full relayout tracks appearing and // disappearing owes. TrackList_AdjustWindows(false); UpdateArrange(); // UpdateTimeline() is owed only when a track was actually toggled into fixed lanes // this apply (SDK requirement for I_FREEMODE changes). if (laneModeChanged) UpdateTimeline(); const bool mint = applyMintsUndoPoint(realSwitch, wrote); Undo_EndBlock2(project, mint ? undoLabel.c_str() : "", mint ? kViewUndoMask : 0); // After the undo block: an EXPOSE report is a console message, not a project // write, and must not join what a Ctrl-Z rolls back. reportExposedTracks(project, exposedNames); // The tool's own account of what it just left driven — the only one that survives // an undo rolling the model back underneath it. noteAppliedState(project, model); return true; } } // namespace bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject* proj) { const bool applied = applyModeImpl(model, targetModeId, proj); // Every membership, snapshot and active-mode mutation in the system reaches the // project through this call, so this is the whole invalidation surface: the // reclaimer's queue is a cache of what the FX chains SHOULD be, never state. // // Dropped on a REFUSAL too, and that is the load-bearing half. Callers mutate the // model FIRST and reapply to make it visible (design_view_actions' tag/untag/ // show-both, render_in_place, the load tick), and every one of them discards this // return — so a refusal is exactly the case where the mutation stands and nothing // else would drop a queue that now converges toward the mode the user just left. // The cost when the refusal followed no mutation is one re-derivation on the timer. invalidateFxReclaim(); return applied; } } // namespace reasampler