// view.cpp — REAPER-facing Design View shell (Phase D2). See view.h. // // Compiled into the reaper_reasampler MODULE. Includes reaper_plugin_functions.h // WITHOUT REAPERAPI_IMPLEMENT — main.cpp is the one TU that defines the API // pointers; here they are extern (CLAUDE.md §contract). // // The tree arithmetic (I_FOLDERDEPTH -> FolderTree) lives in the pure view_tree // module so it is unit-tested outside the DAW; this file owns only the REAPER // reads/writes and the snapshot-before-park ordering. #include "view.h" #include #include #include #include "track_guid.h" #include "view_tree.h" #define REAPERAPI_MINIMAL #define REAPERAPI_WANT_CountTracks #define REAPERAPI_WANT_GetTrack #define REAPERAPI_WANT_GetMediaTrackInfo_Value #define REAPERAPI_WANT_SetMediaTrackInfo_Value #define REAPERAPI_WANT_TrackFX_GetCount #define REAPERAPI_WANT_TrackFX_GetOffline #define REAPERAPI_WANT_TrackFX_SetOffline #define REAPERAPI_WANT_Undo_BeginBlock2 #define REAPERAPI_WANT_Undo_EndBlock2 #include "reaper_plugin_functions.h" namespace reasampler { namespace { // The parmname for each planner Flag. All four are documented bool*/int* track // info params driven through the double-valued Get/SetMediaTrackInfo_Value API. const char* flagParm(Flag f) { switch (f) { case Flag::ShowInTcp: return "B_SHOWINTCP"; case Flag::ShowInMixer: return "B_SHOWINMIXER"; case Flag::MainSend: return "B_MAINSEND"; case Flag::FxEnable: return "I_FXEN"; } return "B_SHOWINTCP"; // unreachable; keeps the compiler quiet } // Reads the arrange-ordered track list and their I_FOLDERDEPTH, keyed by GUID. // The master track is NOT enumerated by GetTrack (index space is the non-master // tracks), so it can never enter the tree — the master-untouched invariant holds // by construction. Also caches the MediaTrack* per GUID so later apply steps // resolve a GUID back to its handle without a second linear scan. std::vector readFolderEntries( ReaProject* proj, std::vector>& handleByGuid) { std::vector entries; int count = CountTracks(proj); entries.reserve(static_cast(count)); handleByGuid.reserve(static_cast(count)); for (int i = 0; i < count; ++i) { MediaTrack* tr = GetTrack(proj, i); if (!tr) continue; std::string guid = guidString(tr); if (guid.empty()) continue; int depth = static_cast(GetMediaTrackInfo_Value(tr, "I_FOLDERDEPTH")); entries.push_back(TrackFolderEntry{guid, depth}); handleByGuid.emplace_back(guid, tr); } return entries; } MediaTrack* resolve(const std::vector>& handleByGuid, const std::string& guid) { for (const auto& kv : handleByGuid) { if (kv.first == guid) return kv.second; } return nullptr; // stale/deleted GUID — pruned by being skipped } // Captures a track's prior driven-flag state BEFORE it is parked. Reads only the // four owned flags + per-FX offline; never B_MUTE/I_SOLO, never the master (not // reachable here). ints preserve whatever REAPER reported (defensive per D1's // TrackSnapshot contract). TrackSnapshot snapshotTrack(MediaTrack* tr) { TrackSnapshot snap; snap.showInTcp = static_cast(GetMediaTrackInfo_Value(tr, "B_SHOWINTCP")); snap.showInMixer = static_cast(GetMediaTrackInfo_Value(tr, "B_SHOWINMIXER")); snap.mainSend = static_cast(GetMediaTrackInfo_Value(tr, "B_MAINSEND")); snap.fxEnable = static_cast(GetMediaTrackInfo_Value(tr, "I_FXEN")); int fxCount = TrackFX_GetCount(tr); snap.fxOffline.reserve(static_cast(fxCount)); for (int fx = 0; fx < fxCount; ++fx) { snap.fxOffline.push_back(TrackFX_GetOffline(tr, fx) ? 1 : 0); } return snap; } // Applies the planner's scalar-flag writes. B_* are bool* params, I_FXEN is int*, // all driven through the double API — marshal the plan's int value to double. void applyFlags(MediaTrack* tr, const std::vector& flags) { for (const TrackFlagOp& op : flags) { SetMediaTrackInfo_Value(tr, flagParm(op.flag), static_cast(op.value)); } } // Parks a track's FX offline: the pure park plan leaves fxOffline empty by design; // the shell expands it from the live FX count and offlines every slot. void parkFxOffline(MediaTrack* tr) { int fxCount = TrackFX_GetCount(tr); for (int fx = 0; fx < fxCount; ++fx) { TrackFX_SetOffline(tr, fx, true); } } // Restores per-FX offline from the snapshot verbatim — each slot back to its // captured value, never a blanket "online". Bounds-checked against the live FX // count in case the plugin chain changed while parked (prune-safe). // // HAZARD (deferred, PLAN "reconcile on delete/restructure"): the remap is by // slot INDEX, not plugin identity. If the FX chain changed while the track was // parked, snapshot slot k is restored onto whatever plugin now occupies slot k — // the bounds-check guards against out-of-range, not against a reshuffled chain. // Acceptable for D2; full identity-based reconciliation is future hardening. void restoreFxOffline(MediaTrack* tr, const std::vector& fxOffline) { int fxCount = TrackFX_GetCount(tr); for (const FxOfflineOp& op : fxOffline) { if (op.fxIndex < 0 || op.fxIndex >= fxCount) continue; TrackFX_SetOffline(tr, op.fxIndex, op.offline); } } } // namespace bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject* proj) { // Reject an unregistered target before touching the project (no partial apply). if (!model.modes().contains(targetModeId)) { return false; } std::vector> handleByGuid; std::vector entries = readFolderEntries(proj, handleByGuid); FolderTree tree = buildFolderTree(entries); TogglePlan plan = model.planToggle(tree, targetModeId); Undo_BeginBlock2(proj); // PARK: snapshot BEFORE mutating, store into the model (so restore survives a // save-while-parked), then apply the park writes + expand the FX-offline loop. for (const TrackPlan& tp : plan.park) { // Every op in a TrackPlan targets the same track; take the guid from the // first flag op (the pure park plan always emits the four flag ops). if (tp.flags.empty()) continue; const std::string& guid = tp.flags.front().guid; MediaTrack* tr = resolve(handleByGuid, guid); if (!tr) continue; // stale GUID — prune model.storeSnapshot(guid, snapshotTrack(tr)); applyFlags(tr, tp.flags); parkFxOffline(tr); } // RESTORE: apply the snapshot-sourced flag + per-FX offline writes verbatim, // then drop the now-consumed snapshot so a re-park recaptures fresh state. for (const TrackPlan& tp : plan.restore) { if (tp.flags.empty()) continue; const std::string& guid = tp.flags.front().guid; MediaTrack* tr = resolve(handleByGuid, guid); if (!tr) continue; // stale GUID — prune applyFlags(tr, tp.flags); restoreFxOffline(tr, tp.fxOffline); model.clearSnapshot(guid); } // PARENT VISIBILITY (derived, never parked): a folder is visible iff at least // one of its descendant leaves is visible in the target mode. That derivation is // pure (membership + active mode), so it is recomputed every toggle rather than // snapshotted — the four leaf-park flags don't apply to parents. Drive only the // two visibility flags; never touch B_MAINSEND/I_FXEN/FX-offline on a parent. std::set visible = model.visibleTracks(tree, targetModeId); for (const FolderNode& node : tree.nodes) { if (!node.isParent) continue; MediaTrack* tr = resolve(handleByGuid, node.guid); if (!tr) continue; // stale GUID — prune double show = visible.count(node.guid) ? 1.0 : 0.0; SetMediaTrackInfo_Value(tr, "B_SHOWINTCP", show); SetMediaTrackInfo_Value(tr, "B_SHOWINMIXER", show); } model.setActiveMode(targetModeId); Undo_EndBlock2(proj, "ReaSampler: apply Design View mode", -1); return true; } } // namespace reasampler