// 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 #include #include #include #include "item_read.h" #include "lane_keys.h" #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_GetSetMediaTrackInfo_String #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 #define REAPERAPI_WANT_TrackList_AdjustWindows #define REAPERAPI_WANT_UpdateArrange #define REAPERAPI_WANT_UpdateTimeline // Lane minting (D2 Wave 3): enumerate a track's items and read/write item-side lane // state to assign each item to its mode's managed lane. #define REAPERAPI_WANT_CountTrackMediaItems #define REAPERAPI_WANT_GetTrackMediaItem #define REAPERAPI_WANT_GetMediaItemInfo_Value #define REAPERAPI_WANT_SetMediaItemInfo_Value #include "reaper_plugin_functions.h" namespace reasampler { namespace { // Track fixed-lane mode value (I_FREEMODE=2). See SDK: 0=normal, 1=free item // positioning, 2=fixed lanes. constexpr int kFreeModeFixedLanes = 2; // C_LANESCOLLAPSED display value (char*). SDK: 1=lanes collapsed, // 2=track displays as non-fixed-lanes but hidden lanes exist. Value 2 is the lever that // makes a tool-split track read like a NORMAL single-lane track showing only the playing // lane — the inactive/silenced managed lanes are present but not drawn as separate rows. constexpr int kLanesDisplayAsNormal = 2; // C_LANESETTINGS bit (char* bitmask). SDK: &32=hide lane buttons. We OR this in (never // clobber the whole mask) to strip the per-lane button chrome from a tool-split track, so // it reads as an ordinary track. We deliberately do NOT set &1 (auto-remove empty lanes at // bottom): a managed lane whose item is later deleted would be silently removed out from // under the ownership index. The lazy-mint decision already avoids ever minting an empty // lane, so &1 buys nothing and risks a reconcile hazard. constexpr int kLaneSettingsHideButtons = 32; // Drives a TOOL-SPLIT track's display transparent: C_LANESCOLLAPSED=2 (render like a normal // single-lane track showing only the playing lane) + OR C_LANESETTINGS &32 (hide lane // buttons). Both are char* params driven through the double API, same convention as // C_LANEPLAYS:N. C_LANESETTINGS is read-modify-write so any pre-existing bit is preserved. // // MANAGED-VS-MANUAL BOUNDARY (load-bearing): these are TRACK-LEVEL settings that affect the // whole track including a user's own manual comp lanes. Every caller gates this on the // tool-driven transition INTO fixed lanes (freeMode != 2 before the flip), so a track the // user already had in fixed-lane mode never reaches it and the user's comp-lane display // prefs are never stomped. Idempotent: a re-run finds the track already at I_FREEMODE==2, // the transition branch is skipped, and these writes do not fire again. void applyTransparentLaneDisplay(MediaTrack* tr) { SetMediaTrackInfo_Value(tr, "C_LANESCOLLAPSED", static_cast(kLanesDisplayAsNormal)); const int settings = static_cast(GetMediaTrackInfo_Value(tr, "C_LANESETTINGS")); SetMediaTrackInfo_Value(tr, "C_LANESETTINGS", static_cast(settings | kLaneSettingsHideButtons)); } // 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); } } // -- Managed-lane application (D2 Wave 2) ------------------------------------ // // The pure planner emits LanePlayOps keyed by (trackGuid, laneKey) where laneKey is // the lane's DURABLE name (lane_keys convention: "reasampler:"). REAPER's // C_LANEPLAYS:N is keyed by the lane's CURRENT ORDINAL, which renumbers on reorder. // So before applying, we build the ordinal<->key reconcile for a track by reading each // lane's P_LANENAME:n; the write then targets the correct current ordinal for a given // durable key even after a reorder (design point #2). A lane whose name lacks the // managed prefix is manual and never appears in this map, so it can never be driven. // Reads lane index `laneIdx`'s durable name off track `tr` (P_LANENAME:n). Empty if // the lane is unnamed or the param is unavailable (non-fixed-lane track). std::string laneName(MediaTrack* tr, int laneIdx) { char parm[32]; std::snprintf(parm, sizeof(parm), "P_LANENAME:%d", laneIdx); char buf[512] = {0}; if (!GetSetMediaTrackInfo_String(tr, parm, buf, false)) return {}; return std::string(buf); } // Maps each MANAGED lane's durable key -> its current ordinal on `tr`, by walking the // track's I_NUMFIXEDLANES lanes and reading each name. Manual (unprefixed/unnamed) // lanes are omitted, so a key absent from the map is a lane the tool must not drive. std::map managedLaneOrdinals(MediaTrack* tr) { std::map byKey; const int numLanes = static_cast(GetMediaTrackInfo_Value(tr, "I_NUMFIXEDLANES")); for (int lane = 0; lane < numLanes; ++lane) { std::optional key = managedLaneKey(laneName(tr, lane)); if (key) byKey.emplace(*key, lane); // first ordinal wins if names collide } return byKey; } // Drives one managed lane on `tr` to `lanePlays` (C_LANEPLAYS value) via the // TRACK-SIDE C_LANEPLAYS:N write. Track-side C_LANEPLAYS:N alone produces the // hide+silence effect for all items on lane N — no per-item write is needed or // possible (item-side C_LANEPLAYS is marked read-only in the SDK). // B_FIXEDLANE_HIDDEN is READ-ONLY (SDK) — hide/show follows from C_LANEPLAYS=0/1, // never written directly. Non-destructive: only reversible play/show flags; no item // is moved or deleted. // // DAW-VERIFY: confirm that track-side C_LANEPLAYS:N alone hides+silences all items // on lane N without a per-item write. (SDK marks item-side C_LANEPLAYS as read-only; // the track-side write is the documented mechanism.) void applyLanePlays(MediaTrack* tr, int laneIdx, int lanePlays) { char parm[32]; std::snprintf(parm, sizeof(parm), "C_LANEPLAYS:%d", laneIdx); SetMediaTrackInfo_Value(tr, parm, static_cast(lanePlays)); } // Applies the plan's managed-lane ops. Groups ops by track, resolves each op's durable // laneKey to the track's current ordinal (skipping any key not present on the live // track — a stale/renamed/deleted managed lane is pruned, never mis-driven), enables // fixed-lane mode on any track that carries a managed lane, and drives C_LANEPLAYS. // UpdateTimeline() is called ONCE at the end (SDK: required after I_FREEMODE changes). // Returns true if any track's I_FREEMODE was (re)set to fixed lanes (⇒ needs timeline // refresh). MANAGED lanes only — plan.lanes never contains a manual lane (pure planner // gates on the ownership index), and a manual lane's name never resolves to a key here, // so the invariant is enforced twice. bool applyLaneOps(const std::vector>& handleByGuid, const std::vector& lanes) { if (lanes.empty()) return false; // Group op indices by track guid so we read each track's lane map once. std::map> byTrack; for (const LanePlayOp& op : lanes) byTrack[op.trackGuid].push_back(&op); bool touchedFreeMode = false; for (const auto& [guid, ops] : byTrack) { MediaTrack* tr = resolve(handleByGuid, guid); if (!tr) continue; // stale GUID — prune // Ensure fixed-lane mode is on before driving lane play state. A track carrying // a managed lane must be in I_FREEMODE=2; set it only if not already, and flag // that a timeline refresh is owed. Every track reaching this loop is already in the // managed-lane ownership index (planToggle only emits ops for managed lanes), so a // track here is one the TOOL split — a re-assert of fixed-lane mode is a tool-driven // (re)split and must carry the same transparent display, mirroring applyMintPlan's // transition branch. It is never a user's untouched manual-fixed-lane track. const int freeMode = static_cast(GetMediaTrackInfo_Value(tr, "I_FREEMODE")); if (freeMode != kFreeModeFixedLanes) { SetMediaTrackInfo_Value(tr, "I_FREEMODE", static_cast(kFreeModeFixedLanes)); applyTransparentLaneDisplay(tr); // tool-managed track ⇒ read like a normal track touchedFreeMode = true; } // Reconcile durable keys -> current ordinals on THIS track, then drive each op. const std::map ordinals = managedLaneOrdinals(tr); for (const LanePlayOp* op : ops) { auto it = ordinals.find(op->laneKey); if (it == ordinals.end()) continue; // key not live on this track — prune applyLanePlays(tr, it->second, op->lanePlays); } } return touchedFreeMode; } // -- Managed-lane minting (D2 Wave 3) ---------------------------------------- // // Mints one managed fixed lane per mode on any track that now holds content of MORE // THAN ONE mode, and assigns each item to its mode's managed lane. The DECISION — // which tracks split, which lanes to mint, which item goes where — is the pure // planLaneMinting; this shell only reads live per-item mode+lane state, calls the // decision, and applies the resulting REAPER + ownership-index writes. // Item GUID + fixed-lane name reads come from the shared item_read seam (item_read.h): // itemGuid(it) and itemLaneName(tr, it). view.cpp no longer carries its own copies. // Maps every item GUID on `tr` to its MediaItem* handle, in one pass. The assign pass // resolves plan item GUIDs back to handles through this map rather than re-scanning the // track per item (avoids the quadratic that a per-item find would incur). std::map itemHandlesByGuid(MediaTrack* tr) { std::map byGuid; const int itemCount = CountTrackMediaItems(tr); for (int i = 0; i < itemCount; ++i) { MediaItem* it = GetTrackMediaItem(tr, i); if (!it) continue; std::string ig = itemGuid(it); if (!ig.empty()) byGuid.emplace(std::move(ig), it); } return byGuid; } // Resolves the mode one item's content belongs to, from the model's membership index. // An item tagged into exactly one mode returns that mode; an untagged item is an // Arrange member by default (mirrors leafBelongsToMode's untagged rule). A show-both or // multi-mode item resolves to its first mode id — such items are unusual for lane // content, and the pure decision only needs A mode per item; the managed-lane it lands // on is that mode's lane. Never returns empty for a real item. std::string itemModeFromMembership(const ViewModeModel& model, const std::string& itemGuid) { const std::set modes = model.membership().modesOf(itemGuid); if (modes.empty()) return kArrangeModeId; // untagged ⇒ Arrange default return *modes.begin(); } // Builds the per-track LaneItem picture the pure decision consumes. For each track and // each item: resolve the item's mode from membership, and — only on a track already in // fixed-lane mode — read whether it sits on a MANUAL lane (exempt). On a non-fixed-lane // track no item is on a manual lane (isOnManualLane returns false for the empty name), // so the manual read is skipped entirely there. std::vector readLaneTracks( const ViewModeModel& model, const std::vector>& handleByGuid) { std::vector tracks; tracks.reserve(handleByGuid.size()); for (const auto& [guid, tr] : handleByGuid) { LaneTrack lt; lt.trackGuid = guid; const bool fixedLane = static_cast(GetMediaTrackInfo_Value(tr, "I_FREEMODE")) == kFreeModeFixedLanes; const int itemCount = CountTrackMediaItems(tr); lt.items.reserve(static_cast(itemCount)); for (int i = 0; i < itemCount; ++i) { MediaItem* it = GetTrackMediaItem(tr, i); if (!it) continue; const std::string ig = itemGuid(it); if (ig.empty()) continue; LaneItem li; li.guid = ig; li.modeId = itemModeFromMembership(model, ig); // Manual-lane exemption: only meaningful on a fixed-lane track. The shared // pure predicate decides; on a normal track it returns false regardless of // name, so we pass an empty name and skip the P_LANENAME read. const std::string ln = fixedLane ? itemLaneName(tr, it) : std::string{}; li.onManualLane = isOnManualLane(fixedLane, ln); lt.items.push_back(std::move(li)); } tracks.push_back(std::move(lt)); } return tracks; } // Assigns item `it` to the managed lane whose durable key resolves to a current ordinal // on `tr` (via managedLaneOrdinals). Idempotent: writes I_FIXEDLANE only when it differs // from the item's current lane, so a re-run does not thrash the item or the undo state. // Returns true iff a write actually changed the item's lane. Non-destructive: only the // reversible I_FIXEDLANE flag is written — the item is never moved in time or across // tracks. (I_FIXEDLANE is settable per SDK: "fine to call with setNewValue".) bool assignItemToLane(MediaTrack* tr, MediaItem* it, int laneOrdinal) { const int current = static_cast(GetMediaItemInfo_Value(it, "I_FIXEDLANE")); if (current == laneOrdinal) return false; // already there — no-op SetMediaItemInfo_Value(it, "I_FIXEDLANE", static_cast(laneOrdinal)); return true; } // Applies the pure LaneMintPlan to the live project. For each track that must split: // enables fixed lanes, ensures the lane count, stamps each managed lane's durable name, // records ownership in the model, then assigns each item to its mode's lane by resolving // the durable key to the lane's current ordinal. Returns true if ANY project write // changed state (⇒ the caller keeps the Undo block and refreshes the timeline). // // MANAGED-LANES-ONLY: the plan only ever names lanes with the managed prefix and only // ever assigns managed-eligible items (manual-lane items were reported exempt and are // absent from the plan). We only ever GROW I_NUMFIXEDLANES to fit the managed lanes and // stamp names on the lanes we mint — a user's existing manual lanes keep their ordinals // below/around ours and are never renamed or reassigned. bool applyMintPlan(ViewModeModel& model, const LaneMintPlan& plan, const std::vector>& handleByGuid) { bool changed = false; // Group mints + assigns by track so each track is set up once. std::map> mintsByTrack; for (const LaneMint& m : plan.mints) mintsByTrack[m.trackGuid].push_back(&m); std::map> assignsByTrack; for (const LaneAssign& a : plan.assigns) assignsByTrack[a.trackGuid].push_back(&a); for (const LaneMintPlan::TrackSplit& split : plan.splits) { MediaTrack* tr = resolve(handleByGuid, split.trackGuid); if (!tr) continue; // stale GUID — prune // Enable fixed-lane mode if not already (SDK: UpdateTimeline() owed after). The // pre-write freeMode read is ALSO the managed-vs-manual boundary signal: a track that // was NOT in fixed-lane mode here is one the TOOL is splitting now, so the tool owns // its lane display and drives it transparent. A track already at I_FREEMODE==2 (user // had fixed lanes, or a prior tool run) skips this branch — its C_LANESCOLLAPSED / // C_LANESETTINGS are left exactly as the user set them. const int freeMode = static_cast(GetMediaTrackInfo_Value(tr, "I_FREEMODE")); if (freeMode != kFreeModeFixedLanes) { SetMediaTrackInfo_Value(tr, "I_FREEMODE", static_cast(kFreeModeFixedLanes)); applyTransparentLaneDisplay(tr); // tool-split track ⇒ read like a normal track changed = true; } // Ensure enough lanes for the managed set WITHOUT shrinking: a track may already // carry the user's manual lanes, so only GROW the count, never reduce it (which // would delete a user lane). The managed lanes we mint occupy the tail ordinals. // laneCount tracks the live I_NUMFIXEDLANES as we grow it: read ONCE here, then // each mint appends at laneCount and bumps it. No per-mint I_NUMFIXEDLANES re-read // is needed — nextOrdinal and laneCount are the same running value. int laneCount = static_cast(GetMediaTrackInfo_Value(tr, "I_NUMFIXEDLANES")); // Which managed keys are already present on this track (durable-name reconcile). std::map present = managedLaneOrdinals(tr); // Mint each managed lane that is not already present, appending at the tail so an // existing manual lane is never overwritten. Record ownership in the model. for (const LaneMint* m : mintsByTrack[split.trackGuid]) { model.lanes().setManaged(m->trackGuid, m->laneKey, m->modeId); // ownership if (present.count(m->laneKey)) continue; // already minted — idempotent // Append at the current tail ordinal, grow the tracked count, stamp its name. const int laneIdx = laneCount++; SetMediaTrackInfo_Value(tr, "I_NUMFIXEDLANES", static_cast(laneCount)); char parm[32]; std::snprintf(parm, sizeof(parm), "P_LANENAME:%d", laneIdx); std::vector name(m->laneKey.begin(), m->laneKey.end()); name.push_back('\0'); GetSetMediaTrackInfo_String(tr, parm, name.data(), true); present.emplace(m->laneKey, laneIdx); // now resolvable for the assign pass changed = true; } // Assign each item to its mode's managed lane, resolving the durable key to the // lane's current ordinal on THIS track. A key not present (shouldn't happen — we // just minted them all) is skipped rather than mis-assigned. Item handles are // resolved through a one-pass GUID map (avoids re-scanning the track per item). const std::map ordinals = managedLaneOrdinals(tr); const std::map itemsByGuid = itemHandlesByGuid(tr); for (const LaneAssign* a : assignsByTrack[split.trackGuid]) { auto ord = ordinals.find(a->laneKey); if (ord == ordinals.end()) continue; // key not live — prune, never mis-assign auto handle = itemsByGuid.find(a->itemGuid); if (handle == itemsByGuid.end()) continue; // stale item GUID — prune if (assignItemToLane(tr, handle->second, ord->second)) changed = true; } } return changed; } } // 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); // Reconcile orphaned model state BEFORE planning: prune snapshots whose track was // deleted from the project (its GUID no longer appears in the live enumeration). // handleByGuid holds every currently-enumerated track GUID, so its keys are the // authoritative live set. Membership is intentionally NOT pruned (undo-delete // restores the same GUID — see ViewModeModel::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); 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 // Snapshot ONCE, at the first park. If a snapshot already exists the track is // still parked from a prior apply, and its live flags are the PARKED (hidden) // values — recapturing here would overwrite the true pre-park state with zeros, // so a later restore would restore the track to hidden and it would vanish for // good. Re-applying the park flags to an already-parked track is idempotent and // fine; only the snapshot must not be recaptured. Restore clears the snapshot, // so the next genuine park recaptures fresh state. if (model.snapshot(guid) == nullptr) 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); } // MANAGED LANES (D2 item-level projection): drive C_LANEPLAYS so the active mode's // managed lane plays+shows and every inactive-mode managed lane is silenced+hidden. // plan.lanes carries MANAGED lanes only (the pure planner gates on the ownership // index); applyLaneOps additionally resolves each op's durable key against the live // track's lane names, so a manual lane — which never carries the managed prefix — // can never be driven. Empty for a D1-only project (no fixed lanes), leaving D1 // behavior byte-identical. UpdateTimeline() is owed only if a track's I_FREEMODE // was (re)set to fixed lanes (SDK requirement); deferred to the refresh block below. const bool laneModeChanged = applyLaneOps(handleByGuid, plan.lanes); // PARENT VISIBILITY (never parked): visibleTracks() marks a parent visible when // a descendant leaf is visible in the target mode OR the parent belongs to the // mode by its own membership (untagged folder → Arrange default). Recomputed // every toggle rather than snapshotted. 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); } // Build the undo label from the ACTUAL target mode's display name, so activating // Arrange doesn't leave an "activate Design view" undo point (and vice versa). // The target is guaranteed registered (checked at entry), so query() is non-null; // 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"; model.setActiveMode(targetModeId); // Force REAPER to rebuild the TCP + MCP so visibility/park changes appear now, // not on the user's next TCP interaction. TrackList_AdjustWindows(false) does the // major (full) relayout required when tracks appear/disappear from the panels; // UpdateArrange() repaints the arrange view. Both are documented for exactly this // "you changed track-info flags, now refresh the panels" case. TrackList_AdjustWindows(false); UpdateArrange(); // A fixed-lane mode change (I_FREEMODE -> 2) requires UpdateTimeline() to take // visible effect (SDK). Call it only when we actually toggled a track into fixed // lanes this apply; the C_LANEPLAYS writes themselves are picked up by the arrange // refresh above. if (laneModeChanged) UpdateTimeline(); Undo_EndBlock2(proj, undoLabel.c_str(), -1); return true; } bool mintManagedLanes(ViewModeModel& model, ReaProject* proj) { std::vector> handleByGuid; std::vector entries = readFolderEntries(proj, handleByGuid); // The minting decision is now folder-tree / visibility aware: it needs the tree to // detect a content-bearing folder derived-visible in >1 mode (which must lane-separate // its own media even when that media is single-mode). Build it exactly as applyMode does. const FolderTree tree = buildFolderTree(entries); // Build the live per-track item picture and run the PURE decision. A track visible in // exactly one mode produces no split; a track visible in >1 mode while carrying its own // media (own items span modes, OR a folder derived-visible across modes) produces mints // + assignments. Manual-lane items are reported exempt inside readLaneTracks; show-both // tracks are skipped inside the decision. const std::vector tracks = readLaneTracks(model, handleByGuid); const LaneMintPlan plan = planLaneMinting(model, tree, tracks); if (plan.empty()) return false; // nothing to mint — no Undo point for a no-op tick // Wrap the structural mutation in ONE Undo block (unlike the invisible membership // tag). Only opened when the plan is non-empty; applyMintPlan reports whether any // write actually changed state so we can label the undo meaningfully. Undo_BeginBlock2(proj); const bool changed = applyMintPlan(model, plan, handleByGuid); if (!changed) { // The plan was non-empty but every write was already satisfied (idempotent // re-run: lanes exist, items already assigned, ownership already recorded). Close // the block with no description so REAPER discards the empty undo point rather // than flooding history with a no-change entry every detection tick. Undo_EndBlock2(proj, "", 0); return false; } // Reapply the active mode's lane visibility so the freshly-minted lanes take their // correct play/show state immediately: the active mode's lane plays+shows, every // other managed lane hides+silences. Reusing planToggle's lane ops keeps the drive // logic in one place; applyLaneOps also (re)asserts I_FREEMODE and drives C_LANEPLAYS. // NOTE: applyMode is NOT reused here — it would re-park/restore whole tracks and // recompute parent visibility, which the minting tick must not do (it only just // changed item lanes). Driving lane play state directly is the minimal correct step. const TogglePlan togglePlan = model.planToggle(FolderTree{}, model.activeModeId()); applyLaneOps(handleByGuid, togglePlan.lanes); // I_FREEMODE was (re)set to fixed lanes on at least one track (the plan minted a // split), so a timeline refresh is owed (SDK). Repaint the arrange too so the new // lane layout appears immediately. UpdateTimeline(); UpdateArrange(); Undo_EndBlock2(proj, "ReaSampler: separate cross-mode content into lanes", -1); return true; } void reconcileManagedLanes(ViewModeModel& model, ReaProject* proj) { std::vector> handleByGuid; readFolderEntries(proj, handleByGuid); // populates handleByGuid (tree unused here) // Walk every track's lanes; for each lane whose durable name carries the managed // prefix, record it MANAGED-for-its-mode in the ownership index. This is a pure READ // of REAPER state (no lane is created, no I_FREEMODE/I_NUMFIXEDLANES/I_FIXEDLANE is // written) plus an index write — self-healing classification from the source of // truth (the durable name) without re-minting or mass-tagging. A lane lacking the // prefix is left alone (manual by default), so a user's own lanes stay off the index. for (const auto& [guid, tr] : handleByGuid) { const int freeMode = static_cast(GetMediaTrackInfo_Value(tr, "I_FREEMODE")); if (freeMode != kFreeModeFixedLanes) continue; // no fixed lanes ⇒ nothing managed const int numLanes = static_cast(GetMediaTrackInfo_Value(tr, "I_NUMFIXEDLANES")); for (int lane = 0; lane < numLanes; ++lane) { const std::string name = laneName(tr, lane); std::optional key = managedLaneKey(name); if (!key) continue; // manual/unnamed lane — leave off the index std::optional mode = modeIdFromLaneName(name); if (!mode) continue; // prefix-only/illegal name — skip defensively // UNREGISTERED-MODE GUARD: the durable name encodes a mode id, but that mode // may no longer be a registered Mode (e.g. a mode removed from the registry // after the project was saved with lanes minted for it). Recording it MANAGED // would make the toggle planner drive a lane keyed to a mode that can never be // the active mode — the lane would stay silenced+hidden forever, orphaning its // items with no way for the user to reach them. So we do NOT record it: the // lane is left off the ownership index and thus treated as manual-by-default // (never driven). Its durable name is preserved on the track, so if the mode is // ever re-registered a later reconcile recovers the ownership cleanly. if (!model.modes().contains(*mode)) continue; model.lanes().setManaged(guid, *key, *mode); } } } } // namespace reasampler